CLI: --arrange forces align_to_y_axis on i3 printers, so --allow-rotations=0 rotates every object 90 degrees
Is there an existing issue for this problem?
- I have searched the existing issues (#12612 and #13087 are about
--orient; this is--arrange)
OrcaSlicer Version
2.4.2 release (Windows). The code is unchanged on main (src/OrcaSlicer.cpp, the arrange_cfg.align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3); lines).
Operating System (OS)
Windows
OS Version
11
Printer
Bambu Lab A1 (any printer whose profile has printer_structure = i3)
How to reproduce
A project with six 200 x 60 x 5 mm two-colour slabs stacked on plate 1 (they overlap, so --arrange has to lay them out). Generator script below, no models needed.
orca-slicer.exe --arrange 1 --outputdir out1 --export-3mf out.3mf stack6.3mf
orca-slicer.exe --arrange 1 --allow-rotations=0 --outputdir out2 --export-3mf out.3mf stack6.3mfThen read the build item transforms out of each out.3mf (3D/3dmodel.model).
Actual results
| command | rotation of every slab | layout |
|---|---|---|
--arrange 1 (rotations allowed, the default) |
0 deg | 4 on plate 1 as rows, 2 on plate 2 |
--arrange 1 --allow-rotations=0 (rotations forbidden) |
90 deg | 4 on plate 1 as columns, 2 on plate 2 |
Forbidding rotation turns every object 90 degrees. Allowing it leaves them alone.
Cause: the CLI forces "Align to Y axis" on for i3 printers and there is no option to turn it off:
src/OrcaSlicer.cpp (2.4.2 lines 4779 and 5229, both --arrange paths):
if (auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure")) {
arrange_cfg.align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3);
}src/libslic3r/Arrange.cpp:158 update_selected_items_axis_align then pre-rotates every selected object so its principal axis lies along Y. With rotations allowed the nester tries {0, 45, 90, 135} on top of that and can turn the slab back to 0 (it did). With --allow-rotations=0 it tries {0} only, so the pre-rotation is the result: every object 90 degrees from how it was loaded.
The GUI reads the same flag from the arrange dialog checkbox (src/slic3r/GUI/Jobs/ArrangeJob.cpp:782, params.align_to_y_axis = settings.align_to_y_axis), so a GUI user can arrange an A1 plate at 0 degrees; a CLI user cannot.
Why it matters: objects arranged at 90 degrees have their top-surface and infill lines running across the part at a different angle from neighbours arranged at 0, which is visible on flat parts. --allow-rotations=0 is the switch a CLI user reaches for to keep the loaded orientation, and it does the opposite.
Two smaller things seen on the way:
--allow-rotations 0(space) is not accepted:0is taken as an input file name and the CLI exits-3(file not found). Only--allow-rotations=0parses. The--helptext does not say so.- With
--allow-rotations=0the objects are also re-ordered onto plates differently from the allowed case (columns instead of rows), which is expected once they are 90 degrees, mentioned so nobody chases it.
Expected results
--allow-rotations=0 keeps every object at the orientation it was loaded with. Either
- do not force
align_to_y_axisin the CLI (default it off, as the GUI checkbox defaults), or - expose it:
--align-to-y-axis=0/1, so a CLI caller can choose like a GUI user can.
Reproducer
make_boxes_3mf.py <project_settings.config> stack6.3mf 6 --one-plate writes the project. <project_settings.config> is the Metadata/project_settings.config from any project saved by the GUI for a Bambu A1 0.4 nozzle, 0.20mm Standard, two Generic PLA filaments (that is the only thing the script does not make up: the printer/process/filament settings).
"""Write a design-free multi-plate Orca project: N two-colour slabs, one per plate.
python make_boxes_3mf.py <settings.json from any A1 project> out.3mf [N] [--one-plate]
Each object = a 200 x 60 x 4 mm base (filament 1) with a 180 x 40 x 1 mm slab on top
(filament 2), so the plate is two-colour and keeps its prime tower. Plates sit on Orca's
grid (cols = ceil(sqrt(N)), gap 1/5 of the bed). --one-plate stacks every object at the
centre of plate 1 (they overlap: something --arrange has to fix)."""
import json, math, sys, uuid, zipfile
BW, BD = 256.0, 256.0
GAP = 0.2
def box(x0, y0, z0, x1, y1, z1):
v = [(x0, y0, z0), (x1, y0, z0), (x1, y1, z0), (x0, y1, z0),
(x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)]
t = [(0, 2, 1), (0, 3, 2), (4, 5, 6), (4, 6, 7), (0, 1, 5), (0, 5, 4),
(1, 2, 6), (1, 6, 5), (2, 3, 7), (2, 7, 6), (3, 0, 4), (3, 4, 7)]
vs = "".join('<vertex x="%g" y="%g" z="%g"/>' % p for p in v)
ts = "".join('<triangle v1="%d" v2="%d" v3="%d"/>' % q for q in t)
return "<mesh><vertices>%s</vertices><triangles>%s</triangles></mesh>" % (vs, ts)
def origin(k, n):
cols = math.isqrt(n - 1) + 1
return (k % cols) * BW * (1 + GAP), -(k // cols) * BD * (1 + GAP)
def main(settings_path, out, n=6, one_plate=False):
cfg = json.load(open(settings_path, encoding="utf-8"))
parts, objs, items, ms_objs, plates = [], [], [], [], []
for k in range(n):
base, top = 1000 * (k + 1) + 1, 1000 * (k + 1) + 2
parts.append('<object id="%d" p:UUID="%s" type="model">%s</object>' % (base, uuid.uuid4(), box(-100, -30, 0, 100, 30, 4)))
parts.append('<object id="%d" p:UUID="%s" type="model">%s</object>' % (top, uuid.uuid4(), box(-90, -20, 4, 90, 20, 5)))
oid = 100 + k
objs.append('<object id="%d" p:UUID="%s" type="model"><components>'
'<component p:path="/3D/Objects/parts.model" objectid="%d" p:UUID="%s" transform="1 0 0 0 1 0 0 0 1 0 0 0"/>'
'<component p:path="/3D/Objects/parts.model" objectid="%d" p:UUID="%s" transform="1 0 0 0 1 0 0 0 1 0 0 0"/>'
'</components></object>' % (oid, uuid.uuid4(), base, uuid.uuid4(), top, uuid.uuid4()))
ox, oy = (0.0, 0.0) if one_plate else origin(k, n)
items.append('<item objectid="%d" p:UUID="%s" transform="1 0 0 0 1 0 0 0 1 %g %g 0" printable="1"/>'
% (oid, uuid.uuid4(), BW / 2 + ox, BD / 2 + oy))
ms_objs.append(' <object id="%d">\n <metadata key="name" value="slab"/>\n <metadata key="extruder" value="1"/>\n'
' <part id="%d" subtype="normal_part">\n <metadata key="name" value="base"/>\n'
' <metadata key="matrix" value="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1"/>\n <metadata key="extruder" value="1"/>\n </part>\n'
' <part id="%d" subtype="normal_part">\n <metadata key="name" value="top"/>\n'
' <metadata key="matrix" value="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1"/>\n <metadata key="extruder" value="2"/>\n </part>\n'
' </object>' % (oid, base, top))
pk = 1 if one_plate else k + 1
plates.append(pk)
plate_xml = ""
for pk in sorted(set(plates)):
inst = "".join(' <model_instance>\n <metadata key="object_id" value="%d"/>\n <metadata key="instance_id" value="0"/>\n </model_instance>\n'
% (100 + k) for k in range(n) if plates[k] == pk)
plate_xml += ' <plate>\n <metadata key="plater_id" value="%d"/>\n <metadata key="plater_name" value=""/>\n <metadata key="locked" value="false"/>\n%s </plate>\n' % (pk, inst)
NS = ('xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:BambuStudio="http://schemas.bambulab.com/package/2021" '
'xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06" requiredextensions="p"')
model = ('<?xml version="1.0" encoding="UTF-8"?>\n<model unit="millimeter" xml:lang="en-US" %s>\n'
' <metadata name="Application">OrcaSlicer-2.4.2</metadata>\n <metadata name="BambuStudio:3mfVersion">1</metadata>\n'
' <resources>\n%s\n </resources>\n <build p:UUID="%s">\n%s\n </build>\n</model>\n'
% (NS, "\n".join(objs), uuid.uuid4(), "\n".join(items)))
parts_model = ('<?xml version="1.0" encoding="UTF-8"?>\n<model unit="millimeter" xml:lang="en-US" %s>\n'
' <metadata name="BambuStudio:3mfVersion">1</metadata>\n <resources>\n%s\n </resources>\n <build/>\n</model>\n'
% (NS, "\n".join(parts)))
modset = '<?xml version="1.0" encoding="UTF-8"?>\n<config>\n%s\n%s</config>\n' % ("\n".join(ms_objs), plate_xml)
# per-plate lists sized to the plate count
for key in ("wipe_tower_x", "wipe_tower_y"):
if key in cfg:
cfg[key] = [cfg[key][0]] * len(set(plates))
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("[Content_Types].xml", '<?xml version="1.0" encoding="UTF-8"?>\n<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">\n'
' <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>\n'
' <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>\n'
' <Default Extension="config" ContentType="application/xml"/>\n</Types>\n')
z.writestr("_rels/.rels", '<?xml version="1.0" encoding="UTF-8"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n'
' <Relationship Target="/3D/3dmodel.model" Id="rel-1" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>\n</Relationships>\n')
z.writestr("3D/_rels/3dmodel.model.rels", '<?xml version="1.0" encoding="UTF-8"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n'
' <Relationship Target="/3D/Objects/parts.model" Id="rel-1" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>\n</Relationships>\n')
z.writestr("Metadata/project_settings.config", json.dumps(cfg, indent=4))
z.writestr("3D/Objects/parts.model", parts_model)
z.writestr("3D/3dmodel.model", model)
z.writestr("Metadata/model_settings.config", modset)
print("wrote", out, "objects", n, "plates", len(set(plates)))
if __name__ == "__main__":
a = [x for x in sys.argv[1:] if not x.startswith("--")]
main(a[0], a[1], int(a[2]) if len(a) > 2 else 6, one_plate="--one-plate" in sys.argv)Source: OrcaSlicer/OrcaSlicer