Compare commits

..
Author SHA1 Message Date
Hanif Koh 2b2c710626 Order GCodeWriter Initializers Like the Members Moved to Protected
The lift, speed and cached-extruder members now live in the protected section ahead of the private ones; list their initializers first so the list reads in construction order. No behaviour change.
2026-09-14 17:27:33 +08:00
Hanif Koh 2b1a7e38df Drop Redundant Belt Checks in BeltGCode
BeltGCode is only created for belt printers, so its hooks no longer re-check belt_printer, and the BBL-machine flag is set once on whichever writer survives init_belt_writer instead of on one about to be discarded.
2026-09-14 17:27:33 +08:00
Hanif Koh b4052ec99f Drop the Redundant Lift Type Alias in eager_lift
effective_type was a plain copy of the parameter.
2026-09-14 17:27:33 +08:00
Hanif Koh 14cf7861e0 Fix Belt Tooltip Spacing and Legend Casing
Drop double spaces in the belt tilt tooltips and match the preview legend header to
the "Belt printer" settings group.
2026-09-14 17:27:33 +08:00
Hanif Koh 18c08862a8 Align up_direction Position in TriangleSelector Fill Calls
seed_fill_select_triangles() now takes up_direction right after highlight_by_angle_deg, as select_patch() does.
2026-09-14 17:27:33 +08:00
Hanif Koh 62ef0fa4f8 Write the Standard Layer Change Tag on Belt Brim Apron Layers
Apron layers appended print_z to the layer change tag, unlike every other layer; write the plain tag line.
2026-09-14 17:27:33 +08:00
Hanif Koh a8a45439db Share One Build Plate Tilt Up-Direction Helper Across the GUI
The bed gravity arrow, volume rendering and the painter/support gizmos each rebuilt
the tilt up-vector from build_plate_tilt_x/y; use one helper that also tolerates
presets without the keys.
2026-09-14 17:27:33 +08:00
Hanif Koh 5cd6661280 Check the Belt Temperature Tower Model Load
Bail out like the other calibration paths when add_model() fails instead of
indexing the empty model.
2026-09-14 17:27:33 +08:00
Hanif Koh 33aec258da Skip CLI Wipe Tower Reservation on Belt Printers
Print::has_wipe_tower() is always false for belt printers, but CLI arrange, plate checks and the pre-slice tower clamp still reserved a phantom tower footprint and wrote a clamped wipe_tower_x/y into the config.
2026-09-14 17:27:33 +08:00
Hanif Koh 76aba24ddf Number Belt Extension Support Layers Sequentially
Extension layers were all created with id 0, so every one of them could be taken for the first layer by id-only checks (ooze-prevention standby temperature, cached layer ids). Renumber the support layers after inserting them.
2026-09-14 17:27:33 +08:00
Hanif Koh cb5b489e90 Invalidate Only G-code Export for Belt Output Options
gcode_back_transform, first_layer_plane* and belt_printer_infinite_y fell through to invalidate_all_steps(), which re-ran tool ordering, skirt/brim and G-code export on toggles that only affect G-code export.
2026-09-14 17:27:33 +08:00
Hanif Koh 19a085206c Print Belt Brim Aprons in Each Object's Brim Filament
Apron-only layers printed every band with the first tool, so objects with different brim filaments at the same apron Z shared one filament. Emit each brim filament's bands with its own toolchange.
2026-09-14 17:27:33 +08:00
Hanif Koh 708212a306 Share One Belt Brim Band Loop Between Apron-Only and Ordinary Layers
The ordinary-layer path kept its own copy of the apron band loop. Give emit_belt_brim_bands() an optional brim filament filter and call it from the per-extruder lambda; without a filter it still prints every band, so apron-only layers are unchanged.
2026-09-14 17:27:33 +08:00
Hanif Koh 3b302b4666 Derive Belt Support Tilt From Slicing Rotation in Print::apply
build_plate_tilt_x/y was synced from belt_slice_rotation* only by the printer Tab, so CLI or 3MF edits of the rotation left the support tilt stale.
2026-09-14 17:27:32 +08:00
Hanif Koh 4a7311bf01 Rebuild the Brim Type Combobox Only When Its Entries Change
toggle_options() now runs on every value change and mode switch; rebuild the
brim_type choices only when the leading-edge entry has to be added or removed.
2026-09-14 17:27:32 +08:00
Hanif Koh f2a11928f6 Read the Belt Tilt Only from the Belt G-code Header
Every printer's config block lists belt_slice_rotation_angle (default 45), so the processor marked all G-code as belt G-code: imported flat G-code got the belt view on a belt printer, and the belt-only Z handling in the processor ran for non-belt prints whose config block precedes the body. Take the angle only from outside the config block, where only the belt header writes it.
2026-09-14 17:27:32 +08:00
Hanif Koh 4ed56954e8 Limit Build Plate Tilt Range Below 90 Degrees
A 90 degree tilt has no finite gravity drift per layer, so the option range
now stops at 89 degrees, matching the cap applied by the support generators.
2026-09-14 17:27:32 +08:00
Hanif Koh 6e33f3f5dd Share and Clamp the Build Plate Tilt Shift in Support Generators
The three support generators each computed lh * tan(tilt), which overflows
coord_t at 90 degrees and flips sign beyond it (belt sync can write up to
180). One helper now returns the tilt slope with the tilt capped at 89 degrees.
2026-09-14 17:27:32 +08:00
Hanif Koh 5092831007 Apply the Belt Slicing Transform to Painted Supports and Seams
Painted support/seam facets, support volumes, seam occlusion, MMU and fuzzy skin painting (top/bottom
and side facets) and the adaptive infill octree used trafo_centered(), or trafo() with a centre-offset
shift, while the layers were sliced with the belt rotation, remap and Z lift; they now share
PrintObject::trafo_sliced().
2026-09-14 17:27:32 +08:00
Hanif Koh 9cca3093ee Keep the Plate Offset When Swapping in the Belt Writer
The belt writer replaced the plate-offset-carrying writer mid-export, so belt G-code for any plate but the first kept the plate origin and long-travel clipping used the wrong frame. GCode now remembers the offset and hands it to the new writer, and the writer's first-layer probes use the plate-local point it emits.
2026-09-14 16:41:40 +08:00
Hanif Koh 6873267a9c Remove Plate Tilt Keys from Per-Object Settings Tables
build_plate_tilt_x/y are printer-preset keys; listing them in the per-object
frequent-settings and object-table bundles stored ignored values in object
configs and crashed the object table on the process config lookup.
2026-09-14 16:41:40 +08:00
Hanif Koh 8e330f951a Merge Main into Belt Printer
Merge origin/main (00429da739) into belt-printer.

Conflicts resolved:
- src/CMakeLists.txt: keep both wxInspector workarounds.
- GCodeProcessor.cpp: keep the belt compare_pos / z_for_height lines.
- PrintObjectSlice.cpp: the belt bbox-Z guard also covers main's
  printable_region_ids bookkeeping.
- TreeSupport.cpp: the belt-floor check runs before main's PendingNode
  queueing.
- Tab.hpp: keep the belt fields, drop the removed upload description
  fields.
- tests/libslic3r/CMakeLists.txt: keep both test files.

Also included:
- eSUN PLA belt presets declare their own filament_id (OFkrxQC4) and
  scripts/filament_id_snapshot.json is regenerated, as main's filament_id
  check requires.
- Custom.json version bumped to 02.04.00.05 so the belt entries reach
  existing installs.
- Fix the ambiguous WithinRel call in the belt apron width test, which
  otherwise breaks the fff_print build.
2026-09-14 16:33:08 +08:00
Joseph Robertson c96945490b Belt Printer Sept 1 Rebase (#15526)
Also a bunch of bug fixes, thanks to the Baby Belt community for finding
issues!
2026-09-03 13:35:23 -05:00
harrierpigeon e5d4ad2aa7 Merge remote-tracking branch 'upstream/main' into haryr/aug25-rebase
# Conflicts:
#	src/libslic3r/Support/TreeSupport.cpp
2026-08-30 23:31:48 -05:00
harrierpigeonandClaude Opus 5 4fab8d0b39 fix: adapt belt sub-layer group emission to upstream m_writer unique_ptr
Upstream changed GCode::m_writer from a value to std::unique_ptr<GCodeWriter>;
the belt mixed_sub_layer_groups path still used value syntax and did not compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ChxXYc6Dp46qAN9c2rQCe
2026-08-30 23:31:21 -05:00
harrierpigeon e341a9b84e Merge remote-tracking branch 'upstream/main' into haryr/aug25-rebase
# Conflicts:
#	src/libslic3r/GCode/ToolOrdering.cpp
#	src/libslic3r/Print.cpp
#	src/libslic3r/PrintApply.cpp
#	src/libslic3r/PrintConfig.cpp
#	src/slic3r/GUI/Tab.cpp
2026-08-30 23:30:51 -05:00
harrierpigeon a7bc054974 docs: authorize private build notifications 2026-08-25 10:27:21 -05:00
harrierpigeon 30351d40e1 tests: adapt belt brim coverage to upstream validation 2026-08-25 10:27:08 -05:00
harrierpigeon d289478618 Merge remote-tracking branch 'upstream/main' into haryr/aug25-rebase
# Conflicts:
#	resources/profiles/Custom.json
#	src/libslic3r/Brim.cpp
#	src/libslic3r/GCode.cpp
#	src/libslic3r/GCode.hpp
#	src/libslic3r/Preset.cpp
#	src/slic3r/GUI/3DScene.cpp
#	src/slic3r/GUI/ConfigManipulation.cpp
#	src/slic3r/GUI/GLCanvas3D.cpp
#	src/slic3r/GUI/Plater.cpp
2026-08-25 06:50:46 -05:00
Joseph Robertson 306e379a2a Multicolor Belt Support & various bug fixes (#15361)
# Description
lots of small bugfixes, and multicolor belt support.
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-24 23:23:47 -05:00
harrierpigeon 3d11b60e71 Harden belt purge tower replanning 2026-08-08 20:14:52 -05:00
Joseph Robertson e8597fe942 Merge pull request #61 from HarrierPigeon/belt/purgeTower
Add Purge Tower and Finalize Multicolor Support
2026-08-08 18:59:00 -05:00
harrierpigeon 99ee8893cd Fix belt purge tower activation and placement safety 2026-08-08 17:14:26 -05:00
harrierpigeon ee3e014f02 allow belt purge to skip unnecessary purge volume 2026-08-08 16:35:55 -05:00
harrierpigeon 3d270c2aa7 workable belt purge, via N-1 individual "purge objects" 2026-08-08 16:35:55 -05:00
harrierpigeon 5ea6ccc56a cleanup, early purge tower stop if no longer necessary 2026-08-08 16:35:55 -05:00
harrierpigeon bcfb09481c pull purge tower into its own files, make purge tower semi-transparent like other purge towers 2026-08-08 16:35:55 -05:00
harrierpigeon c80f1ab312 cancel top of purge tower early if no extra parts to print 2026-08-08 16:35:55 -05:00
harrierpigeon 131b61b726 auto purge tower height calculation works 2026-08-08 16:35:55 -05:00
harrierpigeon d367bcef92 extra height compensation 2026-08-08 16:35:55 -05:00
harrierpigeon 79c93733d3 purge tower additional compensation 2026-08-08 16:35:55 -05:00
harrierpigeon 7cc50d750c automated placement works 2026-08-08 16:35:55 -05:00
harrierpigeon 62d8f22f52 purge tower still centered on X max 2026-08-08 16:35:55 -05:00
harrierpigeon 60e9ee26c9 strategy incremental 2 2026-08-08 16:35:55 -05:00
harrierpigeon 854dae8dd2 strategy incremental 2026-08-08 16:35:54 -05:00
harrierpigeon ec4e9717d3 Part Two: Functional Results 2026-08-08 16:35:54 -05:00
harrierpigeon 88726d76e8 Purge tower part 1 2026-08-08 16:35:54 -05:00
Joseph Robertson 39b087d6ea fix non 45 degree slicing methods (#15181)
# Description
During the UI/UX improvements about a month ago, I got the transforms
wrong, and slicing at anything other than a 45 degree angle was
affected.

Validated on a baby belt pro at 30 & 45 degrees.


[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-08 12:37:17 -05:00
harrierpigeon 8b2e28817d fix non 45 degree slicing methods after regression created while cleaning up UI 2026-08-08 12:33:41 -05:00
Joseph Robertson ac9b5433b7 Belt printer: regression fixes + Belt Printer Brims (#15155) fixes (#15156)
## Summary

Fixes for the `belt-printer` branch ahead of upstreaming, in two groups
(10 commits). Targets `belt-printer` (not `main`) since group 2 fixes
the not-yet-merged Belt Printer Brims feature.

Every fix keeps non-belt (and brim-disabled) output unchanged; belt-only
behavior is corrected. All changed translation units and the two test
files were type-checked (`-fsyntax-only`); a full build + `ctest` still
needs to run in an environment with current deps.

## Group 1 — pre-existing belt-printer regressions

- **[HIGH] BuildVolume belt state not reset when leaving belt mode** —
toggling belt off (or switching belt→normal with matching bed geometry)
left the `BuildVolume` with `m_is_belt_printer=true` and inflated Y
bounds, so out-of-bounds objects were treated as printable on a normal
printer.
- **[HIGH] `GCodeProcessorResult::reset()` didn't clear belt fields**
(`belt_tilt_angle`, `belt_z_origin`, `preslice_remap_*`) — a reused
result corrupted a normal print's start-gcode preview Z.
- **[LOW-MED] `TreeSupport::drop_nodes`** — restored the single critical
section around node invalidation (the two `valid=false` writes had been
moved outside the mutex on the shared tree-support path); removed an
unused local.
- **[LOW] Support overhang hot paths** — avoid unconditional lower-layer
polygon copies when there is no build-plate tilt (`SupportMaterial`,
`TreeSupport3D`); untilted output matches upstream exactly.
- **[LOW] Render loop** — hoisted the frame-invariant slope
`up_direction`/`normal_z` (and their per-volume config lookup) out of
the per-volume loop.
- **[LOW] FDM-support "select by angle"** — restored the exact upstream
threshold when the build plate is untilted (the generalized form
differed for non-uniformly-scaled objects); tilted-gravity form kept
only under tilt.
- **[LOW] Printer tab tilt sync** — only clears the belt-derived
`build_plate_tilt` on a genuine in-place belt→off toggle (tracked,
seeded on preset load), no longer wiping a manually-set tilt.
- **[LOW / opt-in] Axis-remap G-code emission** — always emit full XYZ
under an active `gcode_remap_*`, apply the remap on all base
`travel_to_xyz` destinations, fall back to a linear lift for spiral/arc
under remap, sync `set_axis_remap` each export; fixed belt first-layer
travel speed. Identity/default output unchanged.

## Group 2 — Belt Printer Brims (#15155) fixes

- **[CRITICAL] Dropped brim at first belt contact** — a coincident brim
band on an object layer with no extrusion pass (zero-extrusion leading
slice, or belt support below the Z=0 floor with no coinciding object
extrusion) was never emitted. Now each coincident band's brim filament
is registered in `ToolOrdering`, each band is emitted exactly once in
its brim-filament pass, and an end-of-layer orphan sweep emits any band
whose object layer produced no visit.
- **[Multi-extruder] Wrong tool / double emission** — apron and
coincident bands now print once, in the correct brim-filament pass,
brim-first (were previously emitted with the active tool and could
double-emit per filament plan). Single-extruder / single-object output
is byte-identical apart from the previously-dropped bands now printing.
- **Inner-only predicate** — `has_belt_brim()` no longer reports a brim
(and no longer rejects the prime tower / spiral vase) for `inner_only` +
`brim_width=0` + leading/extra > 0, which produces no inner geometry;
mirrored in `wants_brim`.
- **ToolOrdering raft-gap comment** — clarified why raft-gap synthesis
is suppressed for all belt printers (belt has no rafts;
sub-object-bottom layers are apron / belt-support-below-floor /
lead-in). No behavior change.
- **Tests** — deterministic coverage: brim present at first belt contact
(support on/off), brim-before-perimeters once (no drop/double), single-
and multi-extruder tool selection with no doubling, multi-object
per-filament ordering, inner-only+leading-only not rejecting prime
tower/spiral, and inner-ring / leading-edge-only geometry units.

## Testing

- `-fsyntax-only` passes for all 16 changed source TUs + 2 test TUs
against this branch.
- Please run the full build and `ctest -R 'SkirtBrim|BeltBrim'` before
merging.

## Known follow-up (out of scope)

`extrude_arc_to_xy` does not remap its I-J center, so arc-fitted
*extrusions* under standalone axis-remap would be geometrically wrong —
a separate fix if that combination is supported.

Opened as **draft**.
2026-08-06 17:38:11 -05:00
harrierpigeon a453cb1eba tests: cover belt-brim first-contact emission, tool selection, inner/leading-edge, and predicate (E)
Deterministic tests for: coincident brim at first belt contact not dropped
(C), single- and multi-extruder brim tool selection with no doubling (B),
multi-object apron ordering, inner-only+leading-only not rejecting prime
tower/spiral (D), and inner/holed + leading-edge-only geometry.
2026-08-06 15:40:04 -05:00
harrierpigeon 1bd3404c03 Fix belt brim emission: dropped first-contact bands, tool selection, inner-only predicate (A,B,C,D)
- Emit coincident belt_brim_by_layer bands even when the leading object layer
  has no InstanceVisit (zero-extrusion lead-in / no coinciding support), so the
  brim at first belt contact is no longer dropped.
- Register each coincident band's brim filament in ToolOrdering and emit each
  band exactly once, in its brim-filament pass; emit ordinary-layer aprons in
  the brim pass before object extrusion (correct tool, brim-first) instead of
  with whatever tool was active.
- has_belt_brim(): inner-only brims need brim_width>0 (leading/extra produce no
  inner geometry), fixing spurious prime-tower/spiral rejection; mirror in
  wants_brim. Single-extruder/single-object output is unchanged except
  previously-dropped bands now print.
2026-08-06 15:40:04 -05:00
harrierpigeon c1a90fc451 Fix: correct axis-remap G-code emission and belt first-layer travel speed (B2, B3)
- BeltGCodeWriter::travel_to_xyz final branch used config.travel_speed
  instead of the computed first-layer-aware travel_speed.
- extrude_to_xyz decided emit_xyz vs emit_xy from pre-remap Z; emit full
  XYZ whenever an axis remap is active so remapped machine-Z is never
  dropped.
- base travel_to_xyz now applies apply_axis_remap() on all emitted
  destinations (standalone remap on non-belt printers was unremapped).
- spiral/arc travels fall back to normal linear lift under active remap
  (endpoint-only remap can't preserve arc plane/I-J).
- set_axis_remap() is now synced unconditionally each export to avoid a
  reused writer retaining a stale non-identity mapping.
2026-08-06 14:26:45 -05:00
harrierpigeon 7fc86db5f0 Fix: only clear belt-derived build_plate_tilt on genuine belt->off transition (R8)
update_fff() zeroed any build_plate_tilt matching the dormant belt-derived
tilt (default X/45) within 0.01, wiping a legitimate manual tilt on a
non-belt tilted-bed printer. Track the belt->non-belt transition and the
exact values belt-sync wrote, clearing only those on an in-place toggle;
reset tracking on preset load so preset switches never wipe tilt.
2026-08-06 14:26:45 -05:00
harrierpigeon 6fd2de76e6 Fix: preserve upstream select-by-angle behavior when build plate is untilted (R7)
select_facets_by_angle replaced upstream's limit.dot(down) threshold with
cos(threshold), changing facet selection for non-uniformly-scaled/mirror
objects on ALL printers. Restore the exact upstream computation when no
build-plate tilt is active; keep the tilted-gravity form only under tilt.
2026-08-06 14:26:45 -05:00
harrierpigeon f8fe5a07cd Perf: hoist frame-invariant slope up_direction/normal_z out of the per-volume render loop (R6)
Belt slope-shading changes recomputed up_direction (with a printer-preset
config lookup) and normal_z per volume; both are frame-invariant. Compute
once before the to_render loop and reuse the already-hoisted
support_normal_z. Uniforms are still set per volume; visuals unchanged.
2026-08-06 14:26:45 -05:00
harrierpigeon 049612022a Perf: avoid unconditional lower-layer polygon copies in support overhang paths (R4, R5)
SupportMaterial::detect_overhangs copied lower_layer_polygons per region
even without build-plate tilt; hoist the tilted copy out of the region
loop and use the original polygons directly when untilted. TreeSupport3D
flattened lslices_extrudable to Polygons unconditionally; restore the
upstream ExPolygons offset on the untilted path.
2026-08-06 14:26:45 -05:00
harrierpigeon 10810908a9 Fix: restore atomic node invalidation in TreeSupport::drop_nodes + drop unused var (R3, R9)
The 2-node merge moved the two valid=false writes outside the mutex that
upstream held together with the contact_nodes push_back; restore a single
critical section per branch (belt branch also guards to_buildplate).
Remove an unused top_interface_layers local in drop_nodes.
2026-08-06 14:26:45 -05:00
harrierpigeon 0d92180325 Fix: clear belt fields in GCodeProcessorResult::reset() (R2)
reset() cleared the sibling machine_frame_transform_active but not
belt_tilt_angle/belt_z_origin/preslice_remap_*; a reused result carried
stale belt metadata into a subsequent normal print, flipping the store_z
branch and corrupting start-gcode preview Z for non-belt prints.
2026-08-06 14:26:45 -05:00
harrierpigeon 88d5e9e442 Fix: reset BuildVolume belt state when leaving belt mode (R1)
Non-belt branch of set_bed_shape reset only the 3DBed renderer, not the
BuildVolume; Bed3D::set_shape early-returns on unchanged bed, so a
belt->normal switch or in-place belt toggle-off left the BuildVolume with
m_is_belt_printer=true and inflated Y bounds -> out-of-bounds objects
treated as printable on a normal printer.
2026-08-06 14:26:45 -05:00
Joseph Robertson c51d19f6b2 Add Belt Printer Brims (#15155)
# Description

This adds brim support to belt printers.

Added a new belt printer specific mode, Leading Edge Only and two new
belt-specific parameters, Leading Edge Brim Length, which increases the
number of brim lines on the side of the part printed first, and Extra
Brim Width, which increases the width of brims along the X axis. Because
belt printer first layers are effectively a single line, getting them to
stick properly can be a pain. This PR aims to help alleviate that, or at
least give more options for control.


<img width="1849" height="1043" alt="Screenshot from 2026-08-06
12-20-21"
src="https://github.com/user-attachments/assets/f963ed8e-53e7-48f8-a495-123cb9ae27f7"
/>



[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-06 14:18:21 -05:00
harrierpigeon 849e6493f8 Belt brim: offer "Leading edge only" only on belt printers
"Leading edge only" describes where a part meets a moving belt, so it has no
meaning on a fixed bed and should not clutter the Brim type dropdown there.

Filtered the same way support_style and wipe_tower_wall_type already are a few
lines above in TabPrint::toggle_options(): the field holds its own copy of the
option definition, and Choice maps the combobox selection straight onto that
copy's enum_values, so rewriting the values, the labels and the combobox items
together keeps the mapping correct.

The entry is kept when it is the current value, so opening a project that uses
it on a non-belt printer cannot leave the control displaying an option it does
not offer - which would silently rewrite the setting on the next edit.
Print::validate() already warns that it prints as an ordinary outer brim there.

Matches the scope of the existing precedents: the per-object override panel is
not filtered.
2026-08-06 03:54:19 -05:00
harrierpigeon b1905ebc20 Belt brim: fixes from review
Six issues found by reviewing the previous commit against belt-printer, two of
them release-blocking.

Data race (high).  Print::process() runs generate_support_material() for all
objects in a tbb::parallel_for, and make_belt_brim() runs at its tail, but
belt_brim_obstacles() read every OTHER object's support_layers() - which a
concurrent task may be inside clear_support_layers() deleting.  That is a
use-after-free, and even when it survives, the obstacle set depends on which
object finishes first.  Only this object's own supports are consulted now; they
are complete at that point.  Foreign objects still contribute their slices,
which are finished and immutable before the support phase.

Apron bands dropped (high), two separate causes.  An apron band prints below
its own object's first layer, but another object can already be printing at
that print_z, in which case process_layer() takes the ordinary path and never
emitted the band - the emission is now shared by both paths.  Separately, a
band whose print_z matched a support layer of the SAME object was overwritten
in the print-wide merge, which keeps one record per object per z and could not
detect the collision because LayerToPrint::layer() is null for a band.  The
per-object pairing loop is now a three-way merge over object, support and apron
streams, so each object contributes at most one record per z.

Multi-instance was far too strict (medium).  It refused belt brim for every
multi-instance object, killing plain brim width and inner brim too, and only
warned when a leading length was set.  Only movement ALONG the belt changes an
instance's belt-floor Z, so copies side by side ACROSS the belt share one set of
bands perfectly well; belt_brim_instances_compatible() now tests just that, and
the warning fires whenever the brim is actually suppressed.

Apron layer bookkeeping (medium).  Apron layers count toward m_layer_count and
advance m_layer_index, but emitted no Z/height tags, left m_last_layer_z,
m_max_layer_z and m_last_height stale - so the first object layer computed its
height against a pre-apron Z - and skipped before_layer_change_gcode and
layer_change_gcode entirely.  All of that now matches the ordinary path.

Obstacle cost (low).  belt_brim_obstacles() ran a full-plate union per band.
A bounding-box pre-filter drops non-overlapping objects before materialising any
polygon, and the union is skipped for trivial inputs.

Deliberately unchanged: every apron band still reports cooling layer_id 0.
CoolingBuffer uses it for the initial_layer_fan_speed override and the
close_fan_the_first_x_layers gate, and every band lies on the belt plane itself,
so it is all first-layer material by the only definition that means anything on
a belt.  Numbering the bands would ramp the fan up while still printing on the
belt.  Now documented at the assignment rather than left implicit.
2026-08-06 01:08:44 -05:00
harrierpigeon 55b4dca9bc Belt printers: brim laid onto the tilted belt, with a leading apron
A belt printer slices in a rotated frame, so the belt surface is a tilted
plane rather than the Z=0 bed plane.  Each slicing layer touches the belt
only along a narrow strip at its leading edge - about 0.2mm at 45 degrees -
so a part's first layer is really a first line, with almost no contact patch
to hold it down while the belt drags it forward.  Brim was hard-disabled on
belt printers, leaving no remedy at all.

Generate the brim on the belt plane instead.  The object's belt footprint is
the union over layers of each slice clipped to that layer's contact band; the
brim is offset from it in a "flattened" frame where the shear axis is
stretched by 1/cos(tilt), so ordinary Clipper offsets measure true on-belt
distance.  It is emitted as cross-belt lines, one per layer band, anchored to
a fixed fraction of the band so every line shares a nozzle-to-belt clearance
and therefore comes out the same width; flow is matched to the resulting band
pitch, keeping the sheet uniform and gap-free.

Three new controls, all belt-only:

  * Leading brim length - extends the brim ahead of the part along the belt,
    on every downhill-facing edge of its contact area.  This apron necessarily
    prints BELOW the object's first layer, since layer 0 is the part's leading
    contact, so it needs brim-only bands of its own.
  * Extra brim width - widens the brim sideways across the belt only.
  * Brim type "Leading edge only" - brim at the part's first belt contact and
    nothing after it.  Appended last in BrimType so no existing value shifts;
    degrades to an outer brim off belt printers, with a warning.

The apron bands are lightweight records rather than a Layer subclass, so no
fabricated Layer::id() can leak into initial-layer temperature selection, the
spiral vase probe, cooling or gradual interpolation.  They are generated in
posSupportMaterial because their print_z values must exist before ToolOrdering
is built at psWipeTower, and they are emitted from a short dedicated branch in
process_layer that runs before any layer pointer is dereferenced.

The footprint is closed before offsetting outwards: a belt contact patch is
often a broken-up strip, and the merged offset rings of two islands closer
than 2 x brim_width would otherwise fill the space between them - space that
lies under the part.

Also fixes a pre-existing bug where PrintObject::get_first_layer_bbox()
overwrote a valid bbox with an unassigned one on any belt printer with a brim
configured, because has_brim() was true while make_brim() returned early.

Belt brim is refused alongside the prime tower and spiral vase, and requires
one instance per PrintObject - translating an instance along the belt axis
changes its physical belt-floor Z.  Untilted belt printers are unchanged: they
still get no brim, since the plate brim is emitted out of skirt_brim_groups(),
which _make_skirt() never builds for a belt printer.
2026-08-06 01:08:44 -05:00
Joseph Robertson 386364f84b belt profiles: fix belt printer CI failures (slice check + setting_id) (#15127)
The belt-printer branch is failing two profile gates. Both stem from the
three belt-only vendors (Custom's generic belt printer, IdeaFormer,
Printcepts) not existing upstream, so upstream maintenance passed them
by.

Slice check: 4 of 1015 printers failed - Custom's MyBeltPrinter 0.2/0.4/
0.6/0.8 nozzle all fell back to "Default Setting". No process profile in
the Custom vendor listed any MyBeltPrinter in compatible_printers, and
Custom's fdm_belt_common pointed default_print_profile at "0.20mm
Standard @System", which does not exist in that vendor's index, so the
generic belt printer had no usable process at all. This gap dates to
when MyBeltPrinter was added (2026-04-07); it only started failing now
because the slice-check job is newer than that.

Adds two process profiles modelled on the sibling @MyKlipper ones:
  - 0.20mm Standard @MyBeltPrinter - 0.4/0.6/0.8 nozzles
  - 0.12mm Fine @MyBeltPrinter     - 0.2/0.4 nozzles
The split is forced by hardware: the 0.2 nozzle preset caps
max_layer_height at 0.16, so a single 0.20mm profile cannot legally
cover
it. fdm_belt_common now defaults to the standard profile and the 0.2
nozzle preset overrides to the fine one.

setting_id: 14 files failed the rules introduced in #14432. That
migration renumbered 7425 files across 61 vendors but skipped these
three, leaving BabyBelt Pro, IdeaFormer IR3 V2 and MyBeltPrinter
squatting the "G*" id space reserved for Bambu (GMPC0BBP01, GMIF001,
GM_BELT_00x) and four instantiated filament/process presets carrying no
setting_id at all. Regenerated with
scripts/assign_vendor_setting_ids.py.

Also repoints the identical dangling "0.20mm Standard @System" in
Printcepts' and IdeaFormer's fdm_belt_common at their own real process
profiles. That is a no-op today because both concrete printers override
it, but it is the same landmine that took out MyBeltPrinter.

Vendor index versions bumped so check_installed_vendor_profiles() will
re-install the corrected profiles over an existing install.

Note: changing a shipped preset's setting_id can orphan user presets
that reference it as base_id. #14432 accepted that tradeoff for 61
vendors; this keeps these three consistent with the rest.

Verified: orca_extra_profile_check.py reports 0 errors across 66 vendors
(was 14 files with errors), and OrcaSlicer_profile_validator -s slices
all 1015 printer presets successfully (was 4 failures).


[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-04 17:16:14 -05:00
harrierpigeon 725df64108 profiles: fix belt printer CI failures (slice check + setting_id)
The belt-printer branch is failing two profile gates. Both stem from the
three belt-only vendors (Custom's generic belt printer, IdeaFormer,
Printcepts) not existing upstream, so upstream maintenance passed them by.

Slice check: 4 of 1015 printers failed - Custom's MyBeltPrinter 0.2/0.4/
0.6/0.8 nozzle all fell back to "Default Setting". No process profile in
the Custom vendor listed any MyBeltPrinter in compatible_printers, and
Custom's fdm_belt_common pointed default_print_profile at
"0.20mm Standard @System", which does not exist in that vendor's index,
so the generic belt printer had no usable process at all. This gap dates
to when MyBeltPrinter was added (2026-04-07); it only started failing now
because the slice-check job is newer than that.

Adds two process profiles modelled on the sibling @MyKlipper ones:
  - 0.20mm Standard @MyBeltPrinter - 0.4/0.6/0.8 nozzles
  - 0.12mm Fine @MyBeltPrinter     - 0.2/0.4 nozzles
The split is forced by hardware: the 0.2 nozzle preset caps
max_layer_height at 0.16, so a single 0.20mm profile cannot legally cover
it. fdm_belt_common now defaults to the standard profile and the 0.2
nozzle preset overrides to the fine one.

setting_id: 14 files failed the rules introduced in #14432. That
migration renumbered 7425 files across 61 vendors but skipped these three,
leaving BabyBelt Pro, IdeaFormer IR3 V2 and MyBeltPrinter squatting the
"G*" id space reserved for Bambu (GMPC0BBP01, GMIF001, GM_BELT_00x) and
four instantiated filament/process presets carrying no setting_id at all.
Regenerated with scripts/assign_vendor_setting_ids.py.

Also repoints the identical dangling "0.20mm Standard @System" in
Printcepts' and IdeaFormer's fdm_belt_common at their own real process
profiles. That is a no-op today because both concrete printers override
it, but it is the same landmine that took out MyBeltPrinter.

Vendor index versions bumped so check_installed_vendor_profiles() will
re-install the corrected profiles over an existing install.

Note: changing a shipped preset's setting_id can orphan user presets that
reference it as base_id. #14432 accepted that tradeoff for 61 vendors;
this keeps these three consistent with the rest.

Verified: orca_extra_profile_check.py reports 0 errors across 66 vendors
(was 14 files with errors), and OrcaSlicer_profile_validator -s slices all
1015 printer presets successfully (was 4 failures).
2026-08-04 17:15:20 -05:00
Joseph Robertson c5bf238859 Update Belt-Printer Branch (#15087)
gets belt-printer on top of upstream again.
2026-08-03 02:09:41 -05:00
harrierpigeon f563df04f6 belt: default first_layer_plane to Auto, not BeltAffine
BeltAffine activates the FirstLayerPlane evaluator unconditionally, so on a
non-belt printer on_first_layer(point) stopped agreeing with the legacy
slicing-layer-0 test. Every per-path first-layer call site in _extrude then
took the non-first-layer branch, and first-layer speeds were skipped: brim
came out at the volumetric fallback (24.6 mm/s) instead of initial_layer_speed
(10 mm/s). This is the shared speed path, so it affected all printers on this
branch, not just belt ones.

Auto resolves to BeltAffine only when belt_printer is set with a non-zero
slicing rotation, and to XY (evaluator inactive, legacy behaviour) otherwise --
exactly what the option's own description already promised.

Caught by "Brim uses first layer speed" (upstream #14616), which arrived with
the upstream merge; the bad default dates back to a9bae54f20 (#30). Verified
against a pristine upstream/main build, which passes the same test.

tests/fff_print: 100/100 test cases, 1085 assertions (was 99/100).
Both belt regression tests still pass, confirming Auto still resolves to
BeltAffine for belt printers.

Note: this changes a config default. Projects and profiles that stored
first_layer_plane explicitly are unaffected; those relying on the default will
now get correct first-layer speeds on non-belt printers, so their G-code
changes accordingly.
2026-08-03 01:52:43 -05:00
harrierpigeon 613dad92a1 Add belt-printer regression test for prepare-stage move Z
Processes a minimal belt start sequence through GCodeProcessor::process_buffer
and asserts the move preceding the first extrusion keeps its real Z, so it can
no longer back-transform to model Y~=0 and produce the phantom extrusion line.

Belt printers are non-Bambu, so the processor uses the compatible reserved
tags ("TYPE:"); the test sets s_IsBBLPrinter=false (saved/restored via an RAII
guard) to mirror the real printer. Proven to fail without the fix (the
prepare-stage move's Z is pinned to the first-layer height, 0 here) and pass
with it.
2026-08-03 01:18:55 -05:00
harrierpigeon a83cd8aa29 Fix belt printer phantom extrusion line from Y=0 in preview
On a belt printer the sliced preview drew a stray extrusion-colored line
from Y~=0 to the model, rendered in the first extrusion role's color. It is
not a travel and does not occur on non-belt printers.

GCodeProcessor::store_move_vertex pins a move's stored Z to the first-layer
height during the start-G-code "prepare" stage. That is a harmless cosmetic
tidy-up on a normal printer, but on a belt printer the designed-view
back-transform couples machine Z into the rendered model Y (the belt tilt
mixes the height and belt-feed axes). Pinning Z back-transforms the last
prepare-stage move (the unretract before the first extrusion) to model
Y ~= 0, and libvgcode then draws a phantom extrusion segment from Y ~= 0 to
the first real toolpath.

Keep the real Z for belt printers (gated on belt_tilt_angle, parsed from the
G-code header before the body) so prepare-stage moves back-transform
correctly. Non-belt processing is byte-identical. The emitted G-code was
already correct; this is a preview-geometry fix.
2026-08-03 01:09:23 -05:00
harrierpigeon 02e313a115 Add belt-printer regression test for start-of-print gantry move
Locks in the fix from the previous commit. A fresh BeltGCodeWriter has an
unestablished planar position (is_current_position_clear() == false) and its
m_pos.xy is the origin (0,0). With a pending NormalLift z-hop, travel_to_xyz
used to lift in place via _travel_to_z(), which in belt mode shears the origin
into a machine Y ~= the layer Z — a move far up the gantry.

The test configures an X-tilt 45 deg belt transform, defers a z-hop via
lazy_lift, travels to a near-belt first point (transformed gantry Y ~= 1mm),
and asserts no emitted move has Y anywhere near the layer Z. Verified to fail
without the fix (max emitted Y = 100.0 vs the destination's ~1.0) and pass with
it.
2026-08-03 01:09:11 -05:00
harrierpigeon 04554abae6 Fix belt printer illegal gantry move at print start
On a belt printer the first travel of the print emitted a bogus move to
the bed corner with the nozzle far up the gantry, e.g.
  G1 X95 Y168.19 Z237.857 F12000
right after the first "; printing object" line. Y168 (≈ the layer Z)
is out of the gantry's range.

Root cause: the layer-change z-hop is deferred via lazy_lift and consumed
by the first BeltGCodeWriter::travel_to_xyz, whose NormalLift branch does a
separate lift-in-place via _travel_to_z(target.z()). On a normal printer
_travel_to_z emits a Z-only move, but in belt mode Z is coupled to Y/X, so
_travel_to_z re-emits the current m_pos through the belt shear. At print
start (and after custom gcode) m_pos.xy is still the uninitialised origin
(0,0), which the back-transform + axis-remap shear into machine
(X=bed_max, Y=layer_z) — the illegal move.

Guard the NormalLift branch on is_current_position_clear(), matching the
SlopeLift branch directly above it which already does so. When the position
isn't established there is nothing to lift over, and the xy_z_move that
follows travels straight to the destination with full XYZ, establishing the
correct position. Bookkeeping is unaffected: in this path m_lifted stays 0,
so no spurious restore move is produced.

Verified by re-slicing the repro project: the start-of-print move is now
G1 X44.946 Y.621 Z237.857 (straight to the first object point), no move
touches the bed-max X edge, and the max Y over the whole file is 62.8mm
(printable_height 100).
2026-08-03 00:15:24 -05:00
HarrierPigeon 0342e06d87 last step in fixing the g-code stuff up 2026-08-02 22:13:34 -05:00
HarrierPigeon 79fd847ce3 fix pre-slice warnings 2026-08-02 22:12:46 -05:00
HarrierPigeon 8f6802fff8 step one: post-process analysis 2026-08-02 22:12:09 -05:00
harrierpigeon b61ba98183 belt: adapt BeltGCodeWriter to upstream's per-extruder speed options
Upstream retyped travel_speed and travel_speed_z to ConfigOptionFloatsNullable
and initial_layer_travel_speed to ConfigOptionFloatsOrPercentsNullable, so the
scalar .value / get_abs_value() accessors no longer compile. BeltGCodeWriter.cpp
is belt-only and merged without conflict, so this only surfaced at build time.

Index them the way the base GCodeWriter does -- .get_at(m_cached_extruder_idx)
and get_abs_value_at(..., m_cached_extruder_idx) -- keeping belt's per-point
first_layer_for_point test rather than the base class's m_is_first_layer.

m_cached_extruder_idx moves from private to the existing protected block that
already exposes writer state to subclasses, so the belt writer resolves the
per-extruder index identically to the base writer instead of guessing one.
2026-08-02 16:20:22 -05:00
harrierpigeon 175075fd08 Merge upstream/main into belt-printer
Brings the belt-printer work up to date with 591 upstream commits.

Conflict resolutions (12 files, 42 hunks):

- GCode.cpp: adopted upstream's per-filament/per-nozzle config refactor
  (get_filament_config_index, NOZZLE_CONFIG), the extracted
  generate_timelapse_gcode + farthest-point timelapse, and the
  ConfigOptionFloatsNullable calibration options. Re-applied the belt
  hooks on top: init_belt_writer / axis remap / FirstLayerPlane setup,
  on_set_origin, the belt-corrected calib_z for the volumetric speed
  tower, and path_on_first_layer (belt's per-path first-layer test) in
  place of upstream's layer-index on_first_layer() in the acceleration,
  jerk and overhang-detection paths. Swept upstream's new m_writer.
  uses to m_writer-> since belt holds the writer by unique_ptr.
- interpolate_value_across_layers: kept upstream's banded stepping and
  belt's object-Z-span ratio; dropped upstream's duplicate ratio decl.
- Plater.cpp: took upstream's guarded add_model(...) early-returns and
  the VFA vfa_layer_height plumbing; kept the belt temp-tower path,
  _calib_apply_belt_mode and belt_calib_flip_ringing_tower. Dropped the
  VFA "cut upper" block, superseded upstream by model scaling.
- Brim.cpp: upstream's ObjectInstanceID-keyed brimAreaMap, keeping the
  belt early-return.
- 3DScene.cpp: kept both the belt build-plate tilt up_direction and
  upstream's per-extruder printable-height shading.
- GCodeViewer.cpp: kept upstream's dim-previous-layers setup and belt's
  exemption from the same-result early return.
- TreeSupport.cpp: upstream's >= 0 roof-layer fix inside belt's
  belt-floor branch.
- calib.cpp / GCode.hpp / GCodeWriter.{cpp,hpp} / Print.hpp: upstream's
  additions adapted to belt's pointer-held writer and helpers.
- Custom.json: kept profile version 02.04.00.03 (belt) over upstream's
  02.04.00.01; both bumped from 02.04.00.00.

Building this tree needs the wxInspector dependency, which upstream
added in the interim (python3 and wxWidgets 3.3.2 were already present
in the shared deps prefix).
2026-08-02 16:09:27 -05:00
Joseph Robertson 5428a0715d update belt-printer (#14446)
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-06-26 23:02:49 -05:00
Joseph Robertson 75770321dd Update Belt-Printer (#14425) 2026-06-25 22:40:26 -05:00
Joseph Robertson c950c3fb6b Add BabyBelt Pro Profile, Courtesy of Rexit (#14424) 2026-06-25 22:39:12 -05:00
Joseph Robertson 2ca843a38e Belt Printing: Bugfix: Solid Organic Tree Base, Slim Tree Skirt, Renderer (#14395)
* fix tree support brim
* treesupport3d part 1: more diagnostic logging.  (todo once things are fixed: remove this / gate it properly)
* make area under Z=0 in rotated slice pipeline not solid
* fix solid Z=0 layer for belt printers
* fix renderer
* clean up logging
* final review pass
2026-06-24 22:01:29 -05:00
Joseph Robertson 0ef7c6d581 Belt Printing: Update (#14393)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-06-24 21:34:53 -05:00
Joseph Robertson 34b0d36cda Belt Printer Initial Push (#14385)
# Description

Initial push - documentation available at #12998 

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-06-24 09:42:40 -05:00
Joseph Robertson d619c7e19c Merge branch 'belt-printer' into belt/baseChanges 2026-06-24 09:42:25 -05:00
Joseph Robertson 31b44cb731 Merge pull request #66 from HarrierPigeon/belt/tommyb-rendererChanges
Clean up and implement @tommasobbianchi's belt renderer changes
2026-06-23 00:27:39 -05:00
harrierpigeon ddbee84e68 render the G-code preview upright (designed view) + toggle UI 2026-06-23 00:14:17 -05:00
Joseph Robertson bf6cce1f40 Merge pull request #45 from tommasobbianchi/feat/belt-gcode-cartesian-preview
belt: render the G-code preview upright (model/Cartesian space)
2026-06-22 19:59:27 -05:00
Joseph Robertson 8bdf0df00a Merge branch 'main' into belt/baseChanges 2026-06-22 19:36:17 -05:00
Joseph Robertson d6c9187c71 Merge branch 'main' into belt/baseChanges 2026-06-22 19:36:17 -05:00
Ian Bassi 0cdfb88357 Lang: Gettext update (#14361) 2026-06-22 20:16:55 -03:00
foXaCe 14cec7239b i18n(fr): translate strings added after the post-refactor sync (#14304) 2026-06-22 20:13:19 -03:00
Heiko LiebscherandClaude Opus 4.8 86c6a1a66f Improve German (de) translation (#14352)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:40:14 -03:00
SoftFever 07f08dfe40 bump version to 2.5.0-dev 2026-06-22 00:50:51 +08:00
Noisyfox a4fb5af9e1 Don't allow adding more colors for non-semm printers on obj import color remapping dialog (#14275) 2026-06-21 18:20:58 +08:00
Tommaso Bianchi 8593d66a39 belt: correct the designed-view preview's belt-Z origin and reject mis-mapped outliers
The Cartesian designed-view preview over-extended the toolpaths past the model
shell by a height-proportional amount (up to ~20mm tall parts), most visibly on
long multi-part prints; compact parts like a calibration cube looked fine.

Two coupled causes:
- Belt start G-code that primes with a Z advance and a 'G92 Z0' reset leaves a
  constant machine-Z origin in the GCodeProcessor, so move positions are stored as
  gcode_Z + origin. The linear back-transform mixes that constant with the
  gantry-Y term, leaving a per-move designed-Y error that min-corner anchoring
  cannot cancel when an elevated move (e.g. a bridge) happens to cancel it at the
  bbox minimum. Expose GCodeProcessorResult::belt_z_origin (the m_origin[Z] left by
  the start G-code) and subtract it before the back-transform.
- Elevated features (bridges/overhangs) are mis-mapped by the linear inverse to
  outside the model body; build the anchor bbox only from moves within model_bb +/-
  10mm, with a fallback to the full bbox when the clip would drop the bulk (object
  placed away from the belt entry) so the gross-offset case still anchors.

Preview-only; G-code output is unchanged.
2026-06-21 06:48:44 +02:00
Tommaso Bianchi 3fc3b8a8ae belt: anchor the designed-view G-code preview onto the model bounding box
The belt designed (upright) preview back-transforms the machine-frame G-code
into model space with the linear belt inverse. That inverse recovers the
print's shape and orientation, but not the per-object placement/lift
translation: the object's position on the belt, the BeltSliceStrategy min-Z
lift, and the centering pre-translate are applied OUTSIDE
build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse
cannot undo them. The result was a constant offset (~20 mm on the belt-advance
axis) of the toolpaths from the model shell, on every model.

Recover the missing translation generally — independent of the offset's exact
source or the axis remap — by anchoring the back-transformed object body
(extrusions on layer_id >= 1, i.e. excluding the layer-0 prime/skirt) onto the
upright model bounding box, the same space the shells render in, and folding
that translation into the belt inverse before converting to libvgcode.

Replaces the previous Y=0 anchoring in LibVGCodeWrapper, which pinned the
toolpaths to the belt entry rather than to the model and so left the offset in
place for any object not sitting at the origin.
2026-06-21 06:48:44 +02:00
Tommaso Bianchi 695a1f897a belt: render the G-code preview in model (Cartesian) space
On a belt printer the emitted G-code is in the machine frame (45-deg sheared,
axis-remapped, scaled), so the toolpath preview shows the print as a sheared
slab floating off the bed. Map each toolpath vertex back to model/Cartesian
space for the "designed" view.

The back-transform is the inverse of the full G-code forward pipeline
(BeltGCodeWriter::to_machine_coords):
  model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1
built from config, so it handles any rotation / shear / scale / axis-remap
combination, not just plain 45-deg belt slicing. Computed in load_as_gcode()
from print.config() and applied per-vertex inside libvgcode::convert (display
position only; layer_id, times and the volumetric/flow math keep the raw
machine values, so the layer slider and stats are unaffected).

- Toggle with the existing "Show designed view" checkbox / hotkey B; off shows
  the raw machine-frame G-code (useful for debugging the transform itself).
  Defaults to on.
- Belt printers skip the same-result-id load cache so the upright view applies
  and the toggle takes effect even when the G-code is unchanged.
- The object extrusions (layer_id >= 1) are anchored to the belt entry to drop
  the constant machine-origin offset (start-G-code belt advance) that the linear
  back-transform alone does not capture; start-G-code prime lines are excluded
  so they don't steal the anchor.
2026-06-21 06:48:44 +02:00
Tommaso Bianchi 2d69f6e17c belt: expose MachineFrameTransform's composed matrix
Add a const accessor for the shear*scale transform so the G-code viewer can
build the machine->model back-transform for the upright belt preview.
2026-06-21 06:48:44 +02:00
Joseph Robertson 340ce575e2 Merge branch 'main' into belt/baseChanges 2026-06-20 15:56:59 -05:00
Joseph Robertson d795900fcf Merge pull request #64 from tommasobbianchi/feat/esun-pla-maxvolspeed-tuning
IdeaFormer IR3 V2: tune eSUN PLA white speed from HW max-vol-speed calibration
2026-06-18 09:42:19 -05:00
Joseph Robertson 9b1fb2217a Merge branch 'main' into belt/baseChanges 2026-06-18 09:41:14 -05:00
Tommaso BianchiandClaude Opus 4.8 ef6f65eacc IdeaFormer IR3 V2: tune eSUN PLA white speed from HW max-vol-speed calibration
Physical max-volumetric-speed test (belt #62 v4 asset) on the IR3 V2 with eSUN
PLA white: the wall stayed clean up to ~100 mm/s = ~20 mm3/s before
under-extrusion. The shipped cap of 10 mm3/s was ~half the real ceiling and
was silently throttling infill.

- eSUN PLA @IdeaFormer IR3 V2: filament_max_volumetric_speed 10 -> 20
- 0.20mm Standard @IdeaFormer IR3 V2: sparse_infill_speed 200 (~18 mm3/s at the
  new cap, no longer throttled). Outer wall (45), PA (0.12), accel (1000)
  unchanged — accuracy preserved.
- IdeaFormer.json version bump for profile-cache refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 07:13:59 +02:00
Joseph Robertson 0add523e1b Merge branch 'main' into belt/baseChanges 2026-06-13 08:23:47 -05:00
Joseph Robertson 375036f330 Merge pull request #44 from tommasobbianchi/feat/belt-skip-height-check
belt: don't reject long objects (skip build-height check on belt printers)
2026-06-13 08:23:26 -05:00
Joseph Robertson fbbeb1fab0 Merge pull request #58 from HarrierPigeon/belt/tempTower-TommyB
Belt/temp tower tommy b
2026-06-12 05:53:32 -05:00
harrierpigeon 0bca3fd2e5 make belt printer specific temp tower only accessible to belt printers 2026-06-12 05:13:08 -05:00
Tommaso Bianchi 85fd613cf7 feat(belt/calib): add Overhang temperature-tower model (selectable) (#48)
Belt printers can't slice a tall vertical temperature tower. This adds a
belt-specific temperature-tower model — a row of discrete, individually
engraved provini laid along the belt, each printed at one temperature via
custom per-layer M104. Each provino is an inverted-L overhang that stresses
print quality, so the operator reads the best temperature off overhang
quality rather than a continuous ramp.

It is offered as a "Test model" choice in the temperature calibration dialog
(mirroring the Cornering test's selector), so users keep Joe's counter-rotated
sectioned tower as "Standard" and can pick this one as "Overhang":
- Calib_Params::test_model (existing field) carries the choice.
- Temp_Calibration_Dlg gets a Standard/Overhang radio.
- Plater::calib_temp belt branch: test_model 0 -> _calib_temp_belt_sectioned
  (unchanged Standard path), 1 -> the discrete-provini Overhang path.

Assets: belt_temp_provino_unit.stl + belt_temp_tower_<start>_<end>.stl (6
ranges) + gen_belt_temp_tower.py (manifold engraving). Based on
belt/generic-calibrations. The Overhang path is HW-validated on the IdeaFormer
IR3 V2 (discrete M104 + engraved numbers); not re-validated since the rebase.
2026-06-12 05:13:07 -05:00
Joseph Robertson 0da24cd38b Belt/Standard calibrations (#54)
Enables supported printing of standard Orcaslicer calibration profiles.

* Build 2 Checkpoint

* fix support generation wedge, ghost layers

* flip cornering tests 180 deg to waste less supports

* fix row spacing on the flow ratio calibrations

* more testing, this didn't fix anything

* switched rotation tools, same issue

* fixed Z-offset issues

* add rest of PA features, may look a bit weird on a belt

* make temp towers work

* re-enable spiral on calibrations that want it

* Final cleanup pre-PR and community testing
2026-06-12 03:14:12 -05:00
Rodrigo Faselli d7b75540d0 Merge branch 'main' into belt/baseChanges 2026-06-11 11:59:53 -03:00
Tommaso Bianchi b7bda9912b belt: fix IR3 V2 end G-code reversing the belt into the part (#56)
The IdeaFormer IR3 V2 End G-code ran `G28 ; home all`, which homes the
Z (belt) and Y (gantry) axes. On a belt printer Z is the conveyor, so
homing it runs the belt all the way back to origin, dragging the finished
part back under the gantry that G28 has just lowered — the head knocks the
print (reported by an IR3 V2 user; the `G1 Y50` lift came after the G28,
too late).

Replace the end sequence with a belt-safe one: switch to relative mode
(G91), lift the gantry for clearance, advance the belt forward one full
machine-depth (Z676, the 676 mm product depth) to eject the part and cycle
the belt surface clean, then home X only — never the Z/belt axis.
2026-06-11 09:30:35 -05:00
Tommaso Bianchi 4f3a608009 belt: don't flag the lead-in as an empty-layer error on belt printers (#47)
collect_layers_to_print() warns (CRITICAL) when an extrusion layer sits above
the previous one with an empty gap below — the fixed-bed assumption that
material with nothing under it is floating and unprintable. On a belt printer a
*leading* empty range (the gap starts at Z=0, no prior extrusion layer) is not
floating: it is the conveyor lead-in, and the part rests on the advancing belt
as the first material is laid down well above Z=0. A part not designed for a
belt (e.g. a flat test model tilted into the belt frame) then trips this as a
false "Object can't be printed for empty layer between 0 and N" error.

Suppress only the leading case (belt_printer && last_extrusion_layer == null);
genuine internal gaps are still flagged, since on a belt those can be an
over-angle overhang printing into air. Non-belt output is unchanged.
2026-06-10 23:54:02 -05:00
Tommaso BianchiandClaude Opus 4.8 f682ab5cd3 belt: replace height-check skip with a belt-correct vertical-clearance check
The original PR skipped the max-print-height check entirely on belt printers
because the sliced (virtual) Z is belt travel, not build height. As the reviewer
noted, that removed the only working height guard. Restore a correct guard:

- Print::validate: on belt printers, compare the upright object height
  (max over instances of the scene-space bbox) against printable_height directly.
  printable_height is the usable VERTICAL clearance above the belt: the gantry
  travels up the tilted plane (reach = height/cos(tilt)) and its axis range is
  sized for that (IR3 V2: ~354 mm gantry travel = 250 mm vertical at 45deg, and
  printable_height = 250). Hardware-confirmed 250 mm vertical clearance, so no
  cos(tilt) factor is applied.
- BuildVolume::set_belt_printer: drop the diagonal Z scaling; the build-volume Z
  already equals printable_height, keeping the live 'outside build volume'
  highlight in agreement with validate().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:41:18 +02:00
harrierpigeon 2bcb775b90 update IdeaFormer profiles to new generic belt printer config 2026-06-10 05:10:20 -05:00
Joseph Robertson e29a82c672 add attribution and design notes 2026-06-10 05:10:20 -05:00
Joseph Robertson 5b243eec92 Relocate Pre-Slice remap logic 2026-06-10 05:10:20 -05:00
Joseph Robertson fee6be98b2 unify frame tilt work 2026-06-10 05:10:20 -05:00
Joseph Robertson 9405ac5976 remove mesh origin snapping 2026-06-10 05:10:20 -05:00
Tommaso Bianchi 6ed2437848 Add IdeaFormer IR3 V2 belt printer profile - credit: tommasobbianchi (#43)
* Add IdeaFormer IR3 V2 belt printer profile

Self-contained vendor profile for the IdeaFormer IR3 V2 (45 deg belt printer):
machine (0.4 nozzle) + 0.20mm process + Generic PLA/PETG filaments, with the
belt machine-frame transforms set explicitly on the machine preset
(belt_printer, belt_slice_rotation x/45/global, build_plate_tilt_x=45,
gcode_remap_x/y/z, gcode_shear_z=pos_tan, gcode_scale_y=inv_cos).

The vendor bundles its own machine/process commons (fdm_belt_common,
fdm_klipper_common, fdm_machine_common, fdm_process_common) on purpose:
OrcaSlicer resolves system-preset inheritance per-vendor, so a profile that
inherits the Custom vendor's commons cross-vendor fails to resolve its parent
and the whole IdeaFormer vendor silently fails to load. Bundling the commons
(and listing them in IdeaFormer.json in dependency order) keeps the vendor
self-contained, matching how every other vendor folder is structured.

Machine limits, bed temperature (75 C for belt PLA) and start/end G-code are
taken from a working IdeaFormer IR3 V2.



* feat(belt/profile): eSUN PLA @IdeaFormer IR3 V2 — HW-calibrated belt filament

Add an eSUN PLA belt profile for the IR3 V2, inheriting Generic PLA @IdeaFormer
IR3 V2 (self-contained: parent is in the same IdeaFormer vendor, registered
after it in filament_list). HW-calibrated on the IR3 V2:
- nozzle_temperature 200/200 (temp-tower calibration)
- pressure_advance 0.12 (PA calibration)
- filament_max_volumetric_speed 10 mm³/s (max-vol-speed calibration: wall
  failed at 126 mm/s → 126 × 0.0798 mm³/mm ≈ 10 mm³/s)
2026-06-10 04:13:56 -05:00
Joseph Robertson da3fee2dfa Merge branch 'main' into belt/baseChanges 2026-06-05 11:55:44 -05:00
Joseph Robertson c0d6ae8540 Merge branch 'main' into belt/baseChanges 2026-06-05 03:12:27 -05:00
Joseph Robertson 573e1c6544 Belt/fix profiles and minor oopsies (#42)
* fix duplicate printer, bump version

* clean up extra tab in space

* fix generic defaults
2026-06-05 03:11:38 -05:00
Rodrigo Faselli 20be78a96e Merge branch 'main' into belt/baseChanges 2026-06-04 17:32:35 -03:00
Joseph Robertson 02d45c3258 Finish Fixes from Copilot Review (#39)
* fix: restore BuildVolume bounds when toggling belt mode

set_belt_printer() mutated m_bboxf when enabling but never restored
the original extents on disable or when switching infinite_y true->false,
leaving stale max.y/max.z values that broke collision and object_state
checks. Recompute m_bboxf from m_bed_shape + m_max_print_height at the
top of each call, then apply belt-specific adjustments on top.

Addresses Copilot review comment on PR #12998 (BuildVolume.cpp:196).

* chore: drop [BELT-DEBUG] to_machine_coords log to trace

Was emitting at warning level once per 0.2mm Z bucket during every belt
print export, polluting default user logs. Trace level matches the rest
of the belt diagnostics and is silent in production.

Addresses Copilot review comment on PR #12998 (BeltGCodeWriter.cpp:86).

* chore: drop [BELTRACE] make_perimeters/support logs to trace

Eight warning-level traces around make_perimeters and
generate_support_material were emitting on every call/exit during normal
slicing, cluttering default logs. They're concurrency-debug breadcrumbs
not user-facing diagnostics, so drop them to trace.

Addresses Copilot review comment on PR #12998 (PrintObject.cpp:438).

* perf: gate BeltSliceStrategy diagnostic bbox tracking behind compile flag

apply_to_trafo() walked every model vertex twice (once for min_z, once
for per-volume mesh/slicer bboxes) and emitted seven trace logs per
call. The bboxes and logs are diagnostic only; min_z is the load-bearing
output. Wrap the bbox accumulation, logging, and supporting headers in
SLIC3R_BELT_DIAGNOSTIC_LOG so production builds do the bare min_z scan.

Addresses Copilot review comment on PR #12998 (BeltSliceStrategy.cpp:95).

* fix: apply part_cooling_fan_min_pwm to first-layer plane fan crossings

apply_first_layer_plane_fan_eval emitted band-crossing M106 commands
through GCodeWriter::set_fan() without the per-printer PWM floor that
every other set_fan call in CoolingBuffer applies. On printers with a
non-zero part_cooling_fan_min_pwm, fans could fail to spin up at low
requested speeds near the belt surface.

Addresses Copilot review comment on PR #12998 (CoolingBuffer.cpp:1227).
2026-06-04 14:40:45 -05:00
harrierpigeon f9888c7d7a Merge remote-tracking branch 'upstream/main' into belt/baseChanges 2026-05-31 05:17:32 -05:00
Joseph Robertson 0bda684dd7 delete mesh transforms (#37)
* delete mesh shear, scale and refactor logger

* clean up config options

* reorder UI elements
2026-05-31 05:08:42 -05:00
Joseph Robertson 8a578cdf00 Merge branch 'main' into belt/baseChanges 2026-05-30 21:39:03 -05:00
Rodrigo Faselli 6b256db012 Merge branch 'main' into belt/baseChanges 2026-05-28 07:44:43 -03:00
Joseph Robertson 2dc4900292 Copilot review fixes & upstream code interaction fix (#34)
* first pass at review issue 8
* delete detritus
* fix build compile error due to upstream changes
2026-05-27 21:53:04 -05:00
Joseph RobertsonandCopilot Autofix powered by AI 0f75d6bc4e Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-27 19:25:02 -05:00
Joseph Robertson e913621369 Merge branch 'main' into belt/baseChanges 2026-05-27 11:50:16 -05:00
Joseph Robertson 48b6db93b8 Belt/slice rotate (#33)
* initial commit
* fix upper bounds for assemblies
* significantly less Z shift issues, still not quite tamped down yet though
* add instrumentation to logs
* finally found the issue
* update printer defaults
2026-05-27 11:45:38 -05:00
Joseph Robertson 72cafcbe06 Merge branch 'main' into belt/baseChanges 2026-05-22 15:23:07 -05:00
Joseph Robertson a9bae54f20 Rotate instead of shear for slicing stage (#30)
* initial commit

* fix upper bounds for assemblies

* significantly less Z shift issues, still not quite tamped down yet though

* add instrumentation to logs

* finally found the issue

* update printer defaults
2026-05-22 15:21:33 -05:00
Joseph Robertson 218881c6f6 fix assembly bounding box truncation problems noticed by hotcubcar (#28) 2026-05-20 02:46:41 -05:00
Joseph Robertson cd5fb68d38 Merge branch 'main' into belt/baseChanges 2026-05-19 23:00:14 -05:00
Joseph Robertson f87a46ec6e fix X mirroring (#26)
Thanks to @hotcubcar for catching this!
2026-05-19 22:54:50 -05:00
Rodrigo Faselli 8dc91d8b1d Merge branch 'main' into belt/baseChanges 2026-05-19 08:06:57 -03:00
Joseph Robertson da8b11b8ab HOTFIX: update generic belt printer profile (#23)
oops
2026-05-19 01:06:39 -05:00
Joseph Robertson c79970bedb Clean Up Settings Interface, Update Generic Profile (#22)
* clean up UI elements

* further cleaning

* final cleanup for first round of settings UI streamlining

* update generic belt printer settings

* fix generic again
2026-05-19 00:56:08 -05:00
harrierpigeonandClaude Opus 4.7 7252f6acb7 Merge upstream/main into belt/rebase/may-18
Reconciles the belt-printer branch with upstream PRs through #13723. Six
files had conflicts; three additional files needed manual follow-up fixes
where the auto-merge produced code that referenced upstream-renamed fields
or changed function signatures.

Notable reconciliations:
- TreeSupport.cpp: kept belt-floor early-exit branches around HEAD's
  drop-down logic, folded upstream's `(distance_to_top > 0 ? 1 : 0)`
  formula into the non-belt-floor path (upstream PR #11812). Dropped dead
  `roof_enabled`/`force_tip_to_roof` locals.
- TreeSupport3D.cpp: combined upstream's safety-offset + remove_small
  changes with HEAD's belt-floor clip in the per-slice trim loop. Dropped
  HEAD's `else` block (superseded by upstream's rewritten bottom-contact
  propagation) and re-added the belt-floor clip into the new propagation
  loop. Gated the propagation on belt printers to prevent OOM when
  belt-floor clipping produces empty initial slices.
- TriangleSelector.{cpp,hpp}: merged both new `select_patch` parameters
  (HEAD's `up_direction` and upstream's `select_partially`); body uses
  `dot(up_direction)` for the overhang angle check and forwards
  `select_partially` to `select_triangle`.
- SupportMaterial.cpp: `slicing_params.soluble_interface` →
  `zero_gap_interface_bottom` in HEAD's `detect_belt_floor_bottom_contacts`,
  matching upstream's same-purpose rename at line 2495.
- Custom.json, GCodeWriter.cpp: simple additive merges (kept entries /
  includes from both sides).

Verified by building OrcaSlicer (RelWithDebInfo) after a full deps
rebuild (Eigen v5.0.1, libigl v2.6.0 are now managed deps) and slicing
a scaled Benchy on the NORMALIZER belt-printer profile without OOM.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:53:24 -05:00
Joseph Robertson 6a2d690f45 Decouple Slicing From Machine Frame Logic (#21)
* minor logic swap

* first attempt, has a race condition

* fixed the offset issue

* found a solution, I think things work now (at least once I quash this race condition)

* still chasing down race conditions

* add manual shear / scale order strategy swap

* tweak manual shear, fix ui uninitialization crash

* fix z height / g-code desync issue

* fix shear then scale cutoff planes

* getting closer

* fix support termination planes

* fix incorrect offsets in shear-then-scale mode

* test - fix overextrusion due to model/layer scale
2026-05-18 19:01:43 -05:00
RF47 8fa6a4602b fix profile indentation 2026-05-09 19:51:18 -03:00
harrierpigeonandClaude Opus 4.7 0f29437135 Merge remote-tracking branch 'upstream/main' into belt/baseChanges
Conflicts resolved in src/libslic3r/GCode.cpp and src/slic3r/GUI/GUI_Factories.cpp.

GCode.cpp: combined upstream's air-filtration per-extruder gating
(activate_air_filtration_during_print / _on_completion), the new
extrusion-role-change gcode lambda, ZAA's path.z_contoured arc-fit
disable, raft-aware slow_down_layers branch, and Vec3d/Line3 ZAA
plumbing with the local belt-printer changes (path_on_first_layer,
effective_layer_index_for_point, should_disable_arc_fitting). All
auto-merged m_writer.X() calls converted to m_writer->X() to match
the local unique_ptr<GCodeWriter> refactor.

GUI_Factories.cpp: inserted brim_flow_ratio in the Support category
list and renumbered around the local build_plate_tilt_x/y entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:23:41 -05:00
SoftFever 75cc0de071 Merge branch 'main' into belt/baseChanges 2026-04-17 15:44:03 +08:00
Joseph Robertson bc6d0ef0fb Add first layer detection and fan control - prototype 2026-04-13 22:29:23 -05:00
Joseph Robertson c17ae25bbc Merge branch 'main' into belt/baseChanges 2026-04-13 21:34:34 -05:00
harrierpigeon e981a517cd Merge branch 'belt/global-mesh-transform' into temp-pr19-merge 2026-04-10 11:49:37 -05:00
harrierpigeon 0703728e56 add global mesh transform option 2026-04-10 11:39:08 -05:00
SoftFever 1e9ee0c120 add a generic belt printer 2026-04-09 23:07:08 -05:00
harrierpigeon e9a579b604 switch default shear axis, swap to tan(a) instead of cot(a) 2026-04-09 23:07:07 -05:00
harrierpigeon 783acd932a revert CLAUDE.md 2026-04-09 23:07:07 -05:00
harrierpigeon c8a1bf3a99 Part 3.2: decouple axis remapping, enable viewing settings in Developer mode or when Belt mode is active 2026-04-09 23:07:07 -05:00
harrierpigeon 2facaac9e8 Part 3.1: refactor BeltTransform pipeline
add BeltGCodeWriter

add BeltGCode

consolidate changes into shared classes for BeltGcode
2026-04-09 23:07:07 -05:00
harrierpigeon 9bbac19de4 Part 2.7: Add G-code back-transform and tree support belt floor clipping
- Add BeltBackTransform class that inverts the shear/scale matrix and
  applies it in GCodeWriter::to_machine_coords() so G-code outputs in
  the machine's physical coordinate space, gated by new
  belt_gcode_back_transform config option
- Extend belt floor clipping to all three tree support pipelines
  (Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon
  clipping, anti-overhang integration, and belt raft extension layers
- Fix tree drop_nodes() belt termination, organic support global Z
  offset, collision calculation index bug, and first-layer brim/empty
  layer checks for belt printers

two-shot - first build built but didn't plumb to UI.  Woah.

add pre-slice axis remap, because Y needs to be Z

going to change tactic and move based on bbox min

switch to per axis snapping

per axis swap snap now per object

build plate tilt wasn't invalidating slicer settings

support upper bound now correct, need to get lower bound corrected

axis swapped support termination corrected

Z Shear works with and without pre-slice remap now
2026-04-09 23:07:07 -05:00
harrierpigeon ea5c6776b3 Part 2.6: Add belt floor support clipping for all support types
- Fix support clipping z-shift calculation by removing coordinate-space
  mismatch and sync belt_floor_z_shift with global_z_offset; fix
  invalidation so posSupportMaterial no longer resets slicing params
- Add belt floor polygon clipping to non-organic tree support
  (slim/strong/hybrid) with collision surface integration in
  TreeSupportData, belt extension layers, and first-layer brim
  suppression
- Add belt floor clipping to organic tree support pipeline with virtual
  belt raft layers, per-layer polygons in TreeModelVolumes, and
  post-generation layer trimming; fix pre-existing processing_last_mesh
  bug in calculateCollision()

Fix belt floor support clipping: z-shift, invalidation, and global offset

- Fix support clipping z-shift calculation by removing coordinate-space
  mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and
  sync belt_floor_z_shift with global_z_offset in global shear mode
- Fix invalidation so posSupportMaterial no longer resets slicing params,
  preventing the exact posSlice z-shift from being overwritten by the
  bounding-box approximation on support-only setting changes
- Remove double-counting of global z_offset on support layers — support
  already inherits the offset from object layers during generation

This Work Was Co-Authored-By Claude Opus 4.6 (1M context) <noreply@anthropic.com>

UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced

Fix mesh clipping through build plate after belt shear/scale transform

Generalize G-code viewer designed-view toggle for full belt transform

Clip support layers to transformed belt floor plane

Supports below the tilted build plate (Z = shear_factor * from_axis - min_z)
are now clipped via half-plane intersection after generation. Belt floor
parameters stored in SlicingParameters and populated in both update_slicing_parameters()
and the static slicing_parameters() overload.

Make belt G-code viewer toggle more prominent, add B keyboard shortcut

- Add separator + teal "Belt Printer" header in legend panel
- Append [B] hint to checkbox label
- Add B key shortcut in GLCanvas3D to toggle designed/machine view
- Read belt_printer_angle from loaded G-code headers to enable belt view

Add per-axis global transform option for belt printer shear

New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates
instance shift so objects at different bed positions get position-aware
transform (Z += factor * instance_shift_on_from_axis).

Fix global shear: use layer Z offset instead of mesh transform, add config invalidation

- Global shear offset applied as post-slicing layer print_z adjustment
  instead of mesh transform (which was absorbed by min_z normalization
  or shifted mesh out of slice range)
- Register all belt transform options in Print::invalidate_state_by_config_options
  to trigger posSlice re-slicing (the fallback only invalidated Print steps,
  not PrintObject steps — belt changes had no effect without manual re-slice)
- Belt gcode remap options added to steps_gcode (gcode-export only)
- Skip empty-first-layer check for belt objects with global Z offset

WIP: split instances for global shear, relative Z offsets, debug logging

- PrintApply: when belt global mode active, prevent instance grouping by
  adding unique Z perturbation to trafo — each copy becomes its own
  PrintObject with independent layers
- PrintObjectSlice: compute global Z offset relative to minimum Y shift
  across all PrintObjects (lowest-Y object stays at Z=0)
- Debug logging (warning level) for belt global shift values and offsets

Known issues:
- Cached posSlice results cause stale offsets when mixing copies with
  individually-added objects — need to compute min baseline outside slice()
- Supports still generate to Z=0 instead of object's global Z offset

Fix global shear for copied objects: disable shared-object layer optimization

When belt global Z shear is active, each object needs unique layer Z
values based on its bed position. The shared-object optimization was
causing copies to reuse the source object's layers (and its Z offset)
instead of computing their own position-based offset.

started work on getting supports to work properly

one step forward, one step back

this version didn't quite work.  Getting somewhere though

about to add UI controllable tests

added configuration options for supports

tweak CLAUDE.md to be more aggressive for my machine.  This commit should probably be pulled out before contributing upstream

still chasing down some bugs

moving objects between slices no longer results in improper Z-height because of caching

added more data to the debug logs

Z offset is getting more global again

still not quite there, I think there's a fundamental logic flaw?

hunting for bugs

finally have a functional fix

Add belt floor clipping to tree supports (organic and non-organic)

- Add belt floor polygon clipping to non-organic tree support
  (slim/strong/hybrid) in draw_circles() and terminate nodes at the
  belt surface instead of the horizontal build plate
- Add belt floor clipping to organic tree support pipeline with virtual
  belt raft layers for sub-floor branch generation, per-layer belt
  floor polygons in TreeModelVolumes, and post-generation layer trimming
- Fix pre-existing processing_last_mesh bug in TreeModelVolumes that
  prevented m_anti_overhang (support blockers) from ever being applied;
  skip empty first layer check for belt printers

Commits:

current approach: make a face surface to build supports to

closer!

supports now terminate on shear plane, now need to get shear plane to correct Z height

nearly there

chasing down logic issues still

committing for checkpoint, this still does not work

still got logic problems...

cull support clipping

stashing changes for now.  Going to focus on getting the global shear OFF support generation dialed first.

beginning per object shear calcs

Local shear transform is on correct Z offset now

local shear finally works now and needs more testing

global shear works now, needs thorough testing

debugging non-45 degree angles

debugging part 2

supports at all angles work now

remove debug logging

Add belt floor collision to non-organic tree support pipeline

- Integrate belt floor as a collision surface in TreeSupportData so
  branches route around the belt naturally, replacing the explicit
  termination checks in drop_nodes()
- Add belt extension layers below the object after draw_circles() to
  allow support geometry to extend to the diagonal belt surface instead
  of terminating at a horizontal first layer
- Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds
  int32), skip first-layer brim expansion for belt printers, and
  extend empty first layer check bypass to all belt modes

add debug logging, Z translate for tree supports

still not seeing any cutoff surface yet

adding debug options

attempt #2 at trees

if hit Z buildplate stop but don't set to_buildplate true

getting closer

tree support almost there, just need to get rid of the circles at the beginning

getting closer

belt / shear plane clip works, need to figure out the buidlplate plane issues

more logic, added debugging logs

supports now extend somewhat below Z=0 in global shear mode

fix bad alloc, add 10mm below build plate

fully works now

shear transform + prusa tree support generation works now.

pull out debug logging
2026-04-09 23:07:07 -05:00
harrierpigeon 98f4d34dcb Part 2.5: Add global shear transform, support clipping, and belt UI improvements
- Implement per-object global shear transform in PrintObject with
  layer Z-offset calculation, config invalidation, and fix for
  shared-object layer optimization breaking copied objects
- Clip support layers to the transformed belt floor plane and begin
  work on tree support adaptation for sheared coordinate space
- Improve belt UI: gray out inactive sub-options, add B keyboard
  shortcut for G-code viewer design-view toggle, fix mesh clipping
  through build plate after shear/scale transform

y' = y + z·cot(α),
  while x' = x and z' = z

getting closer to customizable variant

getting closer

X/Y/Z shear initial

clean up UI

add 1/sin(a) transform, idea taken from blackbelt cura plugin

Things work now (turns out I've been using the wrong set of  transforms)
2026-04-09 23:07:07 -05:00
harrierpigeon 501aff7e53 Part 2: Replace belt rotation w/ per-axis shear transforms and G-code axis remap
- Replace monolithic belt rotation transform with independent per-axis
    shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis
    remapping, giving full flexibility to match any belt printer's
    coordinate system
  - Remove all rotation mode logic and intermediate type+axes dropdowns,
    simplifying the pipeline to pure shear matrices while preserving the
    default behavior (Y += Z*cot(45deg) with identity remap)
  - Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new
    shear-only model; expose 12 new settings in printer UI via
    Tab.cpp/Preset.cpp

Implement belt printer tilted slicing

Implement the core belt slicing pipeline that makes the slicer
tilt-aware:

Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation
  from slicing frame to machine frame
Step 2: PrintObject - belt-rotated object height calculation
  (y*sin(a) + z*cos(a)) for correct layer count
Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so
  horizontal slice planes correspond to belt-parallel planes,
  with Z-shift computed from model volumes
Step 4: GCodeProcessor - machine-frame preview (no transform needed)
Step 5: 3DBed - rotate bed visualization about X by belt angle

Fix: belt surface IS the build plate, no mesh rotation

Currently still slicing perpendicular to the belt normal.  Need to figure out why.

Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt

The previous R(+alpha, X) transform produced negative Z values
(-y*sin(a) term dominated). Changed to R(-alpha, X) which gives
machine_z = y*sin(a) + z*cos(a), always positive for points
above the belt surface. Z increases with each layer as expected.

reverting and changing slice methodology

Add pink slicing direction arrow from origin

Shows the effective slicing direction (gantry normal) as a pink
arrow from the origin. Shorter and wider than the gravity arrow.
Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is
the layer stacking direction in the original mesh frame.

Fix slicing arrow visibility and add raw G-code toggle

- Disable depth test for pink slicing arrow so it renders on top of
  the tilted bed geometry (was being occluded)
- Remove unnecessary 5mm Z-offset from arrow position
- Add m_belt_show_raw toggle to GCodeViewer
- Add "Show raw G-code (slicing frame)" checkbox in legend when
  belt mode is active

Implement to_machine_coords inverse rotation for belt printer G-code

The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to
start at 0. The G-code output now undoes this transform via
to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the
original machine-frame coordinates where Y is horizontal and Z is
vertical.

Changes:
- GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift
- GCodeWriter: add belt_z_shift member and setter/getter
- GCode.cpp: compute Z-shift from print objects (same logic as
  PrintObjectSlice) and pass to writer; write z_shift to G-code header
- GCodeProcessor: parse belt_z_shift from G-code header
- GCodeViewer: store belt_z_shift from processor result

Wire raw G-code toggle to apply slicing-frame view transform

When "Show raw G-code (slicing frame)" is checked in the preview
legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift)
to the toolpath rendering. This shows the G-code as it was during
slicing: rotated part with horizontal layers.

Default (unchecked): machine-frame view — upright part with tilted layers.

Remove belt printer placeholder comment from GCodeProcessor

The preview now correctly displays machine-frame G-code with the
optional raw view toggle. No transform is needed in the processor.
2026-04-09 23:07:06 -05:00
harrierpigeon c808653565 Add belt printer transform pipeline: slicing rotation, G-code coords, preview
- Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count
Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode,
GCodeProcessor, and GCodeViewer
Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views

This is a combination of 6 commits.

checkpoint 1: initial MVP.  Slicing functions, but rotates instead of skews are happening and a lot of other stuff too

getting somewhere, getting to the point where I need to figure out how to verify this stuff

this appears to be a dead end.

getting somewhere I think maybe

I'm pretty sure we've completely lost the plot at this point and need to restart this process...

remove slice logic in preparation for new, more invasive plan
2026-04-09 23:07:06 -05:00
harrierpigeon a7441c7f48 stage in changes from off-plate-gravity and remove stuff I didn't need 2026-04-09 23:07:06 -05:00
SoftFever 3bc13e5cfd add a generic belt printer 2026-04-07 10:37:34 +08:00
SoftFever 141749a6f2 Merge branch 'main' into belt/baseChanges 2026-04-06 22:52:31 +08:00
harrierpigeon 4634a5dfd7 switch default shear axis, swap to tan(a) instead of cot(a) 2026-03-30 13:25:40 -05:00
harrierpigeon 372139c770 revert CLAUDE.md 2026-03-30 13:25:40 -05:00
harrierpigeon 44eebdb8ad Part 3.2: decouple axis remapping, enable viewing settings in Developer mode or when Belt mode is active 2026-03-30 13:25:40 -05:00
harrierpigeon c7aa4ca3ef Part 3.1: refactor BeltTransform pipeline
add BeltGCodeWriter

add BeltGCode

consolidate changes into shared classes for BeltGcode
2026-03-30 13:25:40 -05:00
harrierpigeon b297f68921 Part 2.7: Add G-code back-transform and tree support belt floor clipping
- Add BeltBackTransform class that inverts the shear/scale matrix and
  applies it in GCodeWriter::to_machine_coords() so G-code outputs in
  the machine's physical coordinate space, gated by new
  belt_gcode_back_transform config option
- Extend belt floor clipping to all three tree support pipelines
  (Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon
  clipping, anti-overhang integration, and belt raft extension layers
- Fix tree drop_nodes() belt termination, organic support global Z
  offset, collision calculation index bug, and first-layer brim/empty
  layer checks for belt printers

two-shot - first build built but didn't plumb to UI.  Woah.

add pre-slice axis remap, because Y needs to be Z

going to change tactic and move based on bbox min

switch to per axis snapping

per axis swap snap now per object

build plate tilt wasn't invalidating slicer settings

support upper bound now correct, need to get lower bound corrected

axis swapped support termination corrected

Z Shear works with and without pre-slice remap now
2026-03-30 13:25:40 -05:00
harrierpigeon 7ff6bc42b1 Part 2.6: Add belt floor support clipping for all support types
- Fix support clipping z-shift calculation by removing coordinate-space
  mismatch and sync belt_floor_z_shift with global_z_offset; fix
  invalidation so posSupportMaterial no longer resets slicing params
- Add belt floor polygon clipping to non-organic tree support
  (slim/strong/hybrid) with collision surface integration in
  TreeSupportData, belt extension layers, and first-layer brim
  suppression
- Add belt floor clipping to organic tree support pipeline with virtual
  belt raft layers, per-layer polygons in TreeModelVolumes, and
  post-generation layer trimming; fix pre-existing processing_last_mesh
  bug in calculateCollision()

Fix belt floor support clipping: z-shift, invalidation, and global offset

- Fix support clipping z-shift calculation by removing coordinate-space
  mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and
  sync belt_floor_z_shift with global_z_offset in global shear mode
- Fix invalidation so posSupportMaterial no longer resets slicing params,
  preventing the exact posSlice z-shift from being overwritten by the
  bounding-box approximation on support-only setting changes
- Remove double-counting of global z_offset on support layers — support
  already inherits the offset from object layers during generation

This Work Was Co-Authored-By Claude Opus 4.6 (1M context) <noreply@anthropic.com>

UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced

Fix mesh clipping through build plate after belt shear/scale transform

Generalize G-code viewer designed-view toggle for full belt transform

Clip support layers to transformed belt floor plane

Supports below the tilted build plate (Z = shear_factor * from_axis - min_z)
are now clipped via half-plane intersection after generation. Belt floor
parameters stored in SlicingParameters and populated in both update_slicing_parameters()
and the static slicing_parameters() overload.

Make belt G-code viewer toggle more prominent, add B keyboard shortcut

- Add separator + teal "Belt Printer" header in legend panel
- Append [B] hint to checkbox label
- Add B key shortcut in GLCanvas3D to toggle designed/machine view
- Read belt_printer_angle from loaded G-code headers to enable belt view

Add per-axis global transform option for belt printer shear

New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates
instance shift so objects at different bed positions get position-aware
transform (Z += factor * instance_shift_on_from_axis).

Fix global shear: use layer Z offset instead of mesh transform, add config invalidation

- Global shear offset applied as post-slicing layer print_z adjustment
  instead of mesh transform (which was absorbed by min_z normalization
  or shifted mesh out of slice range)
- Register all belt transform options in Print::invalidate_state_by_config_options
  to trigger posSlice re-slicing (the fallback only invalidated Print steps,
  not PrintObject steps — belt changes had no effect without manual re-slice)
- Belt gcode remap options added to steps_gcode (gcode-export only)
- Skip empty-first-layer check for belt objects with global Z offset

WIP: split instances for global shear, relative Z offsets, debug logging

- PrintApply: when belt global mode active, prevent instance grouping by
  adding unique Z perturbation to trafo — each copy becomes its own
  PrintObject with independent layers
- PrintObjectSlice: compute global Z offset relative to minimum Y shift
  across all PrintObjects (lowest-Y object stays at Z=0)
- Debug logging (warning level) for belt global shift values and offsets

Known issues:
- Cached posSlice results cause stale offsets when mixing copies with
  individually-added objects — need to compute min baseline outside slice()
- Supports still generate to Z=0 instead of object's global Z offset

Fix global shear for copied objects: disable shared-object layer optimization

When belt global Z shear is active, each object needs unique layer Z
values based on its bed position. The shared-object optimization was
causing copies to reuse the source object's layers (and its Z offset)
instead of computing their own position-based offset.

started work on getting supports to work properly

one step forward, one step back

this version didn't quite work.  Getting somewhere though

about to add UI controllable tests

added configuration options for supports

tweak CLAUDE.md to be more aggressive for my machine.  This commit should probably be pulled out before contributing upstream

still chasing down some bugs

moving objects between slices no longer results in improper Z-height because of caching

added more data to the debug logs

Z offset is getting more global again

still not quite there, I think there's a fundamental logic flaw?

hunting for bugs

finally have a functional fix

Add belt floor clipping to tree supports (organic and non-organic)

- Add belt floor polygon clipping to non-organic tree support
  (slim/strong/hybrid) in draw_circles() and terminate nodes at the
  belt surface instead of the horizontal build plate
- Add belt floor clipping to organic tree support pipeline with virtual
  belt raft layers for sub-floor branch generation, per-layer belt
  floor polygons in TreeModelVolumes, and post-generation layer trimming
- Fix pre-existing processing_last_mesh bug in TreeModelVolumes that
  prevented m_anti_overhang (support blockers) from ever being applied;
  skip empty first layer check for belt printers

Commits:

current approach: make a face surface to build supports to

closer!

supports now terminate on shear plane, now need to get shear plane to correct Z height

nearly there

chasing down logic issues still

committing for checkpoint, this still does not work

still got logic problems...

cull support clipping

stashing changes for now.  Going to focus on getting the global shear OFF support generation dialed first.

beginning per object shear calcs

Local shear transform is on correct Z offset now

local shear finally works now and needs more testing

global shear works now, needs thorough testing

debugging non-45 degree angles

debugging part 2

supports at all angles work now

remove debug logging

Add belt floor collision to non-organic tree support pipeline

- Integrate belt floor as a collision surface in TreeSupportData so
  branches route around the belt naturally, replacing the explicit
  termination checks in drop_nodes()
- Add belt extension layers below the object after draw_circles() to
  allow support geometry to extend to the diagonal belt surface instead
  of terminating at a horizontal first layer
- Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds
  int32), skip first-layer brim expansion for belt printers, and
  extend empty first layer check bypass to all belt modes

add debug logging, Z translate for tree supports

still not seeing any cutoff surface yet

adding debug options

attempt #2 at trees

if hit Z buildplate stop but don't set to_buildplate true

getting closer

tree support almost there, just need to get rid of the circles at the beginning

getting closer

belt / shear plane clip works, need to figure out the buidlplate plane issues

more logic, added debugging logs

supports now extend somewhat below Z=0 in global shear mode

fix bad alloc, add 10mm below build plate

fully works now

shear transform + prusa tree support generation works now.

pull out debug logging
2026-03-30 13:25:40 -05:00
harrierpigeon 719af2d81d Part 2.5: Add global shear transform, support clipping, and belt UI improvements
- Implement per-object global shear transform in PrintObject with
  layer Z-offset calculation, config invalidation, and fix for
  shared-object layer optimization breaking copied objects
- Clip support layers to the transformed belt floor plane and begin
  work on tree support adaptation for sheared coordinate space
- Improve belt UI: gray out inactive sub-options, add B keyboard
  shortcut for G-code viewer design-view toggle, fix mesh clipping
  through build plate after shear/scale transform

y' = y + z·cot(α),
  while x' = x and z' = z

getting closer to customizable variant

getting closer

X/Y/Z shear initial

clean up UI

add 1/sin(a) transform, idea taken from blackbelt cura plugin

Things work now (turns out I've been using the wrong set of  transforms)
2026-03-30 13:25:40 -05:00
harrierpigeon cb13a22e57 Part 2: Replace belt rotation w/ per-axis shear transforms and G-code axis remap
- Replace monolithic belt rotation transform with independent per-axis
    shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis
    remapping, giving full flexibility to match any belt printer's
    coordinate system
  - Remove all rotation mode logic and intermediate type+axes dropdowns,
    simplifying the pipeline to pure shear matrices while preserving the
    default behavior (Y += Z*cot(45deg) with identity remap)
  - Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new
    shear-only model; expose 12 new settings in printer UI via
    Tab.cpp/Preset.cpp

Implement belt printer tilted slicing

Implement the core belt slicing pipeline that makes the slicer
tilt-aware:

Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation
  from slicing frame to machine frame
Step 2: PrintObject - belt-rotated object height calculation
  (y*sin(a) + z*cos(a)) for correct layer count
Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so
  horizontal slice planes correspond to belt-parallel planes,
  with Z-shift computed from model volumes
Step 4: GCodeProcessor - machine-frame preview (no transform needed)
Step 5: 3DBed - rotate bed visualization about X by belt angle

Fix: belt surface IS the build plate, no mesh rotation

Currently still slicing perpendicular to the belt normal.  Need to figure out why.

Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt

The previous R(+alpha, X) transform produced negative Z values
(-y*sin(a) term dominated). Changed to R(-alpha, X) which gives
machine_z = y*sin(a) + z*cos(a), always positive for points
above the belt surface. Z increases with each layer as expected.

reverting and changing slice methodology

Add pink slicing direction arrow from origin

Shows the effective slicing direction (gantry normal) as a pink
arrow from the origin. Shorter and wider than the gravity arrow.
Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is
the layer stacking direction in the original mesh frame.

Fix slicing arrow visibility and add raw G-code toggle

- Disable depth test for pink slicing arrow so it renders on top of
  the tilted bed geometry (was being occluded)
- Remove unnecessary 5mm Z-offset from arrow position
- Add m_belt_show_raw toggle to GCodeViewer
- Add "Show raw G-code (slicing frame)" checkbox in legend when
  belt mode is active

Implement to_machine_coords inverse rotation for belt printer G-code

The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to
start at 0. The G-code output now undoes this transform via
to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the
original machine-frame coordinates where Y is horizontal and Z is
vertical.

Changes:
- GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift
- GCodeWriter: add belt_z_shift member and setter/getter
- GCode.cpp: compute Z-shift from print objects (same logic as
  PrintObjectSlice) and pass to writer; write z_shift to G-code header
- GCodeProcessor: parse belt_z_shift from G-code header
- GCodeViewer: store belt_z_shift from processor result

Wire raw G-code toggle to apply slicing-frame view transform

When "Show raw G-code (slicing frame)" is checked in the preview
legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift)
to the toolpath rendering. This shows the G-code as it was during
slicing: rotated part with horizontal layers.

Default (unchecked): machine-frame view — upright part with tilted layers.

Remove belt printer placeholder comment from GCodeProcessor

The preview now correctly displays machine-frame G-code with the
optional raw view toggle. No transform is needed in the processor.
2026-03-30 13:25:40 -05:00
harrierpigeon ed6ea086a2 Add belt printer transform pipeline: slicing rotation, G-code coords, preview
- Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count
Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode,
GCodeProcessor, and GCodeViewer
Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views

This is a combination of 6 commits.

checkpoint 1: initial MVP.  Slicing functions, but rotates instead of skews are happening and a lot of other stuff too

getting somewhere, getting to the point where I need to figure out how to verify this stuff

this appears to be a dead end.

getting somewhere I think maybe

I'm pretty sure we've completely lost the plot at this point and need to restart this process...

remove slice logic in preparation for new, more invasive plan
2026-03-30 13:25:40 -05:00
harrierpigeon 08aa277974 stage in changes from off-plate-gravity and remove stuff I didn't need 2026-03-30 13:25:40 -05:00
9965 changed files with 422584 additions and 636844 deletions
-1
View File
@@ -1 +0,0 @@
.claude
@@ -1,86 +0,0 @@
# DELEGATION SPECIFICATION: HARNESS-DRIVEN VALIDATION LOOP
slug: sketch-focus-arbiter · repo: /home/tommaso/projects/apps/orca_cad · branch: cad-mainline
## 1. TARGET GOAL
**Functional Objective.** Keyboard input in the Design tab is routed by WHAT THE KEY IS, not by
which widget the window manager decided to focus. Adopted from FreeCAD's
`DrawSketchKeyboardManager::detectKeyboardEventHandlingMode`
(src/Mod/Sketcher/Gui/DrawSketchKeyboardManager.cpp), which never queries focus at all:
- digit, `-`, `.`, `,` -> the open value field
- Backspace / Delete -> the open value field (when one is open)
- Enter / Return / Tab -> commit the field, control returns to the view
- a letter -> the sketch-tool shortcut map, as today
- Esc -> the existing CadLevel LIFO (DesignInteraction.hpp), unchanged
- anything else -> sticky: whoever had it keeps it
Observable postcondition: for EVERY sketch tool that opens a value field, a value typed
immediately after the field appears — with NO click into the field — is the value committed.
Today the prefill is committed instead whenever the WM withholds focus.
**Target Files / Scope (writable).**
src/slic3r/GUI/CAD/DesignPanel.cpp (the arbiter lives in the existing wxEVT_CHAR_HOOK)
src/slic3r/GUI/CAD/DesignCanvas.cpp/.hpp (forwarding entry points only)
src/slic3r/GUI/CAD/SketchInlineEditor.cpp/.hpp (accept a programmatically delivered character)
scripts/CAD/check-gui-click-edit.py (F2P oracle — authoring exception, see §4)
Everything else read-only. No dependency additions, no reformatting.
**Open Bindings.**
- The in-canvas ImGui field on wip/in-canvas-value-field is NOT in scope. Default: the arbiter
is implemented against the CURRENT wxFrame field on cad-mainline, because content-based
routing makes the window's focus irrelevant either way. If it later moves in-canvas the
arbiter is unchanged.
- Tools whose field is opened by a toolbar button rather than a gesture (Constrain path) are
covered by the same arbiter but are not in the F2P tool list. Default: assert them in P2P only.
## 2. HARNESS ENVIRONMENT & GROUND TRUTH
The rig container `orcacad-gui` on nativedev IS the harness. Xvfb `:11` + openbox, the app under
test, `xdotool` for synthetic input, and an MCP socket at `/tmp/mcp.sock` that reports sketch
state as JSON. It is a closed loop: drive input, read geometry back, assert. No window manager
politics, no human.
Harness interface (ordered; each slot one invocation, one exit code):
S1 sync docker cp <file> orcacad-gui:/OrcaSlicer/<path>
S2 build docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer
S3 restart docker exec orcacad-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh
S4 F2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach
S5 P2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py
**F2P.** `scripts/CAD/check-gui-click-edit.py`. For each of Line, Rectangle, Circle, Slot,
Polygon, Ellipse and Rounded rectangle: arm the tool, draw it, and type a value that differs
from the prefill WITHOUT clicking the field. Assert the committed value equals the typed value.
The ladder must FAIL against unmodified cad-mainline — that is what proves it asserts something.
**P2P.** `scripts/CAD/check-gui-sketching.py`, the existing gesture ladder, minus anything red at
baseline. NOTE: it calls `focus_field()` — one click into the field before typing — which is the
workaround this whole task removes. It stays green as a regression guard; it is NOT evidence.
**Test Integrity Constraint.** `focus_field()` in check-gui-sketching.py must NOT be deleted to
make things pass, and check-gui-click-edit.py must NOT be weakened. Either invalidates the run.
## 3. VERIFICATION COMMANDS
1. Static: `docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer` (warnings delta only;
this repo configures no linter — the compiler is the static gate. Absolute-zero is NOT the gate.)
2. Harness: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach`
3. Regression: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py`
## 4. CONVERGENCE LOOP — ceiling 8 iterations
EDIT (scoped) -> EXECUTE S1..S5 -> PARSE the ladder's per-tool assertions and the [UX]/[KEYTRACE]
lines -> PATCH from the parsed cause. On ceiling without convergence: stop, report the last diff
and the unresolved failure set. Do not report success.
F2P authoring exception: check-gui-click-edit.py is writable, and must be shown RED against
unmodified source before any source edit counts.
## 5. TERMINATION CRITERIA
- [ ] S2 exits 0, and introduces no compiler warning absent from the baseline.
- [ ] S4 ALL_PASSED — every tool commits the typed value, no click into the field.
- [ ] S5 shows zero regressions against its recorded baseline pass count.
- [ ] F2P proven red without the fix (source stashed, ladder re-run, must FAIL).
## 6. GUARDRAILS
Zero-assumption: no completion claim without captured stdout and exit codes. Oracle supremacy:
the ladder's verdict overrides my judgement. Blast radius: §1 files only. Baseline obligation:
run §3 once before the first edit and record it.
-188
View File
@@ -1,188 +0,0 @@
---
name: orca-profiles
description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id, and moving settings that sibling presets repeat onto shared bases after fix-variant or while drafting. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job.
---
# OrcaSlicer system profiles
This skill describes how OrcaSlicer system profiles are drafted and shaped: the rules, equations and
patterns a profile follows. Use it to draft new profiles, modify existing ones, fix profile issues and
review profile changes.
A bundle is the index `resources/profiles/<Vendor>.json` plus the folder `<Vendor>/`. The vendor id is
the filename stem (`BBL`), not the index's display `name` (`Bambulab`). The index is the loader's only
entry point: an unindexed preset never loads. `OrcaFilamentLibrary` is the shared filament bundle,
loaded first; `blacklist.json` is data, not a bundle.
## References
Read the reference for the task before editing; load others only when the task crosses into them.
Paths below are relative to this skill. Commands run from the repository root; on Windows use `py -3`
for `python3`.
| Task | Read |
| --- | --- |
| Add or tune a filament, brand or material; fix compatibility, alias shadowing or overlapping coverage | [filament-profiles.md](references/filament-profiles.md) |
| Add a printer or nozzle; change models, variants, assets or per-extruder vectors | [machine-profiles.md](references/machine-profiles.md) |
| Add or tune extruder variants (`extruder_type` Direct Drive / Bowden × nozzle volume type Standard / High Flow / TPU High Flow / E3D High Flow / Extra High Flow variants) on a printer, process or filament | [extruder-variants.md](references/extruder-variants.md) |
| Add a quality tier or tune a process | [process-profiles.md](references/process-profiles.md) |
| Draft several presets, or clean up after `fix-variant`: which base each shared value belongs on, when a new base pays off, proving nothing loads differently | [shared-bases.md](references/shared-bases.md) |
| Name a preset; check what a name must equal; base names, uniqueness, filenames | [naming.md](references/naming.md) |
| Create a vendor bundle; index, `version`, `inherits`, `include`; migrate preset names; diagnose why a bundle fails to load | [vendor-bundle.md](references/vendor-bundle.md) |
| Change ids; diagnose AMS identity | [ids.md](references/ids.md), then `docs/HLSD/filament_id.md` for identity changes |
| Run checks, interpret failures, test another tree or verify in the app | [validation.md](references/validation.md) |
| Review a profile diff | [review-checklist.md](references/review-checklist.md) |
## Rules
1. **Bump the `version` of every bundle you change**, `OrcaFilamentLibrary.json` included when affected.
Increment the last component and carry `.99` into the third (`02.04.00.99` → `02.04.01.00`). The
updater installs only a strictly newer version, and CI does not check the bump.
2. **Register every preset, bases included, parents before children.** `update-index` writes the four
`*_list` arrays from the files on disk; `check` fails unless the index equals its output. Each index
entry's `name` must equal the file's `name`.
3. **Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New presets
normally omit them until `generate-id`; bases carry no `setting_id`. BBL's own `setting_id`s and a wrongly
inherited `filament_id` need the explicit handling in [ids.md](references/ids.md).
4. **A name is an identity; preserve shipped selectable names.** Every reference (`inherits`,
`compatible_printers`, `default_*`, `printer_model`) is the exact, case-sensitive `name`. Renaming or
deleting a shipped selectable preset, or flipping its `instantiation` from `"true"` to `"false"`,
needs `renamed_from` (a `;`-separated string) on a selectable successor
([migration rules](references/vendor-bundle.md#renamed_from)); update in-tree references too.
5. **Values are strings or arrays of strings.** `"instantiation": "false"`, never `false`. A
`machine_model`'s `nozzle_diameter` is a `;`-separated string; a `machine`'s is an array. Custom
G-code is one string. Wrong types can abort loading of the bundle or of every vendor
([failure scopes](references/vendor-bundle.md#failure-scopes)).
6. **Unknown keys are dropped silently.** Confirm every new key exists in
`src/libslic3r/PrintConfig.cpp`; a key a neighbouring file writes is no evidence it exists. `check` rejects,
and `normalize` removes, known obsolete keys, but neither detects an arbitrary misspelling. A key
missing from the definitions may be a legacy name the loader still renames
(`tool_change_gcode` → `change_filament_gcode`) or whose value it rewrites (`DirectDrive` →
`Direct Drive`); check `PrintConfigDef::handle_legacy` before removing one, and write the current name
in new edits.
7. **Write overrides only.** Inherit the bundle's bases and restate just what differs; follow the
bundle's existing layering and style, except that a new filament prefers the library's bases. A
value that every preset of a group shares goes on the group's base
([shared bases](references/shared-bases.md)).
8. **One load error can discard a whole vendor bundle**: an unresolved `inherits`, a missing indexed
file, two selectable presets with one name, an unknown `printer_model` or `printer_variant`, a
filament with no resolvable `filament_id`, `nil` in a non-nullable key. `inherits` and `include`
resolve only inside the bundle, except that filaments may inherit from OrcaFilamentLibrary.
9. **Filament compatibility names exact printer variants.** Every instantiated filament outside the
library writes a non-empty `compatible_printers` in its own file. Library fallbacks may omit it;
library printer-specific tunes use a non-empty list. One variant may be claimed by only one preset
per filament product (`filament_id`); an overlap is resolved by moving the variant to the most
specific preset, which is preferred over deleting a preset
([one variant, one profile](references/filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)).
10. **One all-printer preset per product; colour is a runtime property, never a preset.** Never ship
presets that differ only by colour. CI accepts them, so this is a review call
([colour](references/filament-profiles.md#colour-is-a-runtime-property)).
11. **Write a variant key at full width or not at all.** A key in the four variant sets holds exactly
`N` values in the selectable preset that writes it, `N = S × k` in the
[sizing equation](references/extruder-variants.md#sizing-equation): one per variant, a (normal,
silent) pair per variant for the `machine_max_*` limits; one value is not "the same for every
variant". Profiles must be correct as written: `check` judges each selectable preset by the
equation, never by what the loader pads or cuts, and `check --strict` also holds what a preset
inherits to its own width, as BBL writes it. A base is never judged on its own: its array widths
count, under `--strict`, where they reach a preset, while the id and layout rules judge the
composed preset, inherited values included, without it. Declare the variant layout on a
multi-extruder printer whose extruders need different values
([widths](references/extruder-variants.md#widths)).
12. **Run the full checks before reporting completion.** A `--vendor` run is only a development loop.
Review also covers version bumps, assets, non-default processes and hardware tuning, which CI cannot
establish.
## Names
| Type | Shape | What the loader uses |
| --- | --- | --- |
| `machine_model` | `<Model>` (`Bambu Lab X1 Carbon`) | the exact string, named by each variant's `printer_model`; also the `<Model>_cover.png` stem |
| `machine` | `<Model> <nozzle> nozzle` | the exact string, named by `compatible_printers`; `printer_variant` holds the nozzle token (`0.4`, [rules](references/machine-profiles.md#printer_model-and-printer_variant)) |
| `process` | `<lh>mm <Quality> @<target>` | the exact string when referenced or selected; `@<target>` is a label, compatibility comes from the preset's list or condition |
| `filament` | `<Product> @<target>` | text before the first `@` is the alias (shadowing, `filament_id`); the rest is a label, with reserved targets `@base` and `@System` |
The shapes are convention; `check` enforces only uniqueness. Per-type conventions, base names and
filename rules are in [naming.md](references/naming.md).
## Creating or modifying a profile
1. **Inspect the diff and the neighbouring presets.** Read their `name`, parent chain and children:
edits to a base, or to a leaf that others inherit, propagate. Match the bundle's structure and write
only overrides. New files use tab indentation, LF and a trailing newline; preserve unrelated
formatting in existing files. Match filename case exactly and use
[cross-platform names](references/naming.md#filenames-and-paths).
2. **Author explicit metadata.** Set `type` yourself (`machine` vs `machine_model` especially), and use
`"from": "system"` and a string `instantiation` on config presets. Omit ids on new presets unless
[ids.md](references/ids.md) requires special handling; retain them on existing ones. Complete
compatibility, defaults, assets and any rename migration using the task reference.
3. **Put shared values on shared bases** when drafting several presets, and after `fix-variant`, which
widens an array in every preset that writes it and moves nothing. Each value goes on the base of the
level that determines it, a new base only where it pays for itself, and a restructure must leave
every selectable preset loading what it loaded: `snapshot` before the edit, `compare` after it
([shared-bases.md](references/shared-bases.md)).
4. **Bump the version**, then run the authoring commands in order for each affected bundle, reading
every diff and resolving every error before moving on:
```bash
python3 scripts/orca_profile_tool.py normalize --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py update-index --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py generate-id --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py check
```
Writing commands accept `--dry-run`. `normalize` changes content and can reformat entire files;
rerun `update-index` after any change to `inherits` or `include`, since it orders by them.
**Do not use `trim` in this workflow:** it deletes unindexed files, including one you just added. Do
not use `normalize --force` for routine edits. An error in a bundle you did not touch predates your
change: confirm it on a clean checkout and report it rather than fixing it in the same change.
5. **Validate:**
```bash
./scripts/check_profile.sh --vendor "<Vendor>" # development loop
./scripts/check_profile.sh # full tree before the PR
```
On Windows use `scripts\check_profile.bat -Vendor "<Vendor>"` / `scripts\check_profile.bat`. Logs
land in a per-user cache dir ([validation.md](references/validation.md)). Id checks stay tree-wide
under `--vendor`, and filament-only bundles skip the default slice check. Under `--vendor` read only
`profile_tool` and `validate_slice`: the other three checks fail on library presets that name other
vendors' printers ([why](references/validation.md#the-five-checks)).
6. **Verify the changed behaviour.** Slice newly added non-default processes and filaments
[explicitly](references/validation.md#checking-a-copy-of-the-tree), and
[test in the app](references/validation.md#testing-in-the-app) for selection or UI behaviour. Report
the checks actually run, their failures and skips, and any hardware tuning still unverified.
## Symptom → first look
| Symptom | Start here |
| --- | --- |
| A vendor disappears | the app's log or the `validate_system` log; [failure scopes](references/vendor-bundle.md#failure-scopes) |
| A setting has no effect | key spelling or a legacy name (`PrintConfigDef::handle_legacy`), value type, or a config key placed on a `machine_model` |
| A preset exists but is not selectable | index registration, `instantiation`, whether it is installed (chosen in the setup wizard, or listed in the model's `default_materials`), compatibility |
| A filament is missing, duplicated, or matches the wrong spool | [compatibility and alias shadowing](references/filament-profiles.md#compatible_printers), [ids](references/ids.md) |
| Presets differ only by colour, or an all-printer library preset lacks `@System` | [colour is a runtime property](references/filament-profiles.md#colour-is-a-runtime-property) |
| High Flow (or a second extruder) slices with Standard (or extruder 1) values; a variant switch is missing; a variant's tuned values never arrive | [extruder variants](references/extruder-variants.md#variant-strings), [widths](references/extruder-variants.md#widths), [variant names](references/validation.md#variant-names) |
| Values land on the wrong extruder or mode after a variant was added | [inserting a variant](references/extruder-variants.md#adding-a-variant-inserts-its-values-at-its-variant-index), [padding and composition](references/extruder-variants.md#padding-truncation-and-composition) |
| A resolved value matches neither the file nor its `inherits` parent; an array is not the width the file wrote, or a child got only a base's first value | [composition order and `include`](references/vendor-bundle.md#inherits-and-include) |
| A bed temperature is ignored | [the twelve plate keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) |
| A change is absent from the running app | version bump and [installed profile location](references/validation.md#testing-in-the-app) |
| A check fails | [error → remedy](references/validation.md#error--remedy) |
## Source of truth
When this skill and the checkout disagree, the checkout wins: `scripts/orca_profile_tool.py` for the
tool and its flags; `src/libslic3r/Preset*.cpp` for loading and compatibility;
`src/libslic3r/PrintConfig.cpp` for keys, types, nullable options, the variant key sets and legacy
handling; `src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml`
for validation coverage; `docs/HLSD/filament_id.md` for filament identity. The wiki's
[profile guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/how_to_create_profiles.md)
is a tutorial; confirm loader and CLI details against these sources.
## Editing this skill
The skill describes what a profile must be, not what the shipped tree currently is. State rules,
equations, patterns and profile shapes; never inventories of shipped defects, counts, dated
measurements or lists of which vendors do what. Test each sentence: if editing the profiles alone,
with the engine and tool unchanged, could make it false, state the rule behind it or give a generic
example instead. Example files named as models to copy, and commit hashes cited as the reason for a
rule, are fine.
@@ -1,547 +0,0 @@
# Extruder variants
Use this when a printer's hotend or extruder can be in more than one hardware configuration that
needs different settings: a Standard and a High Flow nozzle, a TPU High Flow, E3D High Flow or Extra High Flow nozzle, or
a Direct Drive and a Bowden extruder on one machine. Variants let one printer, process and filament preset carry a
separate value per configuration; the user picks the configuration in the sidebar, and slicing uses
the matching values. They are not for nozzle **diameter**: that stays one `machine` preset per
`printer_variant`.
## Variant strings
- A **variant string** is `"<extruder_type> <nozzle volume type>"`: the extruder's `extruder_type`
value, a space, and its nozzle volume type (the project's `nozzle_volume_type`, seeded by the
printer preset's `default_nozzle_volume_type`), e.g. `"Direct Drive High Flow"`.
- A **variant** is one entry of a preset's variant list, named by its variant string (plus an extruder
id for printer and process lists). A variant-aware key holds **one value per variant** (a
(normal, silent) pair for the `machine_max_*` limits), and each preset declares its variants in
that list. Slicing picks, for each extruder, the variant whose
variant string equals the current `extruder_type` + nozzle volume type.
- Matching is an **exact string compare** of the whole string (plus the extruder id for printer and
process lists) against the preset's own resolved list, in any order. With no match the **first
variant** is used, silently: variant index 0 for printer and process keys (extruder 1's first
variant, whichever extruder asks), the filament's own first variant for filament keys. Printer and
process keys are matched only on a printer with several extruders or whose
`extruder_variant_list` offers several variant strings (`support_different_extruders`); on any
other printer their variant index 0 is read whatever it names. Nothing rejects a profile for a
variant mismatch; mistakes surface only as wrong values in the G-code.
- Variant index and array length follow [Widths](#widths).
The complete enum is `s_keys_map_ExtruderType` and `s_keys_map_NozzleVolumeType` in
`src/libslic3r/PrintConfig.cpp` (`grep -A5 s_keys_map_NozzleVolumeType` there to confirm):
| Part | Values |
| --- | --- |
| Extruder type | `Direct Drive`, `Bowden` |
| Nozzle volume type | `Standard`, `High Flow`, `TPU High Flow`, `E3D High Flow`, `Extra High Flow` (`Hybrid` exists but is runtime-only) |
So the ten legal variant strings are the two extruder types × the five writable nozzle volume types,
and this table is the whole test of legality: a string's presence in a shipped profile is no evidence
for it. `Hybrid` (an extruder with several sub-nozzles) is never a variant: a filament on it reads the
variant of its own nozzle volume type from the project's `filament_volume_map`, printer and process
keys get one variant per nozzle volume type the extruder holds (`extruder_nozzle_stats`), and any
other lookup reads `Standard`. Never write it in a variant string. The same per-filament and
per-type reading applies on any extruder once `extruder_nozzle_stats` lists more nozzle volume
types than there are extruders.
Every other string (a nozzle volume type name from another slicer, a typo, a variant copied from a
shipped profile) is a **dead variant**: nothing selects it, and since lookup is by string it does not
shift the variants beside it; it still counts toward the variant length when arrays are sized.
`orca_profile_tool.py check` reports it as an error in every bundle, BBL included
([variant names](validation.md#variant-names)). Legacy names are errors too: in the four variant
lists, `default_nozzle_volume_type` and `nozzle_volume_type`, the loader still rewrites `Normal` →
`Standard` and `Big Traffic` → `High Flow`, so a ported `Direct Drive Normal` variant would load, but
`check` rejects the spelling and names the enum name to write. `DirectDrive` is rewritten only in
`extruder_type`, so `DirectDrive Standard` in a variant list is dead. Write the enum names;
`normalize` does not convert them.
### A nozzle the enum does not name
The nozzle volume types are fixed by the engine, and a profile cannot add one. A nozzle the table does
not name needs the type added in code first, which is outside profile work; until then any string
for it is a dead variant. Once the engine has the type, its variant string is the enum name after
the extruder type, e.g. `"Direct Drive <name>"`, and `check` accepts it with no tool change, since it
reads the enum from `PrintConfig.cpp`. Existing arrays are unaffected
([slice time](#slice-time-and-existing-users)).
## When to use variants
| Hardware | Do |
| --- | --- |
| One extruder, one nozzle type | Nothing. Without a variant list the printer has one variant ([variant length](#widths)) and no printer-key lookup takes place; a filament with several variants still gets the one for the printer's variant string. |
| Bowden-only printer | Nothing either. A list-less printer matches no string, so every printer-key lookup reads variant index 0 whether that variant is called `"Bowden Standard"` or `"Direct Drive Standard"`; naming it is cosmetic while it is the printer's only variant. A multi-extruder Bowden printer that declares the layout writes `"Bowden Standard"` variants: `"Direct Drive Standard"` entries match none of its extruders, so every extruder reads variant index 0 (and `check` reports them under [Printer rule 3](#printer-machine)). A filament with a `"Bowden Standard"` variant does get that variant there. |
| Nozzle types the user swaps (Standard / High Flow / TPU High Flow / E3D High Flow / Extra High Flow) | The printer lists them as variants; tune the keys that really differ per nozzle volume type. |
| Extruders of different types on one machine | One `extruder_type` per extruder, each extruder listing its own variants. |
| Several extruders that need different values in a variant key (retraction, z-hop, `nozzle_volume`, the `machine_max_*` limits) | Declare the layout even with a single nozzle volume type: `extruder_variant_list` with one `"<type> Standard"` per extruder, and the flattened pair. Without it the arrays still hold one value per extruder ([variant length](#widths)), but the loader keeps only extruder 1's value of a list-less printer's arrays, so every extruder prints with it. |
A preset without variant keys keeps working on a variant printer: its single variant is applied to
every extruder. So adding variants to a printer does not break existing processes or library
filaments; it only makes per-variant tuning possible.
## Printer (`machine`)
```json
"extruder_type": ["Direct Drive", "Direct Drive"],
"extruder_variant_list": ["Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow,Direct Drive TPU High Flow"],
"printer_extruder_id": ["1", "1", "2", "2", "2"],
"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive TPU High Flow"],
"default_nozzle_volume_type": ["Standard", "Standard"]
```
Rules:
1. `extruder_variant_list` has **one entry per extruder**; each entry is the `,`-joined variants that
extruder supports. It is the per-extruder menu the sidebar offers. It is in no
[variant set](#the-four-key-sets), so the variant-length resize leaves it alone. Extruder 1's
first variant, variant index 0 of the flattened pair, is the fallback of every extruder whose
variant is missing when the arrays are collapsed for slicing
([slice time](#slice-time-and-existing-users)); it is not necessarily the configuration the user
sees, which is `default_nozzle_volume_type` (rule 4).
2. `printer_extruder_variant` is that list **flattened** extruder-major, one entry per variant, and
`printer_extruder_id` gives each entry its 1-based extruder. These two size and address every
variant. Write all three keys and keep them in agreement. At load with
`single_extruder_multi_material` off, and in the app when the printer tab loads a printer with a
different number of extruders, the pair is rebuilt from `extruder_variant_list` (one
`Direct Drive Standard` per extruder when the list is absent) and the variant arrays are resized to
the rebuilt pair, padded with their first value or cut. The resize skips the `machine_max_*` limits
and `hotend_heating_rate` / `hotend_cooling_rate`: they keep their width, and an extruder beyond it
reads their first value, so extruder 2 and up of a list-less printer take extruder 1's normal limit
as their silent one too. With the three in agreement that changes nothing; a pair written without
the list is replaced. A listed variant the pair lacks is a menu choice that reads variant index 0.
- The pair without `extruder_variant_list` slices, but the sidebar offers no variant switch and
the app cannot add variants to a list-less process: nothing is lost while every extruder
has exactly one variant, and every further variant is unreachable.
- A missing or one-value `printer_extruder_id` is extruder 1 at every index
([the id trap](#padding-truncation-and-composition)) unless the load-time rebuild above
replaces the pair (`single_extruder_multi_material` off).
- `check` reports against this rule ([variant arrays](validation.md#variant-arrays)). Errors: a
pair that is not the flattening, an id array that does not give each entry its extruder, a list
of more than one variant without the pair, and a pair without the list that puts several variants
on one extruder. A pair without the list and one variant per extruder is a warning where the
load-time rebuild would replace it (`single_extruder_multi_material` off, and a pair other than
one `Direct Drive Standard` per extruder), and passes otherwise.
3. Every variant string in an entry must start with that extruder's `extruder_type`.
4. `default_nozzle_volume_type` has one value per extruder and must name a nozzle volume type that
extruder's variants list; it seeds the sidebar. The live choice is `nozzle_volume_type` in the
project config, never a preset key.
5. Size every array by the set its key belongs to (the three sets are listed in full in
[The four key sets](#the-four-key-sets); the lengths are worked through in the
[sizing equation](#sizing-equation)): exactly the length below in each selectable preset that
writes it, or leave the key out and the preset takes what reaches it, the default or its base's
array. One value is no
shorthand for "the same for every variant"; write the value for every variant:
| Set | Length | Keys |
| --- | --- | --- |
| `printer_extruder_options` | extruders (`E`) | the 8 per-extruder keys outside the variant scheme (`extruder_type`, `nozzle_diameter`, `default_nozzle_volume_type`, …) |
| `printer_options_with_variant_1` | variant length (`S`) | the full list below; not guessable from names |
| `printer_options_with_variant_2` | 2 × variant length | the 16 `machine_max_*` limits, at [stride 2](#widths): a (normal, silent) pair per variant |
Put the variant layout on the shared base of all printers that share the hardware, and let the
nozzle-diameter siblings inherit it, restating only the variant arrays whose values change. `check`
does not judge a base on its own; its arrays count where they reach a selectable preset, under
`check --strict`. The loader stores a base at its **own** `printer_extruder_variant`, one variant
when it writes none whatever its extruder count, and cuts a wider array to its first values before
any child inherits it; so a multi-extruder base whose extruders need different values declares the
layout itself ([composition](#padding-truncation-and-composition)). When a variant array can move from
the presets to a base is in [shared-bases.md](shared-bases.md#variant-arrays-on-a-base).
## Process
1. `print_extruder_variant` + `print_extruder_id` list the (extruder id, variant string) pairs of the
process's variants. A variant is found by that pair, never by position: what is **required** is
that every pair a compatible printer (by list or condition) can select is present, in any order. A
missing pair reads the process's variant index 0 for that extruder, an extra pair is a variant
nothing selects, and neither is reported. The exception is a single-extruder printer whose
`extruder_variant_list` offers one variant string: no pair is matched there and variant index 0
is read, so a process shared with such a printer lists that printer's pair first. **Mirroring** the printer's `printer_extruder_variant` +
`printer_extruder_id` entry for entry is the convention; follow it, so the arrays compare by eye,
but a different order with every pair present is a nit, not a defect. A process shared by printers
whose pairs differ falls back to variant index 0 on the pairs it lacks; give each layout its own base.
2. Every key in `print_options_with_variant` ([the full list](#the-four-key-sets), which is not
"every speed") has exactly one value per variant, or is left out. `print_extruder_id` needs its
value per variant too: one value pads to extruder 1 everywhere. `check` holds a
`print_extruder_id` that reaches the preset, written or inherited, to one entry per variant on any
printer, without `--strict`, and warns when it is absent and a variant repeats.
3. Only add process variants if speeds or accelerations really differ per nozzle volume type or
extruder. Otherwise omit the variant keys: a one-variant process needs no list, because match and
no-match both read variant index 0, and that variant is copied to every extruder at slice time.
4. Put the lists on the process base for that printer layout, so leaves stay small.
## Filament
```json
"filament_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow"],
"filament_max_volumetric_speed": ["21", "29"],
"filament_flow_ratio": ["0.98", "0.98"],
"filament_retraction_length": ["nil", "0.4"]
```
1. `filament_extruder_variant` lists variants **without extruder ids**: a filament's High Flow variant
is used on whichever extruder is in High Flow. Its entries must be distinct, since the lookup
returns the first equal string and a repeated entry is a variant nothing selects. There is no
filament id key: `filament_extruder_id` exists only as a G-code placeholder, not as a filament
option (its option and its set entry are commented out in `PrintConfig.cpp`), so a filament file that writes it loses the key as unknown.
`filament_extruder_compatibility` is unrelated to variants (it says which extruders the filament
may be loaded into).
2. Every key in `filament_options_with_variant` ([the full list](#the-four-key-sets)) that reaches the
preset, whether written, included or inherited, is resized to its variant count: write it at exactly
that width, or leave it out. Keys outside the set
(`filament_type`, plate temperatures, `fan_max_speed`, `slow_down_min_speed`, …) are never addressed by variant index; the preset contributes their first value however wide a
file writes them.
3. Cover every variant the material is meant to print on across its `compatible_printers`. Leave out
a variant deliberately when the material should not be tuned for it (e.g. a TPU High Flow variant
only on TPU filaments); an extruder reporting that variant string then reads the first variant.
4. Order the variants like the printer's, Standard first, so the first-variant fallback is the
conservative one.
5. Tune what really differs: `filament_max_volumetric_speed` is the usual difference between nozzle
volume types, then flow ratio, temperature and retraction. Use measured values; never copy the
Standard value into the High Flow variant and call it tuned.
6. `nil` is legal per variant in the nullable override keys, occupies one entry like any value, and
keeps the printer's value for that variant only.
7. Declare a multi-variant list with the arrays it sizes: on the filament, in its own file or in a
template it pulls in with [`include`](vendor-bundle.md#inherits-and-include). The filament may be
compatible with printers that list fewer variants, or none: each printer takes the filament's variant
for its own variant string, else the filament's first variant. A one-variant list on a shared base
is harmless, because one variant is the width a list-less preset has anyway; and a list-less base is
stored at that width, so a wider array on it reaches its children as its first value only.
A key such a base writes reaches a multi-variant child as one value, which the loader spreads over
the child's variants; `check --strict` reports it at the child, which restates it at its own width.
## Widths
Every variant key is a flat array addressed by **variant index**: index `n` belongs to entry `n` of
the preset's own variant list (`printer_extruder_variant`, `print_extruder_variant` or
`filament_extruder_variant`). The index is found by exact compare of the string
`"<extruder_type> <nozzle volume type>"` (plus the 1-based extruder id for printer and process lists),
and the value is read at `index × stride`:
| Stride | Keys | Layout |
| --- | --- | --- |
| 1 | `printer_options_with_variant_1`, `print_options_with_variant`, `filament_options_with_variant` | `[variant 0, variant 1, …]` |
| 2 | `printer_options_with_variant_2` (the `machine_max_*` limits) | `[variant 0 normal, variant 0 silent, variant 1 normal, variant 1 silent, …]` |
So a variant array is `variant length × stride` long. The **variant length** is the number of
entries in the preset's variant list, counted on the preset's config **after** `include` and
`inherits` are applied, dead variants included. An inherited array arrives already resized to the
base's own variant length, an included one at the width its file wrote
([composition](#padding-truncation-and-composition)):
| Preset | Variant length |
| --- | --- |
| `machine` | `len(printer_extruder_variant)`; without it, the variants `extruder_variant_list` offers; without both, **one per extruder** (`len(nozzle_diameter)`), since the list's default is one `Direct Drive Standard` per extruder. `extruders_count` is a printer-tab field, not a preset key. The loader honours that default only halfway for a system preset: it sizes the composed preset by the one-entry default `printer_extruder_variant`, cutting every variant array to its first value, and only then (with `single_extruder_multi_material` off) rebuilds the pair to one variant per extruder ([Printer rule 2](#printer-machine)) and pads the arrays with that value. The rule's width is still one per extruder; to give extruders different values, declare the layout. |
| `process` | `len(print_extruder_variant)`; without one, 1 |
| `filament` | `len(filament_extruder_variant)`; without one, 1 |
The per-extruder keys in `printer_extruder_options` ([listed with the sets](#the-four-key-sets)) are
outside this scheme and stay one value per extruder. Keys outside [the four sets](#the-four-key-sets)
are never variant-resized or addressed by variant index, however wide a shipped file writes them; a
per-extruder vector holds `len(nozzle_diameter)` values, and a shorter one acts as padded with its
first value ([machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers)).
### Sizing equation
For a key in one of the four variant sets:
```
E = extruders = len(nozzle_diameter) = len(extruder_type)
= len(default_nozzle_volume_type) = len(extruder_variant_list)
V_i = variants listed for extruder i = ","-separated entries of extruder_variant_list[i],
each "<extruder_type[i]> <nozzle volume type>"
S = variant length = V_1 + V_2 + … + V_E
= len(printer_extruder_variant) = len(printer_extruder_id)
k = stride = 2 for printer_options_with_variant_2, else 1
N = values the key holds, one per variant:
machine (printer_options_with_variant_1, _2) = S × k
process (print_options_with_variant) = len(print_extruder_variant) = len(print_extruder_id)
filament (filament_options_with_variant) = len(filament_extruder_variant)
values[s × k + m] = variant index s, mode m (m = 0 normal, m = 1 silent; only m = 0 at stride 1)
variant index s = (printer_extruder_id[s], printer_extruder_variant[s])
```
`printer_extruder_variant` is not sized by the equation; it defines `S`: it is `extruder_variant_list`
flattened extruder by extruder, and `printer_extruder_id[s]` is the extruder that index `s` came from.
A process that mirrors the printer's pairs, as [Process rule 1](#process) asks, has `N = S`; a
filament lists each variant string it is tuned for once, with no extruder id, so its `N` is its own
and independent of any one printer: it serves every printer in its `compatible_printers`, and a
variant string no extruder of a printer reports is simply never read there (a two-extruder printer
with `S = 7` serves a filament whose `N` is 3, and the filament keeps its 3 on a printer that offers
two variant strings). The
pair is what the loader reads, so the equality with the sum holds when the three keys agree, as
[Printer rule 2](#printer-machine) requires.
A selectable preset that writes a variant key writes it at its own `N`; any other width, one value
included, is an error. A preset that leaves a key out takes what reaches it, the default or an array
it inherits or includes, which the loader resizes to the preset's `N`; `check --strict` holds that
array to the preset's `N` too. A base is not judged on its own: its arrays count only where they reach
a preset that does not override them. BBL is the model for strict: every printer-specific machine,
process and filament declares its layout and restates every variant key at its own `N`, even where all
the values are the same, keys Orca added to the sets included.
Without the lists, `extruder_variant_list` defaults to one
`Direct Drive Standard` per extruder, so a machine has `V_i = 1` and `S = E`, and a process or filament
has `N = 1`; the loader cuts a list-less machine to its first variant and, with
`single_extruder_multi_material` off, widens it again with that value ([variant length](#widths)). The per-extruder keys outside the sets
(`printer_extruder_options` plus `extruder_offset` and `extruder_colour`) and `extruder_variant_list`
itself hold `E` values.
### Sizing examples
**One extruder, two nozzle volume types**: `E = 1`, `V_1 = 2`, so `S = 2`; stride-1 keys hold 2
values, `machine_max_*` hold 4:
```json
"nozzle_diameter": ["0.4"],
"extruder_type": ["Direct Drive"],
"extruder_variant_list": ["Direct Drive Standard,Direct Drive High Flow"],
"printer_extruder_id": ["1", "1"],
"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow"],
"default_nozzle_volume_type": ["Standard"],
"retraction_length": ["0.8", "1.0"],
"machine_max_speed_x": ["500", "200", "600", "250"]
```
The matching process lists the same two pairs (`print_extruder_id` `["1", "1"]`,
`print_extruder_variant` as above) and holds 2 values per key, e.g. `outer_wall_speed`
`["200", "260"]`; a filament for it lists `["Direct Drive Standard", "Direct Drive High Flow"]` and
holds 2 values per key, e.g. `filament_max_volumetric_speed` `["16", "24"]`.
**Four extruders, one nozzle volume type each** (a tool changer): `E = 4`, every `V_i = 1`, so
`S = 4`; stride-1 keys hold 4 values, `machine_max_*` hold 8:
```json
"nozzle_diameter": ["0.4", "0.4", "0.6", "0.4"],
"extruder_type": ["Direct Drive", "Direct Drive", "Direct Drive", "Direct Drive"],
"extruder_variant_list": ["Direct Drive Standard", "Direct Drive Standard",
"Direct Drive Standard", "Direct Drive Standard"],
"printer_extruder_id": ["1", "2", "3", "4"],
"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive Standard",
"Direct Drive Standard", "Direct Drive Standard"],
"default_nozzle_volume_type": ["Standard", "Standard", "Standard", "Standard"],
"retraction_length": ["0.8", "0.8", "1.2", "0.8"],
"machine_max_speed_x": ["500", "200", "500", "200", "500", "200", "500", "200"]
```
The (normal, silent) pair is repeated per extruder: `["500", "200"]` would be width 2 against
`S × k = 8` (the loader pads it to `500, 200, 500, 500, …`) and `["500"]` width 1; both are errors. The
process mirrors the four pairs (`print_extruder_id` `["1", "2", "3", "4"]`) with 4 values per key, or
omits the variant keys altogether when nothing differs per extruder (then one value per key). A
filament for it lists only `["Direct Drive Standard"]`: one entry, so one value per key. Drop the
layout from this printer and the widths stay the same, since the default list gives `S = E = 4`; but
the loader then keeps only the first variant ([variant length](#widths)): `retraction_length`
becomes `0.8` on every extruder and extruder 3 loses its `1.2`.
### Adding a variant inserts its values at its variant index
Indexes run extruder-major: extruder 1's variants in `extruder_variant_list` order, then extruder
2's. A new variant's values go in at its index, not at the end. Giving extruder 1 of a two-extruder
printer a High Flow option, when only extruder 2 had one:
| Key | Before | After |
| --- | --- | --- |
| `extruder_variant_list` | `["Direct Drive Standard", "Direct Drive Standard,Direct Drive High Flow"]` | `["Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"]` |
| `printer_extruder_id` | `["1", "2", "2"]` | `["1", "1", "2", "2"]` |
| `printer_extruder_variant` | `[Standard, Standard, High Flow]` | `[Standard, High Flow, Standard, High Flow]` (full strings in the file) |
| `retraction_length` (stride 1) | `["0.8", "1.0", "1.2"]` | `["0.8", "?", "1.0", "1.2"]`: one value at index 1 |
| `machine_max_speed_x` (stride 2) | `["500", "200", "600", "250", "700", "300"]` | `["500", "200", "?", "?", "600", "250", "700", "300"]`: a (normal, silent) pair at position 2 |
| `print_extruder_id` / `print_extruder_variant` / `outer_wall_speed` | mirror the printer | the same insertion at index 1 |
| `filament_extruder_variant` `[Standard, High Flow]` | — | unchanged: filament variants carry no extruder id, so the existing High Flow variant now serves both extruders |
Every `?` is a measured value for that nozzle, on the machine limits as much as on retraction.
Removing or renaming a variant shifts the later values the same way in reverse; a variant string that
no longer matches the enum is simply a variant nothing selects.
### Padding, truncation and composition
**Every key in the set widens, whether or not a file restates it.** At load each variant key of the
composed config is resized to the length of the preset's own `*_extruder_variant` (the one-entry
default when it writes none) × stride: a short array is **padded by repeating its first value**, a
long one is truncated to its first values, without a word from the loader or the validator. The rule
does not follow from this padding: a file writes a variant key at **exactly `variant length × stride`**
or not at all. Any other length, one value included (which the loader spreads over every variant, at
stride 2 over normal *and* silent alike), is a mistake the loader hides and
`orca_profile_tool.py check` reports as an error in the selectable preset that writes it, even when
every value is the same; `check --strict` also reports an array that reaches a selectable preset at
another width
([variant arrays](validation.md#variant-arrays)).
The loader sizes a list-less preset of any type to one variant at this step, a list-less machine
included: a two-extruder machine without a layout that writes `retraction_length` `["0.8", "0.9"]`, the
width the equation asks for, stores `["0.8"]`, which the pair rebuild and the slice-time collapse hand
to both extruders. A 3-value array on a preset of variant length 2 keeps its first two; at length 4 it
becomes `[a, b, c, a]`.
Composition hands down widths in two ways:
- **`inherits` hands down the parent's resized arrays.** A base is stored after its own resize, at
the length of its own `*_extruder_variant` (one variant for a base of any type that writes none,
whatever its extruder count), so an array wider than the base's list is cut to its first values
before any child sees it, and a child that adds variants gets those first values padded. Widen an
array only on a preset whose own resolved list already has the entries. `check` judges selectable
presets only, at the width each file wrote: an array a base's own resize cuts is not seen (a review
item), and an inherited array of another width than a child's list is left to the loader's resize
unless `check --strict`, which asks the child to restate it at its own width.
- **`include` hands down the template's diff at its pre-resize width.** The template contributes every
key where its composed config differs from the built-in defaults, taken before its own resize, so the
arrays it writes arrive at the width its file wrote, and the includer's own list sizes them. A key the
template sets to the built-in default is not passed on
([`include`](vendor-bundle.md#inherits-and-include)).
The id keys are the trap in this padding: `printer_extruder_id` and `print_extruder_id` are members
with default `[1]`, so a missing or one-value id array beside a longer variant list is padded to
extruder 1 at every index. That is right on a single-extruder printer and wrong on a multi-extruder
one, where every variant is then addressed as extruder 1's. A machine escapes it only where the
load-time pair rebuild of [Printer rule 2](#printer-machine) runs (`single_extruder_multi_material`
off); a process's `print_extruder_id` is never rebuilt at load. `check`
holds an id array that reaches a selectable preset, written or inherited, to one entry per variant on
any printer; `fix-variant` never pads an id array or a variant list, since those address the
variants rather than fill them.
Consequences:
- A base that gains a variant silently pads every descendant that restates a variant array at the old
width, and a short `machine_max_*` array copies variant 0's *normal* limit into the silent entries
too; a descendant that restates nothing inherits the widened array and needs no edit. Extend, in one
change: the printer base and each nozzle-diameter sibling that restates a variant array, the process
bases that mirror the printer's variants, and the filaments that should cover the variant.
- A one-value override is reported, and the loader spreads it over **all** variants, overwriting the
ones that should differ.
- A process or filament that omits the new variant is not padded; the variant resolves to its index 0.
### Slice time and existing users
**Slice time collapses variants to extruders.** When printer, process and filaments are combined on a
printer with several extruders or several variant strings, each printer and process variant key is
re-gathered to one entry per extruder (× stride) in extruder order, using each extruder's live nozzle
volume type, or to one entry per nozzle volume type for an extruder that holds several (Hybrid, or
`extruder_nozzle_stats` listing more types than there are extruders); on any other printer they are
not re-gathered and variant index 0 is read. Filament keys are re-gathered to one entry per filament,
on a printer with a single variant too once a filament has several variants, and under a dynamic
nozzle map to one entry per variant each filament prints through. Custom G-code and the
`machine_max_*` limits therefore index by extruder or filament, never by variant index; variant order
matters only inside the preset. An extruder with no matching variant reads variant index 0, extruder
1's first variant, whichever extruder it is; under layered nozzle grouping, `get_config_index_base`
reads the collapsed arrays and falls back to the same extruder's first entry instead.
**Existing user presets and projects follow the variant string, not the position.** A user preset
stores every variant array it changed (nullable keys as per-variant diffs, `nil` where equal to the
parent). On load each parent variant takes the child's value for the variant with the same extruder id
and variant string; variants the parent gained keep the parent's value, and a child whose arrays the
parent cannot map keeps its own. Per-object process overrides are remapped when a printer change
alters the extruder count or the length of `printer_extruder_variant`, by variant string alone (no
extruder id; of several matching values the smallest wins), and a single-value override applies to
every variant. Adding or reordering variants in a shipped preset is therefore safe for existing
users; renaming a variant, or moving it to another extruder id, loses their values for it.
**A new nozzle volume type changes no array.** Adding one to the code widens nothing until a profile
lists the new variant string; until then every existing array keeps its length and meaning.
## The four key sets
Membership is literal (four `std::set<std::string>` initializers in `src/libslic3r/PrintConfig.cpp`)
and **not guessable from names**: `ironing_speed`, `skirt_speed`, `wipe_speed`, `scarf_joint_speed`,
`small_support_perimeter_speed` and `wipe_tower_max_purge_speed` are process speeds outside the set,
while every process `*_acceleration` and `*_jerk` key is inside, and so are
`small_perimeter_threshold`, `top_solid_infill_flow_ratio` and `slowdown_for_curled_perimeters`;
`filament_flush_temp` is in and `filament_flush_temp_fast` out; `use_firmware_retraction` is out and
`travel_slope` and `retract_lift_enforce` in. The three variant-list keys and the two id
keys are members of their own set (the list sizes itself, a no-op; the id keys are padded like any
other member, [the id trap](#padding-truncation-and-composition)), while `extruder_variant_list` is in
no set: the variant-length resize leaves it alone, and only the pair rebuild of
[Printer rule 2](#printer-machine) pads it. The per-extruder `printer_extruder_options` is listed
last for contrast; it is not a variant set.
`check` and `fix-variant` read the four sets from `PrintConfig.cpp` on every run, so they follow the
engine. The lists below are from the 2026-09-29 checkout; regenerate them from the repository root
before relying on them (the recipe strips comments, since an initializer can carry a commented-out entry):
```bash
python3 - <<'EOF'
import re
src = open('src/libslic3r/PrintConfig.cpp', encoding='utf-8', errors='replace').read()
for name in ['printer_options_with_variant_1', 'printer_options_with_variant_2',
'print_options_with_variant', 'filament_options_with_variant',
'printer_extruder_options']:
body = re.search(r'std::set<std::string>\s+' + name + r'\s*=\s*\{(.*?)\};', src, re.S).group(1)
body = re.sub(r'/\*.*?\*/', '', body, flags=re.S)
body = re.sub(r'//[^\n]*', '', body)
print(name, sorted(set(re.findall(r'"([^"]+)"', body))))
EOF
```
**`printer_options_with_variant_1`**, machine, stride 1 (27): `deretraction_speed`, `hotend_cooling_rate`, `hotend_heating_rate`, `long_retractions_when_cut`, `nozzle_flush_dataset`, `nozzle_type`, `nozzle_volume`, `printer_extruder_id`, `printer_extruder_variant`, `retract_after_wipe`, `retract_before_wipe`, `retract_length_toolchange`, `retract_lift_above`, `retract_lift_below`, `retract_lift_enforce`, `retract_restart_extra`, `retract_restart_extra_toolchange`, `retract_when_changing_layer`, `retraction_distances_when_cut`, `retraction_length`, `retraction_minimum_travel`, `retraction_speed`, `travel_slope`, `wipe`, `wipe_distance`, `z_hop`, `z_hop_types`
**`printer_options_with_variant_2`**, machine, stride 2 (16): `machine_max_acceleration_e`, `machine_max_acceleration_extruding`, `machine_max_acceleration_retracting`, `machine_max_acceleration_travel`, `machine_max_acceleration_x`, `machine_max_acceleration_y`, `machine_max_acceleration_z`, `machine_max_jerk_e`, `machine_max_jerk_x`, `machine_max_jerk_y`, `machine_max_jerk_z`, `machine_max_junction_deviation`, `machine_max_speed_e`, `machine_max_speed_x`, `machine_max_speed_y`, `machine_max_speed_z`
**`print_options_with_variant`**, process, stride 1 (45): `bridge_acceleration`, `bridge_speed`, `default_acceleration`, `default_jerk`, `default_junction_deviation`, `enable_overhang_speed`, `gap_infill_speed`, `infill_jerk`, `initial_layer_acceleration`, `initial_layer_infill_speed`, `initial_layer_jerk`, `initial_layer_speed`, `initial_layer_travel_acceleration`, `initial_layer_travel_jerk`, `initial_layer_travel_speed`, `inner_wall_acceleration`, `inner_wall_jerk`, `inner_wall_speed`, `internal_bridge_speed`, `internal_solid_infill_acceleration`, `internal_solid_infill_speed`, `outer_wall_acceleration`, `outer_wall_jerk`, `outer_wall_speed`, `overhang_1_4_speed`, `overhang_2_4_speed`, `overhang_3_4_speed`, `overhang_4_4_speed`, `print_extruder_id`, `print_extruder_variant`, `slowdown_for_curled_perimeters`, `small_perimeter_speed`, `small_perimeter_threshold`, `sparse_infill_acceleration`, `sparse_infill_speed`, `support_interface_speed`, `support_speed`, `top_solid_infill_flow_ratio`, `top_surface_acceleration`, `top_surface_jerk`, `top_surface_speed`, `travel_acceleration`, `travel_jerk`, `travel_speed`, `travel_speed_z`
**`filament_options_with_variant`**, filament, stride 1 (54): `activate_air_filtration`, `activate_air_filtration_during_print`, `activate_air_filtration_on_completion`, `adaptive_pressure_advance`, `adaptive_pressure_advance_bridges`, `adaptive_pressure_advance_model`, `adaptive_pressure_advance_overhangs`, `complete_print_exhaust_fan_speed`, `during_print_exhaust_fan_speed`, `enable_pressure_advance`, `filament_adaptive_volumetric_speed`, `filament_cooling_before_tower`, `filament_deretraction_speed`, `filament_extruder_variant`, `filament_flow_ratio`, `filament_flush_temp`, `filament_flush_volumetric_speed`, `filament_ironing_flow`, `filament_ironing_inset`, `filament_ironing_spacing`, `filament_ironing_speed`, `filament_long_retractions_when_cut`, `filament_max_volumetric_speed`, `filament_pre_cooling_temperature`, `filament_pre_cooling_temperature_nc`, `filament_preheat_temperature_delta`, `filament_ramming_travel_time`, `filament_ramming_travel_time_nc`, `filament_ramming_volumetric_speed`, `filament_ramming_volumetric_speed_nc`, `filament_retract_after_wipe`, `filament_retract_before_wipe`, `filament_retract_length_nc`, `filament_retract_length_toolchange`, `filament_retract_lift_above`, `filament_retract_lift_below`, `filament_retract_lift_enforce`, `filament_retract_restart_extra`, `filament_retract_restart_extra_toolchange`, `filament_retract_when_changing_layer`, `filament_retraction_distances_when_cut`, `filament_retraction_length`, `filament_retraction_minimum_travel`, `filament_retraction_speed`, `filament_wipe`, `filament_wipe_distance`, `filament_z_hop`, `filament_z_hop_types`, `long_retractions_when_ec`, `nozzle_temperature`, `nozzle_temperature_initial_layer`, `pressure_advance`, `retraction_distances_when_ec`, `volumetric_speed_coefficients`
**`printer_extruder_options`**, machine, one value per extruder, not a variant set (8):
`default_nozzle_volume_type`, `extruder_max_nozzle_count`, `extruder_printable_area`,
`extruder_printable_height`, `extruder_type`, `max_layer_height`, `min_layer_height`,
`nozzle_diameter`. These are never addressed by variant index; `extruder_offset`, `extruder_colour` and
`extruder_variant_list` are per extruder too. A shorter array acts as padded with its first value, and
entries beyond the extruder count are never read
([per-extruder vectors](machine-profiles.md#multi-extruder-idex-and-tool-changers)).
## Checking and testing
Checklist for a new variant profile set:
1. Decide the variants per extruder from the real hardware; pick legal strings only.
2. Printer base: `extruder_type`, `extruder_variant_list`, flattened `printer_extruder_variant` +
`printer_extruder_id`, `default_nozzle_volume_type`; every variant array at variant length, every
`machine_max_*` at 2 × variant length as (normal, silent) pairs, even where the values are the
same, values in variant order.
3. A process base per variant layout, or no variant keys at all.
4. Filaments for the printer: a variant list covering the intended variants, every variant key at that
width, measured values per nozzle volume type.
5. Run the usual authoring commands and full checks. `check` holds each array of steps 2–4 to the
width of the selectable preset that writes it, `check --strict` also to every selectable preset it
reaches, and
the printer's layout keys through every selectable preset
([variant arrays](validation.md#variant-arrays)); `check_variant_names` holds every variant string,
`extruder_type`, `nozzle_volume_type` and `default_nozzle_volume_type` to the enums
([variant names](validation.md#variant-names)). The choice of variants, the variant order and
the measured values are not checked.
6. In the app, for each nozzle volume type in the sidebar combo: slice and confirm the G-code uses that
variant's values (e.g. volumetric speed limit, retraction). `validate_slice` only slices the default
nozzle volume type.
To review rather than author, run the same list against the diff. `check` catches a wrong array length,
a layout key out of step, and a variant string the enums cannot build. The failures no check catches: a
new variant appended instead of inserted at its index, an array widened on a base whose list is
shorter (cut before any child inherits it), a process lacking a pair its printer can select, and a High
Flow variant copied from Standard.
### UI facts to design around
- The single-extruder sidebar shows a **nozzle volume type combo** (tooltip `Flow`, in place of the
nozzle-diameter selector) only when `extruder_variant_list` offers more than one distinct variant
string (`support_different_extruders`); four extruders listing `Direct Drive Standard` each show
none.
- The two-extruder sidebar's per-extruder nozzle volume type combos are shown for BBL printers only.
Another vendor's multi-extruder printer falls back to the single-extruder layout: one combo (for the
first extruder) when the variants differ, otherwise the nozzle-diameter selector, so the other
extruders' nozzle volume type stays at `default_nozzle_volume_type`.
- High Flow is hidden from the combo when `printer_variant` is `0.2` or the printer model is
`Bambu Lab X1E`, and E3D High Flow unless `printer_variant` is `0.4` or `0.6`. A variant listed on
another nozzle diameter is never selectable there.
- The filament tab shows a variant switch built from the filament's own `filament_extruder_variant`;
a multi-variant filament whose tab shows none did not resolve its variant list through `include` or
`inherits`. The printer and process tabs show one entry per extruder instead, labelled with that
extruder's live nozzle volume type (two for Hybrid), and only on two-extruder printers whose
`extruder_variant_list` offers more than one variant string; elsewhere they edit the variant of the
nozzle volume type selected in the sidebar (an X1C shows no switch).
### Worked examples in the tree
`BBL/machine/fdm_bbl_3dp_001_common.json` (one extruder), `fdm_bbl_3dp_002_common.json` (two
extruders), `Bambu Lab X1 Carbon 0.4 nozzle.json` (Standard + High Flow), `Bambu Lab H2D 0.4
nozzle.json` (extruders with different variant sets), `Bambu Lab X2D 0.4 nozzle.json` (Direct Drive +
Bowden), with their `@BBL` processes and filaments. The H2D and X2D examples also carry
`E3D High Flow` variants. BBL's multi-variant filament lists come from `fdm_filament_template_*`
presets that the printer-specific filaments pull in with `include`, not from their `inherits` chain.
@@ -1,286 +0,0 @@
# Filament profiles and OrcaFilamentLibrary
`OrcaFilamentLibrary` is the filament-only bundle the loader reads **first**, so any vendor's filament may
inherit a library preset by name. It is the only cross-bundle parent: vendor-to-vendor inheritance
always fails.
## Where a filament goes
| Contribution | Location |
| --- | --- |
| Generic material for all printers | `OrcaFilamentLibrary/filament/Generic <mat> @System.json` |
| A brand's product, all printers | `OrcaFilamentLibrary/filament/<Brand>/` |
| A brand's tune for one printer vendor | `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/` (recommended); `<PrinterVendor>/filament/<Brand>/` also works |
| A printer vendor's tune of a generic, or its own product | `<PrinterVendor>/filament/` |
Both locations in the third row are supported: `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/<Name>.json`
(the shape the wiki shows) and `<PrinterVendor>/filament/<Brand>/`. The library path is the one a
filament brand should contribute to: `OrcaFilamentLibrary/filament/<Brand>/` is the brand's own folder,
while a printer vendor's folder belongs to that printer vendor.
Library layout: `filament/base/fdm_filament_*.json` material roots, root-level
`Generic <mat> @System.json` generics, and one subfolder per brand, which may nest printer-specific
tunes one level deeper. Adding a brand means adding a folder here; the folder name is a directory label
only, and `filament_vendor` inside the JSON is the real vendor string.
## The three-part shape
```jsonc
// OrcaFilamentLibrary/filament/Polymaker/Fiberon PA6-CF @base.json — the product root: identity + material values
{ "type": "filament", "name": "Fiberon PA6-CF @base", "from": "system",
"instantiation": "false", "inherits": "fdm_filament_pa",
"filament_id": "OFkOviHk", // minted here by generate-id; every child inherits it
"filament_vendor": ["Polymaker"], "filament_type": ["PA6-CF"], /* … */ }
// OrcaFilamentLibrary/filament/Polymaker/Fiberon PA6-CF @System.json — the selectable all-printer shim, 7 keys
{ "type": "filament", "name": "Fiberon PA6-CF @System", "from": "system",
"instantiation": "true", "inherits": "Fiberon PA6-CF @base",
"setting_id": "…", "compatible_printers": [] }
// BBL/filament/Polymaker/Fiberon PA6-CF @BBL X1C.json — a printer tune (BBL keeps its own copy of the @base)
{ …, "inherits": "Fiberon PA6-CF @base", "filament_max_volumetric_speed": ["14"],
"compatible_printers": ["Bambu Lab X1 Carbon 0.4 nozzle", …] }
```
- `@base` is the convention for a root; a root is really `instantiation: "false"`. A base carries
**no** `setting_id`, no `compatible_printers` and no `filament_settings_id`. Only the `setting_id`
half is enforced; the other two are unchecked, so a neighbouring base that carries them is no model.
- Every `@System` shim must be `"instantiation": "true"`; one set to `"false"` would ship but could
never be selected, and no check catches it. The shim exists only for products in the library.
- `filament_cost`, `filament_density`, `filament_type` and `filament_vendor` belong on the root and
should not appear in a printer tune.
- A brand `@base` duplicated across bundles is legal (a vendor bundle may keep its own copy of a
library product root, with the same id): bases never become selectable presets and the
duplicate-name error covers only those, so there is none.
- You may inherit from an instantiated preset as well as from a base.
## Colour is a runtime property
`filament_id` identifies a product, not a colour; filament sync and AMS read the colour from the spool
at runtime. A product ships one all-printer preset and the colour is chosen at runtime, never a sibling
preset that differs only by colour. A material family (PLA vs PLA Matte vs PLA Silk) is a new product;
a colour is not. A printer tune keeps the product alias and does not multiply per colour either.
CI does not catch this (per-colour presets pass `check`), so it is a review call.
## The two most common contributions
**A printer vendor tuning a generic.** Keep the `Generic X` alias so it shadows the library preset on
your printers, inherit `Generic X @System`, declare **no** `filament_id` (inheriting the library's is
correct: the product really is the library's generic), and give it a non-empty `compatible_printers` in
its own file:
```jsonc
// <Vendor>/filament/Generic PETG @Acme One 0.4 nozzle.json
{ "type": "filament", "name": "Generic PETG @Acme One 0.4 nozzle", "from": "system",
"instantiation": "true", "inherits": "Generic PETG @System",
"filament_flow_ratio": ["0.95"], "filament_max_volumetric_speed": ["10"],
"compatible_printers": ["Acme One 0.4 nozzle"] }
```
**A printer vendor's own branded product.** Give it a `@base` root on a material base so `generate-id`
can mint the id, then one instantiated leaf per printer in the same bundle. Inheriting
`Generic X @System` directly gives the product the generic's id, which the tool cannot fix
([ids.md](ids.md#what-generate-id-does-and-does-not-fix)). No `@System` shim: that is only for a product
entering OrcaFilamentLibrary.
```jsonc
// <Vendor>/filament/Acme Aura PETG @base.json — instantiation false, no setting_id
{ "type": "filament", "name": "Acme Aura PETG @base", "from": "system",
"instantiation": "false", "inherits": "fdm_filament_pet",
"filament_vendor": ["Acme"], "filament_type": ["PETG"] } // filament_id minted here
// <Vendor>/filament/Acme Aura PETG @Acme One 0.4 nozzle.json
{ "type": "filament", "name": "Acme Aura PETG @Acme One 0.4 nozzle", "from": "system",
"instantiation": "true", "inherits": "Acme Aura PETG @base",
"filament_max_volumetric_speed": ["11"],
"compatible_printers": ["Acme One 0.4 nozzle"] }
```
Omit `filament_settings_id` from new presets: it is runtime bookkeeping the app rewrites to the preset
name.
To offer either kind by default, add its name to each model's `default_materials`; put it first in the
machine's `default_filament_profile` only if it should be the preselected filament
([machine keys](machine-profiles.md#other-keys)).
## `compatible_printers`
- **Library fallbacks** (`@System`): empty `[]` or absent, so they are offered on all printers except
where [alias shadowing](#alias-shadowing) supplies a printer-specific tune.
- **Library printer-specific tunes**: non-empty, listing exact printer **variant** names. These
supersede a same-alias fallback just like a tune in a printer vendor's bundle.
- **Instantiated filaments in every other vendor**: non-empty, listing exact printer **variant** names.
Enforced twice, but not identically: `validate_system` reads the resolved config, so an inherited list
satisfies it, while `check` reads the file's **own** key. Write the list in the file itself. This is
the most common filament CI failure.
- Emptying it to "make it apply everywhere" fails that check *and* collides with the library generic's
`filament_id` on every printer.
- Copying a base's full printer list onto a nozzle-specific tune produces duplicate combobox entries: a
real shipped bug twice over.
## Overlapping coverage: one variant, one profile per product
`filament_id` is the **product** key, not the preset key: every preset of one product shares it
(`<filament_vendor>/<filament_type>/<alias>`). So if one printer variant appears in the
`compatible_printers` of two presets of that product, the slicer cannot tell them apart at AMS match
time. `validate_system` reports `Ambiguous AMS filament match: N filament presets share filament_id "X"
and are all compatible with printer "Y"`; `orca_profile_tool.py check` does **not** see it and passes.
Resolve the overlap by **specificity**: keep the variant on the most specific profile and remove it from
every more general one. Deleting a profile is the least preferred fix: moving coverage keeps the tune
that users rely on.
Judge specificity from the profile's `compatible_printers` (how many variants it actually covers) and
use the name only as a secondary, often vague hint; decide by the lists, with a judgement call on the
name. Naming conventions differ by vendor: BBL's is the reference (`@<Vendor> <Model>` for a whole
model, `@<Vendor> <Model> <nozzle> nozzle` for one variant, `@<Vendor>` for a vendor-wide generic), but
others vary (`@<printer model>`, a printer serial, or Creality's `@<Model>-all`). A name never overrides
the list; see [preset naming](naming.md#filament) for the shapes.
Specificity, most to least:
1. **Variant-specialized**: lists a single printer variant (BBL-style
`… @<Vendor> <Model> <nozzle> nozzle`).
2. **Model-specialized**: lists the variants of one printer model (BBL-style `… @<Vendor> <Model>`). It
should cover every variant of its model, not only the nozzle it was authored for.
3. **Family / series**: lists variants spanning a printer family or series.
4. **Generic / catch-all**: vendor-wide, covering many unrelated models (often the bare
`Generic <mat> @<Vendor>`).
Rules:
- A model-specialized profile is extended to **all** variants of its model, and each variant it thereby
starts covering is removed from the family and generic profiles that also listed it, including
variants that had no overlap before. Apply it per nozzle, not just 0.4.
- Apply it **per product**: trim only the material that has a specialized profile from the generic; a
material whose product has no specialized profile keeps the variant in the generic.
- Never strip coverage a variant has nowhere else to get. If a variant has no variant-level specialized
profile, the next level down keeps it; when the model has specialized profiles, the model-level one
wins over the family and generic ones.
- Moving coverage is preferred over deleting. If a profile must be deleted, remove the more general one,
not the specialized profile that carries the tune.
- After moving coverage, repoint the affected machine's `default_filament_profile` and clean the model's
`default_materials`: they should name the most specific profile that covers the variant, and should
not keep generic entries that no longer cover the model. This rule applies equally when adding or
fixing defaults.
Several profiles of one product with **disjoint** `compatible_printers` is the intended end state.
Adding coverage to the specialized profile and removing it from the generic is the preferred direction.
**Detection caveat:** `orca_profile_tool.py check` is blind to this; only the validator behind the full
`./scripts/check_profile.sh` reports it (`validate_system`). Always confirm with that, not the
vendor-scoped loop.
## Alias shadowing
A printer-specific filament in either the library or a vendor bundle supersedes the library fallback on
the printers it lists. The matching key is the **alias**: the preset name up to the **first** `@`,
right-trimmed (no `@` → the whole name). So `QIDI ABS-GF@Q2-Series` aliases to `QIDI ABS-GF`.
A library preset with an empty `compatible_printers` is hidden on every printer that a same-alias preset
lists in a non-empty `compatible_printers`, whether that preset is in the library or in a vendor bundle
(a printer matches by its own name or its parent's).
Two consequences:
- **Only an unrestricted library fallback can be shadowed.** Two printer-specific presets sharing an
alias do not hide each other; overlapping lists for the same product trip the ambiguous-match error
above instead.
- This is why adding `Generic PLA @<printer>` to a vendor silently removes the library
`Generic PLA @System` from that printer. That is intended, and the reason a vendor tuning a generic
must **keep the `Generic X` alias**.
The literal spelling `Generic <mat> @System` is load-bearing beyond shadowing: when a user preset, an
imported preset or a 3MF project names a parent that no longer resolves and contains `Generic`, the
loader rewrites the name into `Generic <mat> @System` and retries. Only the library ships those names,
so keep them.
## `filament_id`, `filament_vendor`, `filament_type`
`filament_id` is minted from the triple `(filament_vendor, filament_type, alias)`. `filament_vendor` and
`filament_type` are therefore **identity, not decoration**: editing either, or the alias, re-mints the
id. Read `docs/HLSD/filament_id.md` before changing any of them, and see [ids.md](ids.md) for the
tooling.
A filament with no resolvable `filament_id` anywhere in its `inherits` chain is a **hard load error**
that discards the vendor bundle. The id inherits across bundles, so a vendor's `Generic ABS @X`
inheriting `Generic ABS @System` gets the library's id for free; a vendor's own product must resolve its
own.
- `filament_type` **must be a JSON array**: the one vector key `check` rejects as a scalar outright. A
scalar `"PP"` once hung the filament and printer selection UI.
- It is an **open** enum: an unlisted value is accepted silently and falls back to 190–300 °C defaults
and adhesion 1.0. Prefer a value from `MaterialType::all()` in
`src/libslic3r/MaterialType.cpp`, or add a row there.
- Generics use `filament_vendor: ["Generic"]`, which `fdm_filament_common` already defaults to.
## `"nil"`
`"nil"` is legal only in an option defined as nullable (`add_nullable`, or `nullable = true`, in
`src/libslic3r/PrintConfig.cpp`). Anywhere else it fails the file, and with it the **whole bundle**
(`Failed loading configuration file`, after `Deserializing nil into a non-nullable object` or
`Invalid value provided for parameter <key>: nil`). To leave a non-nullable key unset, omit it; do not
write `nil`.
In filament presets a minority of the `filament_*` keys are nullable, plus `long_retractions_when_ec`
and `retraction_distances_when_ec`. About half of them are the extruder overrides (`filament_retraction_length`,
`filament_z_hop`, `filament_wipe`, `filament_retract_*`, `filament_retraction_speed`,
`filament_deretraction_speed`, `filament_retraction_minimum_travel`, `filament_wipe_distance`,
`filament_long_retractions_when_cut`, `filament_retraction_distances_when_cut`, …), where `nil` means
*keep the printer's or extruder's own value*. The rest are ordinary nullable options
(`filament_flow_ratio`, `filament_flush_temp`, `filament_adaptive_volumetric_speed`, …), where it means
*unset*. Check the option's definition before writing `nil` anywhere else.
## Tuning per nozzle and per variant
Between a product's `@X` and `@X 0.N nozzle` tunes the keys that usually differ, most often first, are
`filament_max_volumetric_speed`, `filament_retraction_length`, `slow_down_min_speed`,
`filament_flow_ratio`, `slow_down_layer_time`, `nozzle_temperature` and `pressure_advance` (switched on
by `enable_pressure_advance`).
Use measured values for the material, hotend, extruder and nozzle combination. Neither maximum
volumetric speed nor pressure advance has a universal nozzle-only lookup table. When cloning a 0.4
preset for a 0.2 nozzle, explicitly revisit flow limits; do not infer a pressure-advance value, or a
required direction of change, from diameter alone.
On a printer with extruder variants, a filament tunes these per variant too:
`filament_max_volumetric_speed`, `filament_flow_ratio`, `nozzle_temperature`, pressure advance and the
retraction overrides carry one value per variant of `filament_extruder_variant` (Standard, High Flow, …). The
exact key set is [`filament_options_with_variant`](extruder-variants.md#the-four-key-sets);
`slow_down_min_speed` and `fan_max_speed` are not in it. Keep every such array at exactly that width, even where the
setting does not differ per variant, and measure the High Flow variant rather than copying Standard
([extruder-variants.md](extruder-variants.md#filament)).
## Bed temperature is twelve keys, not one
There is no single "bed temperature". The plate type selected for the printer (`Cool Plate`,
`Engineering Plate`, `High Temp Plate`, `Textured PEI Plate`, `Textured Cool Plate`, `Supertack Plate`)
picks one of six keys, each with an `_initial_layer` twin: `cool_plate_temp`, `eng_plate_temp`,
`hot_plate_temp`, `textured_plate_temp`, `textured_cool_plate_temp` and `supertack_plate_temp`.
`textured_cool_plate_temp` is the one most often forgotten. A non-BBL printer with
`support_multi_bed_types` off hides the plate selector and uses the printer preset's `default_bed_type`
(High Temp Plate, `hot_plate_temp`, when unset or invalid), but a loaded project or a CLI config can
still carry another plate. So set every plate the printer plausibly has, as the sibling presets in the
bundle do.
## Style
- Overrides, not full copies: an instantiated filament preset carries around a dozen non-meta keys,
and a library `@System` shim two or three. A preset that restates fifty-plus keys from its parent is
the pattern to move away from, not to copy. Commit `6943b6ddc3` is the stated model for converting such presets: flip true
bases to `instantiation: "false"`, strip their `compatible_printers`, `setting_id` and
`filament_settings_id`, and add `renamed_from` on the surviving selectable preset. A value every
printer tune of a product shares goes on the product's `@base`
([shared bases](shared-bases.md#levels)).
- Prefer the library's `fdm_filament_*` bases over a vendor-local copy; for a new preset, even in a
bundle whose older presets use one: a local copy drifts from the library's.
- Canonical key order, written by `orca_profile_tool.py normalize` when it rewrites a file: `type`,
`name`, `renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, `instantiation`, then
everything else in the order you wrote it. Not enforced on its own: a file that leads with
`compatible_printers` passes `check`.
- **Every vector-typed (`co…s`) key must be a JSON array.** Only a scalar `filament_type` is an outright
error; `normalize` silently arrayifies five more (`filament_cost`, `filament_density`,
`temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`), and `check` fails
when it would. Every other vector key is on you, including `filament_start_gcode`,
`filament_end_gcode`, `filament_extruder_variant`, `compatible_printers` and the plate temperatures.
@@ -1,156 +0,0 @@
# `setting_id` and `filament_id`
Orca-generated ids are deterministic hashes of identity. **Never invent an id or copy a sibling's
`setting_id`.**
Use `scripts/orca_profile_tool.py`; the two special cases are
[a wrongly inherited filament id](#what-generate-id-does-and-does-not-fix) and
[BBL's authoritative setting ids](#bbls-exception-precisely).
`docs/HLSD/filament_id.md` is the authoritative design document for `filament_id`: the id landscape,
the checks CI runs, and the Bambu catalog map. This page is the tooling half.
| | `setting_id` | `filament_id` |
| --- | --- | --- |
| Identifies | one selectable preset | one filament **product** |
| Key hashed | `<vendor folder>/<type>/<name>` | `filament_product/<filament_vendor>/<filament_type>/<alias>`, using the resolved (inherited) first values and the name up to the first `@`, right-trimmed |
| Shape | 16 base62 characters | `OF` + 6 base62 characters |
| Required on | every `instantiation: "true"` preset | every **instantiated** filament, own or inherited |
| Forbidden on | bases (`instantiation` not `"true"`) | — (a product's root base is exactly where it belongs) |
| Scope | unique across the whole tree | shared by every preset of the product, in every bundle |
`<type>` is `machine`, `process` or `filament`, and the vendor is the **folder** name (`BBL`), not the
display name (`Bambulab`). Renaming a preset changes its `setting_id`; renaming a filament's alias, or
editing its `filament_vendor` or `filament_type`, also changes its `filament_id`, and the old id is not
forwarded.
## The tool
`scripts/orca_profile_tool.py` takes a subcommand:
| Command | Does |
| --- | --- |
| `check` | everything CI's `profile_tool` step runs; see [validation.md](validation.md#orca_profile_toolpy-check) |
| `generate-id` | writes `setting_id` and `filament_id` |
| `normalize` | rewrites profile files into their canonical shape |
| `trim` | deletes profile files no `<Vendor>.json` list references |
| `update-index` | rebuilds the `*_list` sections from the files on disk |
The order after adding, renaming or deleting files is `normalize` → `update-index` → `generate-id` →
`check`. Each step feeds the next, so it is not interchangeable. The
[authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands.
> **`trim` deletes.** It removes every profile file the index does not list, including the one you just
> added and have not registered yet. Register first, or skip `trim` entirely: it is a cleanup sweep, not
> part of landing a profile. Preview with `--dry-run`.
**Register, then mint.** The `filament_id` pass reads `<Vendor>.json`'s `filament_list`, not the
filesystem (the `setting_id` pass walks the filesystem, so a bundle whose index has not landed yet is
still assignable). A new filament file is therefore invisible to `generate-id`'s `filament_id` pass
until it is registered; its `setting_id` is written regardless.
- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`) and
writes nothing.
- `--filament-id` / `--setting-id` narrow `generate-id` to one pass; they exclude each other, and
passing neither writes both.
- `--vendor` is repeatable and narrows **only what is written**: an id is a function of its own key
alone, so a narrowed run writes exactly what a full run would. An unknown vendor exits 1 before any
write. `--vendor` on `check` narrows the per-vendor checks only; the `setting_id` and `filament_id`
passes stay tree-wide.
- `--profiles DIR` points any command at another tree; see
[Checking a copy of the tree](validation.md#checking-a-copy-of-the-tree).
- `--profile-type` narrows `normalize`, `trim` and `update-index` to `machine_model`, `process`,
`filament` or `machine`.
- Exit codes: 0 clean, 1 errors found (`generate-id` still writes what it could), 2 argparse misuse.
- Output is ANSI-coloured; searching for the literal `[ERROR]` still works.
`generate-id` is **idempotent and byte-preserving**: BOM and CRLF are kept, and each pass touches only
its one key line. A legitimate `generate-id` diff is one or two changed lines per file: a new instantiated
filament gets both a `filament_id` and a `setting_id`. `normalize` is the opposite by design (it
rewrites whole files into canonical shape), which is why `check` demands it already be a no-op. A file
committed with CRLF line endings changes on every line under `normalize`; read the diff before
committing it.
Exit 1 from `generate-id` does not mean nothing was written: it writes every id it can and reports the
rest, so read the diff before rerunning. On a clean tree `check` and `generate-id --dry-run` both exit 0
with zero findings; that is the baseline to restore before opening a PR.
## What `generate-id` does and does not fix
Writes:
- a `setting_id` into any instantiated preset that lacks one, or whose value does not match the formula;
- strips a `setting_id` from a base;
- deletes the misspelled key `settings_id`, moving its value to `setting_id` only on an instantiated BBL
preset that lacks one (everywhere else the old value is discarded and a fresh id minted);
- a `filament_id` into the id-less **root(s)** of an instantiated filament that resolves none;
- rewrites a **declared** `filament_id` that is not the mint of its own triple.
Refuses to write (reports only): a base62 collision between two products, an empty `filament_vendor` or
`filament_type`, a broken `inherits` chain, and roots of one filament resolving different
`(filament_vendor, filament_type)` pairs.
**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b (the label `docs/HLSD/filament_id.md` and the tool use), and it is the trap most likely to bite.
It happens when a branded filament inherits a generic for its settings:
```jsonc
{ "name": "Phrozen Aura PETG @Phrozen Arco 0.4 nozzle",
"inherits": "Generic PETG @System" } // resolves the library generic's id: wrong product
```
The preset resolves *an* id, so `generate-id` neither inserts nor rewrites one, and `check` fails with
`inherits filament_id "X" but its own triple "V/T/N" mints "Y"`.
Two fixes, in order of preference:
1. **Give the product a `@base` root** inheriting a material base (`fdm_filament_pet`,
`fdm_filament_pla`, …) with its own `filament_vendor` and `filament_type`. No `fdm_filament_*` base
carries a `filament_id`, so the filament now resolves none and `generate-id` mints it on the root.
This is the product-root shape ([the three-part shape](filament-profiles.md#the-three-part-shape)).
2. **Declare the tool-computed id on the preset itself.** Use the expected value `check` reports, or
compute it with the function below; this is not a manually chosen id. First make sure the preset
resolves the right `filament_vendor` and `filament_type`: with neither set, the triple resolves
through the generic parent and the branded product is minted under vendor `Generic`. If you need the
id before the file exists:
```bash
python3 -c "import sys; sys.path.insert(0,'scripts'); from orca_profile_tool import generate_filament_id as g; print(g('Polymaker','PLA','PolyLite PLA'))"
# -> OF5CgdDq
```
The quoting works unchanged in cmd and PowerShell; only swap `python3` for `py -3`.
`generate_preset_setting_id('<vendor folder>', '<type>', '<name>')` is the `setting_id` equivalent.
A vendor's tune of a generic that keeps the `Generic X` alias is not this case: inheriting the
generic's id is correct there, because the product really is the library generic
([filament-profiles.md](filament-profiles.md#the-two-most-common-contributions)).
## BBL's exception, precisely
The exception covers **`setting_id` assignment only**, keyed on the *folder* name `BBL`:
- The tool never mints or replaces a `setting_id` in `BBL/`: those are Bambu's own ids. A new
instantiated BBL preset with no `setting_id` therefore **cannot be fixed by the tool**, yet the
presence rule still applies to it: carry over Bambu's authoritative id by hand.
- BBL is not exempt from anything else: bases still get their `setting_id` stripped, ids must still be
unique across the tree, and BBL `filament_id`s are minted like everyone else's, as `OF…` ids.
## Ids other systems compose
No id from another system is the mint of a triple, so `check` rejects one used as a `filament_id` like
any other bad id: same error, same remedy, whoever wrote it. Three such spaces exist near the tree;
recognise them so you do not copy one into a profile:
- **Bambu's `GF…` catalog**: external and opaque, correlated to Orca's ids by the generated
`resources/printers/bambu_filament_ids.json`. `blacklist.json` and
`BBL/filament/filaments_color_codes.json` reference Bambu catalog ids by design. The rule is about
`filament_id` and nothing else: every BBL `setting_id` starts with `G`, and that is Bambu's own
preset id, not a leaked catalog id.
- **Qidi's `QD_…`**: composed at runtime by the printer's filament box
(`QD_<series>_<vendor>_<typeidx>`), not a preset id.
- **`P` + 7 hex digits, and `"null"`**: what the app gives a *user*-created filament.
## Tests
`python3 -m unittest discover -s scripts/tests -t scripts` (`py -3 -m …` on Windows) runs the tool's
unit tests. Note the `-t scripts` argument; without it the imports fail. CI runs them as the first,
non-`continue-on-error` step of the profile job; see [validation.md](validation.md#ci).
@@ -1,232 +0,0 @@
# Printer models and variants
Both live in `resources/profiles/<Vendor>/machine/`; models go in `machine_model_list`, variants and
shared bases in `machine_list`. Every one of them is registered. A bundle may nest further subfolders
under `machine/`, so recurse rather than globbing `machine/*.json`.
## `machine_model`: a record, not a config preset
The loader reads a fixed set of keys from a `machine_model` and stores only these (`version` and `url`
are recognised and discarded):
`name`, `model_id`, `nozzle_diameter`, `machine_tech`, `family`, `bed_model`, `bed_texture`,
`hotend_model`, `default_materials`, `not_support_bed_type`, `image_bed_type`,
`bottom_texture_end_name`, `bottom_texture_rect`, `bottom_texture_rect_longer`, `middle_texture_rect`,
`use_double_extruder_default_texture`.
**Everything else is silently dropped**, a printer config key such as `default_bed_type` or a
misspelling included, so a neighbour carrying a key is no evidence it does anything. Printer config options belong on the `machine` preset, never here. The loader drops a model
silently if its index entry has no name or its `nozzle_diameter` yields no sizes; `check` also requires
the file's own `name`.
```json
{
"type": "machine_model",
"name": "Phrozen Arco",
"machine_tech": "FFF",
"family": "Phrozen",
"model_id": "Phrozen Arco",
"nozzle_diameter": "0.4",
"bed_model": "Phrozen Arco_buildplate_model.stl",
"bed_texture": "Phrozen Arco_buildplate_texture.svg",
"hotend_model": "",
"default_materials": "Generic PLA @Phrozen Arco 0.4 nozzle"
}
```
| Field | Notes |
| --- | --- |
| identity | **the `name` of the `machine_model_list` entry**, which is what a variant's `printer_model` must equal. `check` forces it to equal the file's `name`, so they coincide. |
| `model_id` | a *separate* cloud/device printer type. Optional, and not required to be unique. Not the model's identity; changing it changes device matching. |
| `machine_tech` | only a value starting with `SL` means SLA; everything else is FFF. Write `FFF`; `FGF` behaves as FFF. |
| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (`0.4;0.2;0.6;0.8` puts the default first). This list is the authoritative set of legal `printer_variant` values. |
| `default_materials` | `;`-separated filament **preset names**, not `,`; case-sensitive (`@System`); order is ignored. Used to preselect filaments in the setup wizard *and* to install a printer's filaments on first run, so a dangling entry costs a real user a filament. Every name must exist (`check` fails on a name matching no filament file, here or in `default_filament_profile`), and every variant of the model needs at least one entry compatible with it (`validate_system`). |
| `family` | a wizard grouping label only; give every model one. |
### Assets
`bed_model`, `bed_texture` and `hotend_model` are paths relative to the **vendor folder** (named by the
vendor id). Convention: `<Model>_buildplate_model.stl` and `<Model>_buildplate_texture.svg`. An
empty string is the legal "none", and is the norm for `hotend_model`.
**Nothing checks that the files exist.** A missing `hotend_model` falls back to
`resources/profiles/hotend.stl`; a missing `bed_model` makes the bed render as a generic custom bed, and a missing
`bed_texture` renders no texture.
Verify by hand, in exact case.
Every model also has a `<Model>_cover.png` in the vendor folder; treat it as required, not optional.
240×240 is the cap `scripts/optimize_cover_images.py` enforces. A missing cover degrades to a placeholder in both the wizard and the sidebar.
## `machine`: the variant
```json
{
"type": "machine",
"name": "Phrozen Arco 0.4 nozzle",
"inherits": "fdm_machine_common",
"from": "system",
"setting_id": "lvaYKTUZr5C9jSwk",
"instantiation": "true",
"printer_model": "Phrozen Arco",
"printer_variant": "0.4",
"nozzle_diameter": ["0.4"],
"default_print_profile": "0.20mm Standard @Phrozen Arco 0.4 nozzle",
"default_filament_profile": ["Generic PLA @Phrozen Arco 0.4 nozzle"],
"printable_area": ["0x0", "300x0", "300x300", "0x300"],
"printable_height": "300"
}
```
Minimum viable key set: `type`, `name`, `from`, `instantiation`, `setting_id`, `inherits`,
`printer_model`, `printer_variant`, `nozzle_diameter`, `printable_area`, `printable_height`,
`default_print_profile`. `default_filament_profile` is optional; when written it
is an array (`["Generic PLA @System"]`), while the model's `default_materials` is a `;`-separated
string. Unlike a `machine_model`, a `machine` **is** a config preset, so a key belonging to another
preset type is a reported error and is removed; a misspelled key is still dropped silently.
### `printer_model` and `printer_variant`
1. `printer_model` is non-empty and names a model of this bundle exactly.
2. `printer_variant` is non-empty and an exact token of that model's `;`-separated `nozzle_diameter`
list.
3. For instantiated presets, when validating: split `printer_variant` on `+`; each token must start
with a number (a trailing non-numeric suffix such as `HF` is ignored), and the resulting **set** must
equal the set of `nozzle_diameter` values.
Rules 1 and 2 are loader-enforced: failing either discards the whole bundle. Rule 3 only raises a
validation error: the preset still loads, but the validator exits non-zero.
`nozzle_diameter` lists one entry **per extruder**; `printer_variant` lists the **distinct** diameters
joined with `+`: `["0.4","0.4","0.6","0.6"]` against `"0.4+0.6"` passes because the comparison is on
sets.
Write `printer_variant` as a bare diameter matching the model's list, with no unit. The conventional
values are `0.2`, `0.25`, `0.4`, `0.5`, `0.6`, `0.8` and `1.0`; a suffixed form (`0.4HF`, `0.4HS`) or
the `+` form is legal under rule 3. A `printer_variant` is **not** required to be unique within a
model: an IDEX model's normal, `COPY MODE` and `MIRROR MODE` presets can all be `0.4`.
The converse is **unchecked**: a nozzle size in the model's list with no matching variant is offered in
the wizard and resolves to nothing. A variant whose `printer_model` names a sibling model by mistake
leaves its own model's size in exactly that state.
### Other keys
- `default_print_profile` is a **scalar**, matched by exact preset name; not a `;` list. The named
process must be compatible with this printer through its resolved list or condition.
`validate_slice` attempts to select it and rejects generic Default fallbacks, but compatibility
updates can choose another compatible preset, and `check` does not resolve the name. Check the exact
default reference yourself.
- `default_filament_profile` is an **array**, one name per element. Entry 0 is the filament preselected
when the printer is chosen; entry *i* is the preferred replacement when filament *i* is incompatible,
and any listed name outranks an unlisted one. The validator checks every entry. The list of a
printer's filaments is the model's `default_materials`: a new filament goes into `default_materials`;
put it first in `default_filament_profile` only if it should become the preselected one.
- `printable_area` is an array of `"XxY"` strings: four points for a rectangle; a delta or other circular bed
is a polygon with one point per segment.
- A non-BBL printer shows the plate selector only with `support_multi_bed_types` `"1"`; otherwise it
uses its `default_bed_type` (a plate name such as `"Textured PEI Plate"`; High Temp Plate when unset).
Filaments still set every plate
([twelve keys](filament-profiles.md#bed-temperature-is-twelve-keys-not-one)).
- `gcode_flavor` is usually set once in the base; the common values are `klipper`, `marlin`, `marlin2`
and `reprapfirmware`.
- `printer_settings_id` does nothing in a preset file: the app replaces it with the selected preset's
name before slicing. Omit it, and do not copy it when cloning a bundle.
- `min_layer_height` / `max_layer_height` are **machine** keys (one per extruder), never process keys.
## Bases
The conventional machine root is a base named `fdm_machine_common`, with `fdm_klipper_common` on top
of it for Klipper printers. Which values a hardware
family or model base holds, and when adding one pays off, is in [shared-bases.md](shared-bases.md).
**There is no leading-underscore convention for bases.**
## Adding a printer to an existing bundle
1. Choose the names first: model, variant(s), process(es); everything else references them
([naming.md](naming.md)).
2. Add the model (`machine_model_list`) and one `machine` variant per nozzle; the minimum key sets are
above. Bed assets and `<Model>_cover.png` go directly in `<Vendor>/`.
3. Add at least one process per variant naming it in `compatible_printers`
([process-profiles.md](process-profiles.md#adding-a-quality-tier-or-a-nozzles-processes)).
4. Add the variants to the filaments they should offer, and set the model's `default_materials` so every
variant has a compatible entry.
5. Register everything (`update-index`), bump the version, run the id tool, validate: the
[authoring workflow](../SKILL.md#creating-or-modifying-a-profile).
## Adding a nozzle variant
1. Extend the model's `nozzle_diameter` (`"0.4"` → `"0.4;0.6"`).
2. Add the variant preset. Either inherit the shared base (the usual choice), or the 0.4 sibling
(a smaller diff, but the sibling's edits now reach this file too). Follow the bundle.
3. Override what actually changes with the nozzle: `nozzle_diameter`, `printer_variant`,
`default_print_profile`, `default_filament_profile`, `min_layer_height` / `max_layer_height`, and
retraction if the vendor tunes it.
4. Add at least one process for the new nozzle (see [process-profiles.md](process-profiles.md)), and
extend the filaments' `compatible_printers` so at least one `default_materials` entry covers the new
variant.
5. Register both, bump the version, run the id tool, validate.
## Multi-extruder, IDEX and tool changers
Per-extruder vectors hold one value per extruder (`len(nozzle_diameter)`), and a wrong length raises no
error: a short vector acts as padded with its **first** value, not the last (`extruder_offset`
`["0x0","50x0"]` on a 4-extruder machine reads as `0x0, 50x0, 0x0, 0x0`), and entries beyond the
extruder count are never read.
Note the two sizing families. The plain per-extruder keys (`printer_extruder_options`: `extruder_type`,
`nozzle_diameter`, `default_nozzle_volume_type`, `extruder_printable_height`, `min_layer_height`,
`max_layer_height`, …, given in full with the [key sets](extruder-variants.md#the-four-key-sets), plus
`extruder_offset` and `extruder_colour`) hold one value per extruder. The variant sets
(`retraction_length`, `z_hop`, `wipe`, `nozzle_type`, the `machine_max_*` limits at stride 2;
[the full lists](extruder-variants.md#the-four-key-sets)) are sized to the variant length:
`len(printer_extruder_variant)`, or one variant per extruder when the resolved preset writes no layout,
since `extruder_variant_list` defaults to one `Direct Drive Standard` per extruder
([widths](extruder-variants.md#widths)). `extruders_count` is a printer-tab field, not a preset key; the
loader drops it.
- Give **one entry per extruder** for ordinary per-extruder vectors such as `extruder_offset`,
`extruder_colour`, `min_layer_height` and `max_layer_height`. Size the variant sets to the variant
length × stride, one value per variant (per extruder when there is no layout; a (normal, silent)
pair for the `machine_max_*` limits), even where the values are the same; the loader keeps only the first value of a list-less printer's variant arrays, so declare
the layout when the extruders differ. A single `["0x0"]` `extruder_offset` on a dual or
multi-extruder machine pads every extruder to the same offset, so the offset never applies.
- Overriding `nozzle_diameter` to a different count without restating every per-extruder vector is the
other half of the trap: a 4-extruder preset on a 5-extruder base inherits 5-entry vectors against 4
extruders.
The reverse is silent too: a base is stored resized to its own `printer_extruder_variant` (one variant
when it writes none, whatever its extruder count), so a wider variant array on it reaches the children
as its first value padded ([composition](extruder-variants.md#padding-truncation-and-composition)).
`check` does not judge the base on its own; where its extruders need different values, declare the
layout on the base.
Structure to copy: `Custom/machine/fdm_toolchanger_common.json` + `Custom/machine/MyToolChanger 0.4
nozzle.json` (a minimal variant on a base that gives the per-extruder vectors five entries), and
`Ratrig/machine/RatRig V-Core 4 IDEX 300 0.4 nozzle.json` for IDEX. Take the structure from them and
the widths from the [sizing equation](extruder-variants.md#sizing-equation). Both are list-less, so
every variant array holds one value per extruder and the loader keeps only the first: fine while every
extruder shares the same retraction and limits. Add the extruder-variant layout
(`extruder_variant_list`, `printer_extruder_variant` / `printer_extruder_id`,
`default_nozzle_volume_type`) when the hardware has swappable nozzle volume types or mixed extruder
types, or as soon as one extruder needs its own value in a variant key; how to author it, and the
matching process and filament variants, is in [extruder-variants.md](extruder-variants.md).
(`nozzle_volume_type` itself is not a machine-preset key.)
## Custom G-code
The keys are `machine_start_gcode`, `machine_end_gcode`, `change_filament_gcode`,
`machine_pause_gcode`, `before_layer_change_gcode` and `layer_change_gcode`. Each is one string with
embedded `\n`. Never split G-code into a JSON array of lines: the loader joins array elements with `,`
into a single line (a one-element array is equivalent to the string): a two-element
`machine_start_gcode` becomes one line, `PRINT_START …,SET_PRESSURE_ADVANCE ADVANCE=0.046`.
Conditionals are `{if …}` / `{elsif …}` / `{else}` / `{endif}`.
Placeholder errors only surface when the G-code is actually expanded, which means `validate_slice`:
```bash
./scripts/check_profile.sh --vendor "<Vendor>" validate_slice
# Windows: scripts\check_profile.bat -Vendor "<Vendor>" validate_slice
```
What the sweep covers is in [validation.md](validation.md#validate_slice); a printer whose output has no
`CP TOOLCHANGE START` fails it, because its `change_filament_gcode` never expanded.
@@ -1,94 +0,0 @@
# Preset naming
A preset's `name` is its identity, not decoration. The index registers it; `inherits`,
`compatible_printers`, `printer_model` and the `default_*` keys reference it by the exact,
case-sensitive string; `renamed_from` migrates it; `setting_id` and `filament_id` hash it
([ids.md](ids.md)). Treat a name change as an identity change that needs
[migration](vendor-bundle.md#renamed_from), not a relabel. Which part of each name the loader acts on
is the table in [SKILL.md](../SKILL.md#names); this page holds the conventions.
## `machine_model`
`<Model>`, vendor-prefixed: `Bambu Lab X1 Carbon`, `Creality K1`, `Prusa CORE One`. Each variant's
`printer_model` names it verbatim (a mismatch discards the bundle), and its index entry equals the
file's `name`. It is also the stem of `<Model>_cover.png` and, by convention, of the bed assets
(`<Model>_buildplate_model.stl`).
## `machine` (variant)
`<Model> <nozzle> nozzle` is near-universal (`Bambu Lab X1 Carbon 0.4 nozzle`). Casing varies
(`nozzle` / `Nozzle`): match the bundle, not this page. A variant that is not nozzle-specific (a
special toolhead, a multi-material build, IDEX copy and mirror modes such as
`<Model> COPY MODE (0.4 nozzle)`) may drop or reshape the suffix; it is still an exact reference. `printer_variant`
holds the nozzle token: `0.4`, a suffixed `0.4HF`, or `0.4+0.6` for mixed nozzles
([rules](machine-profiles.md#printer_model-and-printer_variant)).
## `process`
`<layer height>mm <quality> @<target>`. The quality word stays before `@` and the printer target after
it: a printer model in the quality position leaves the tier undescribed. The `@<target>` is a label,
and need not equal any variant name; compatibility comes from `compatible_printers` or the
condition. The quality ladder and per-nozzle labels are in
[process-profiles.md](process-profiles.md#naming).
## `filament`
`<Product> @<target>`. The product half, up to the first `@` and right-trimmed, is the **alias**:
shadowing matches on it and `filament_id` hashes it. The target half is a label, except for the
reserved forms:
- `@base`: a non-instantiated product root. Convention only; a base is really
`instantiation: "false"` without `setting_id`
([the three-part shape](filament-profiles.md#the-three-part-shape)).
- `@System`: the OrcaFilamentLibrary selectable shim, and the convention for an all-printer product
(`<Product> @System`, empty `compatible_printers`). Not enforced, so a deviation is worth a review
comment. The literal `Generic <mat> @System` is also load-bearing for project recovery
([alias shadowing](filament-profiles.md#alias-shadowing)).
- Printer tunes. BBL's shape is the reference: `@<Vendor>` (vendor-wide), `@<Vendor> <Model>` (one
model), `@<Vendor> <Model> <nozzle> nozzle` (one variant). Other vendors differ: a bare model
(`QIDI ABS-GF@Q2-Series`), a printer serial, Creality's `@<Model>-all`. Judge specificity from
`compatible_printers`, never from the name
([one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)).
- No colour in the product name: `<Product> <Colour>` presets are not authored; colour is chosen at
runtime ([colour](filament-profiles.md#colour-is-a-runtime-property)).
## Bases
| Type | Base names |
| --- | --- |
| `machine` | `fdm_machine_common`, `fdm_<vendor>_common`, `fdm_klipper_common`, or an established machine-family base |
| `process` | `fdm_process_*`: shared roots and per-layer-height or per-nozzle bases such as `fdm_process_single_0.20` or `fdm_process_<vendor>_<lh>_nozzle_<n>` |
| `filament` | `fdm_filament_*` material roots, `<Product> @base` product roots |
There is no leading-underscore convention. Base names repeat across bundles by design:
every bundle may have its own `fdm_process_common`, and a product root such as `Fiberon PA6-CF @base`
can exist in both the library and a vendor. Investigate a newly authored base that kept an unrelated
selectable preset's name from a copy.
## Uniqueness
- Type + name is unique within a bundle, indexed or not; `check` enforces it.
- `machine_model` names are unique across the whole tree; `check` enforces it even with `--vendor`.
Printer-type lookup matches `printer_model` against every vendor's models and takes the first, so a
duplicate makes it depend on vendor order.
- At load, two selectable presets with one name discard the bundle, and a duplicate across vendors
is a validator error. Two bases with one name, or a base and a selectable preset, load silently and
the first in the index wins; an unindexed twin is therefore one `sub_path` edit away from becoming
the parent every child resolves to.
## Filenames and paths
The loader keys off `name`, and a filename that disagrees usually still loads, but keep the filename
equal to the `name` and to the index `sub_path`. Match the exact case of every `sub_path` and asset
filename: Linux filesystems distinguish case even when a macOS or Windows checkout does not, and
preset-name references are case-sensitive on every platform. Avoid Windows-invalid characters
(`< > : " | ? *`), reserved device names such as `CON` and `NUL` (with any extension), and trailing
spaces or dots in a path component; a space right before `.json` is not a trailing space.
## Checking names
Check the `name` of every newly added profile, and every intentional rename, against its type and role
(model, selectable preset or base). `check` does not enforce the shapes on this page: inspect the
added or renamed presets in the diff, use neighbouring names as context, and follow the bundle's
established style where the conventions allow variation. Preserve shipped names during ordinary
tuning; renaming a shipped selectable preset needs `renamed_from`.
@@ -1,155 +0,0 @@
# Process profiles
Processes live in `resources/profiles/<Vendor>/process/`, selectable leaves and shared bases alike, and
every one of them is registered in `process_list`. There are no global processes shared across vendors.
## Naming
`"<layer height>mm <quality> @<target>"` is near-universal, so match it: the quality word before `@`,
the printer label after it ([naming.md](naming.md#process)).
Follow the bundle's existing quality vocabulary. BBL's common ladder relates the quality word to the
layer height / nozzle ratio; it is a naming convention, not a loader constraint:
| Quality | Ratio | 0.2 nozzle | 0.4 | 0.6 | 0.8 |
| --- | --- | --- | --- | --- | --- |
| Extra Fine | 0.2× | — | 0.08 | — | — |
| Fine | 0.3× | 0.06 | 0.12 | 0.18 | 0.24 |
| Optimal | 0.4× | 0.08 | 0.16 | 0.24 | 0.32 |
| Standard | 0.5× | 0.10 | 0.20 | 0.30 | 0.40 |
| Draft | 0.6× | 0.12 | 0.24 | 0.36 | 0.48 |
| Extra Draft | 0.7× | 0.14 | 0.28 | 0.42 | 0.56 |
This is the `fdm_process_single_<lh>_nozzle_<n>` ladder; 0.4 is commonly the unsuffixed nozzle default.
Newer BBL printers add High Quality, Balanced Quality and Strength tiers. Match neighbouring names rather
than renaming shipped tiers to fit the table. On a model with several nozzles, processes for the other
nozzles usually carry the nozzle in the label (`0.30mm Standard @BBL X1C 0.6 nozzle`); follow the bundle.
The `@target` is a human label, not a reference: it need not equal any printer variant name.
Compatibility comes from the resolved list or condition, not this label.
## Shape
A selectable leaf has `type`, `setting_id`, `name` and `instantiation`, normally `inherits` and
`from`, plus compatibility; its slicing keys, `layer_height` included, normally come from its bases. A
base has `type`, `name`, `instantiation`, `from`, and **no** `setting_id`.
**Target shape: a 7-key leaf.** `OrcaArena` is the cleanest model:
`fdm_process_common` → `fdm_process_arena_common` → `fdm_process_arena_<lh>_nozzle_<n>` → leaf, where the
leaf carries only `type`, `name`, `inherits`, `from`, `setting_id`, `instantiation` and
`compatible_printers`, and the per-nozzle base holds the layer height and all eight line widths.
[shared-bases.md](shared-bases.md#levels) says which level each process setting belongs to.
BBL's *layering* is a model too (every leaf inherits a base, names its printers directly and holds no
layer height of its own), but not its content: its leaves carry multi-variant `print_extruder_variant`
arrays that no single-variant vendor needs ([extruder-variants.md](extruder-variants.md#process)).
A bundle has its own `fdm_process_common` as the inherits-less root, since a process inherits only
inside its bundle; starting a new bundle's from another vendor's copy is fine.
Beware leaf-inherits-leaf: a bundle may chain selectable processes several levels deep, so editing one
silently changes others. Check a leaf's children before editing it.
## Compatibility
A leaf sets `compatible_printers` directly, inherits it from a base, or falls through to
`compatible_printers_condition`. After resolving `inherits`, **every selectable process has one or the
other**: that is the invariant to review against. Unlike filaments, inheriting `compatible_printers` is
legitimate for a process, and no check enforces its presence.
- A non-empty `compatible_printers` makes `compatible_printers_condition` **dead**. Use one or the
other.
- A condition that fails to parse means *compatible with everything*: a warning, not an error. A typo
widens compatibility instead of narrowing it.
- A regex in a condition must match the **whole** string, so wrap the keyword in `.*`; `.` also spans
the newlines inside `printer_notes`.
- A `printer_notes` keyword that prefixes another model's keyword matches both. Guard it with a
character class after the keyword, and combine terms with `and`:
```
printer_notes=~/.*PRINTER_MODEL_COREONE[^_a-zA-Z0-9].*/ and nozzle_diameter[0]==0.4 and printer_notes=~/.*HF_NOZZLE.*/
```
The `[^_a-zA-Z0-9]` exists because `PRINTER_MODEL_COREONE_L` also contains `PRINTER_MODEL_COREONE`.
A leaf listing a whole model family is where a newly added printer is usually forgotten.
## Values to review per nozzle
| Key group | Review |
| --- | --- |
| `line_width` and per-region widths | resolved widths suit the nozzle and layer height |
| `layer_height`, `initial_layer_print_height` | within the printer's `min_layer_height` / `max_layer_height` |
| print speeds | consistent with flow limits and hardware tuning |
| shell layers, wall loops, accelerations, support Z distances | preserve the intended thickness, motion and support behaviour |
**A common starting pattern is line width = nozzle + 0.02 mm**: 0.22 / 0.42 / 0.62 / 0.82 / 1.02. In
that pattern, at 0.4, `inner_wall_line_width`, `sparse_infill_line_width`, `skin_infill_line_width` and
`skeleton_infill_line_width` widen to 0.45 and `initial_layer_line_width` to 0.5; at 0.2,
`initial_layer_line_width` widens to 0.25. Also derived, and easily missed:
`ironing_inset = line_width / 2` (0.11 / 0.21 / 0.31 / 0.41). These are examples, not required values;
preserve intentional vendor tuning and percentage or automatic widths, and validate their resolved
values.
`min_layer_height` and `max_layer_height` are machine keys; no process file sets them.
### Slicing limits
Slicing rejects a process that breaks one of these (the message in italics):
1. `initial_layer_print_height` ≤ the smallest `nozzle_diameter` (with a raft, the nozzle of the raft's
first-layer extruder).
2. `layer_height` ≤ the smallest `nozzle_diameter`: *"Layer height cannot exceed nozzle diameter."*
3. `line_width` and the seven per-region widths (inner and outer wall, sparse infill, internal solid
infill, top surface, skin, skeleton) > `layer_height`: *"Line width too small"*.
`support_line_width` is checked only when the object has support or a raft;
`initial_layer_line_width` is never checked. A width that resolves to 0 (automatic) is skipped.
4. Every width ≤ 5 × the largest `nozzle_diameter`: *"Line width too large"*.
Two further rules cover `bridge_line_width`: it must not exceed the nozzle diameter, and must exceed
`layer_height` unless `thick_bridges` and `thick_internal_bridges` are both on. The slice sweep starts
from printer defaults rather than enumerating every process: **a new non-default process gets no
dedicated slice coverage in CI.**
## What CI checks on a process
Structure, not content: `process_list` name consistency **and** index coverage the other way, two files
claiming one process name, the `extruder_clearance_radius` / `extruder_clearance_max_radius` conflict
pair, duplicate JSON keys, a file `normalize` would rewrite, the five `setting_id` rules (present on
selectable presets, absent from bases, equal to the formula outside `BBL/`, unique across the tree, and
no misspelled key `settings_id`), and the variant arrays: every array of `print_options_with_variant`
exactly `variant length × stride` wide in each selectable process that writes it, and a written
`print_extruder_id` one entry per variant
([variant arrays](validation.md#variant-arrays)). `compatible_printers` presence is checked for
**filaments only**.
The loader derives a missing `setting_id` on the fly, so the validator accepts a process without one;
only `orca_profile_tool.py check` catches it.
Running the validator alone gives a false all-clear.
## Silent failures specific to processes
- **Unknown or misspelled keys are discarded with no error and no warning**, both plain typos
(`inital_layer_height`, `tree_support_bramch_diameter_angle`, `sparse_infill_patter`) and keys
copied from other slicers that Orca never defined.
- Keys on the tool's obsolete list (`adaptive_layer_height`, `overhang_totally_speed`, …) are rejected
by `check`'s normalization pass across preset types; `normalize` removes them. The additional per-key
obsolete warnings read `filament/` only.
- A dangling `compatible_printers` inside an `instantiation: "false"` base is reported only through a
selectable child that inherits it unchanged; it goes unreported when every child overrides the list,
or when the base has no instantiated children.
- Nothing flags an orphan base that nothing inherits, usually the leftover of a half-finished nozzle
addition.
## Adding a quality tier or a nozzle's processes
1. Choose the layer height and quality label using the bundle's existing ladder.
2. If the bundle has per-nozzle bases, add one (`fdm_process_<vendor>_<lh>_nozzle_<n>`) with the layer
height, nozzle-appropriate line widths, `initial_layer_print_height` and `ironing_inset`.
3. Add the leaf: 7 keys, `compatible_printers` naming the exact printer variant(s).
4. Register both in `process_list`, parent first, bump the version, run the id tool and validate: the
[authoring workflow](../SKILL.md#creating-or-modifying-a-profile).
5. Slice this process explicitly with its intended printer
([on a copy of the tree](validation.md#checking-a-copy-of-the-tree)); the sweep gives non-default
tiers no dedicated coverage. If it is a printer's `default_print_profile`, verify the exact name and
resolved compatibility too: the sweep may fall back or select another compatible process.
@@ -1,277 +0,0 @@
# Reviewing a profile change
Run `./scripts/check_profile.sh` on the applied diff first ([validation.md](validation.md) says what CI
runs), then work through the items below: delivery, identity and backward compatibility first, then the
affected preset types. The table lists the gaps CI cannot see, so only a reviewer catches them.
| Not checked by CI | Consequence |
| --- | --- |
| The `version` bump | The change never reaches an upgrading user; an absent `version` hides the vendor from the setup wizard |
| A misspelled setting key | The setting silently has no effect |
| A filename Windows cannot check out, or one that differs from its `sub_path` only in case | Works on the author's machine, breaks the bundle on another platform |
| `bed_model` / `bed_texture` / `hotend_model` / cover pointing at a missing asset | A missing bed model renders a generic custom bed and a missing texture renders none, the hotend falls back to the generic model, the cover shows a placeholder |
| A nozzle size in a model's list with no matching variant | The size is offered and resolves to nothing |
| A non-default process | `validate_slice` gives non-default quality tiers no dedicated coverage |
| Whether the intended default survived compatibility selection | The sweep can select a different compatible preset |
| A dangling `compatible_printers` inside a base whose children all override it (or that has no instantiated children) | The reference check walks resolved selectable presets, so it reports a base's list only through a child that inherits it unchanged (a bad `inherits` in a base *is* caught) |
| A base nothing inherits | Dead weight, usually the leftover of an unfinished nozzle addition |
| A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name |
| A preset differentiated only by colour, or an all-printer library preset without `@System` | Per-colour presets split one product across several ids and the selector fills with near-duplicates; CI stays green |
| Plate temperatures for plates the printer has | The user's plate reads an unset or inherited temperature |
| Per-extruder vector length on a multi-extruder printer | Silently padded (with the **first** value) or truncated |
| A new variant appended instead of inserted at its variant index, a variant array widened on a base with a shorter list, or a variant a filament/process lacks | Values shift onto the wrong extruder, are cut before any child inherits them, or resolve to the first variant: High Flow silently gets Standard values |
| A name that ignores its type's convention: a printer model in a `process` quality position, or an unrelated target label or base name left in a copied preset | The selector misrepresents the preset's quality or intended printer |
| Values: temperatures, speeds, widths, pressure advance | A wrong value prints wrong while CI stays green |
## 1. Was the vendor `version` bumped?
For **every** bundle whose folder the diff touches, `resources/profiles/<Vendor>.json` must have its
`version` incremented: last component, carrying `.99` into the third component. A library change means
bumping `OrcaFilamentLibrary.json`.
*Why:* nothing in CI checks it, and the app reinstalls a bundled profile set only when its version is
newer than the installed one: without a bump the change reaches neither an upgrading user nor the
author's own running app. Without any `version` the vendor vanishes from the setup wizard, and neither
`check` nor the validator reports it.
## 2. Was the index rebuilt, and does the diff contain only this change?
`check` fails on an unregistered file, on an index `update-index` would reorder, and on a file
`normalize` would rewrite, so a PR that skipped them arrives red and you do not have to spot the
omission yourself. Three things are still yours:
- **The index diff belongs to this change.** `update-index` rewrites whole `*_list` sections. If the
bundle had drifted, the author's PR now carries someone else's reordering; ask for it in a separate
commit rather than reviewing it inline.
- **A deleted selectable preset needs a successor** as in item 4. `update-index` removes its
registration; `validate_custom` detects the break only for names covered by released fixtures.
- **`normalize` edits content, not just layout.** It drops `version` and `is_custom_defined` from preset
files, removes obsolete keys, deletes six print-speed keys from filament profiles, and resolves
`extruder_clearance_radius` against `extruder_clearance_max_radius` by keeping the larger
([what normalize changes](validation.md#normalize-and-update-index-are-part-of-the-check)). Check that
the keys it removed were meant to go.
Index order is dependency order, not alphabetical: parents and include templates before the presets
that use them, then, in name order, the entries that neither depend on nor are depended on by another
entry of their own list (such as a leaf filament whose only parent is in the library), and any entry on
a dependency cycle. Judge a hand-placed entry only by
`update-index --dry-run`: if it reports nothing to rebuild, the position is not a finding.
*Why:* the index is the loader's only entry point. Out-of-order entries fail with `can not find inherits`
and take the whole vendor bundle down; an unindexed file gets reviewed, merged and never loads.
## 3. Are ids generated, not written?
No hand-typed or copied `setting_id` / `filament_id`. Instantiated presets have a `setting_id`; bases do
not. `check` enforces all of that; what it cannot tell you is whether the identity *should* have moved.
A rewritten or removed `filament_id` means a product's identity moved (a renamed alias, or an edited
`filament_vendor` / `filament_type`), and the old id is not forwarded anywhere. Confirm that was
intended, and that a new id is not a rename in disguise.
*Why:* a duplicate `filament_id` on one printer makes AMS spool matching a coin toss; a copied
`setting_id` breaks preset identity. See [ids.md](ids.md).
## 4. Does anything disappear for existing users?
A rename, a deletion, or a flip of `"instantiation": "true"` → `"false"` on a shipped selectable preset
removes the name from the preset collection. It needs `renamed_from` on a selectable successor
([renamed_from](vendor-bundle.md#renamed_from)), and only one preset may claim a given old name. The
claimed old name must **not** still be a live preset; the redirect is inert if it is.
*Why:* user presets inheriting it die with `can not find parent <name> for config <file>!`; 3MF-embedded
presets are dropped with no error at all. Commit `33923464ae` reverted exactly this for Cubicon;
`6943b6ddc3` redid it correctly with `renamed_from`. CI's `validate_custom` catches the shipped-name
case, but not an inert `renamed_from`.
## 5. Is `compatible_printers` right?
Exact printer **variant** names, non-empty on every instantiated filament outside OrcaFilamentLibrary,
and written in the preset's own file ([SKILL.md rule 9](../SKILL.md#rules); the resolved-vs-own-key trap
is in [filament-profiles.md](filament-profiles.md#compatible_printers)). A `machine_model` name instead
of a variant name is the usual mistake: `check` passes it, the validator reports
`references unknown compatible_printers`. Watch for a nozzle-specific tune that inherited or copied the
base's full printer list, and for two presets of one product with overlapping lists: duplicate combobox
entries and an ambiguous AMS match.
Two presets of one product (`filament_id`) must not share a variant. Resolve it by specificity: move the
variant to the most specific preset and remove it from the more general ones, which is preferred over
deleting a profile. Then repoint the machine's `default_filament_profile` and the model's
`default_materials` at the profile that now covers it. See
[one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product).
*Why:* real shipped bugs twice (`b7b3418baf` filaments "showing up everywhere", `ff83aa41ef` duplicate
Flashforge entries). The Python `check` passes on an overlap; only the full `check_profile.sh`
(`validate_system`) reports `Ambiguous AMS filament match`.
## 6. One product, one all-printer preset; colour is not a preset
No presets that differ only by colour: `filament_id` identifies a product, and the colour comes from the
spool at runtime. An all-printer library product is a `<Product> @System` shim with an empty
`compatible_printers` ([colour](filament-profiles.md#colour-is-a-runtime-property)).
*Why:* per-colour presets pass every check, so this is a review call.
## 7. Model ↔ variant ↔ process consistency
- A new nozzle size → the model's `nozzle_diameter` list extended, a variant with a matching
`printer_variant`, and at least one process listing that variant. Every size in the model's list has a
variant (unchecked).
- `default_print_profile` is one exact name (not a `;` list), and that process's resolved compatibility
list or condition includes this printer.
- `default_filament_profile` is an array of names that exist.
- Each variant of the model has at least one compatible entry in the model's `default_materials`.
*Why:* an unlisted `printer_variant` is a hard bundle-load failure. Default process selection is weaker:
the sweep attempts the named default, then updates compatibility and rejects generic Default fallbacks,
so another compatible process can conceal a bad reference. Inspect it even after a pass.
## 8. Types and spellings
Every value a string or an array of strings; `filament_type` an array; `instantiation` the string
`"true"` / `"false"`; custom G-code one string, never an array of lines ([SKILL.md rule 5](../SKILL.md#rules)).
Check index metadata and model `nozzle_diameter` especially: wrong types there can abort loading for
**every** vendor.
The part only a reviewer can do: check new setting keys against `src/libslic3r/PrintConfig.cpp`. A
misspelled key is silently discarded ([rule 6](../SKILL.md#rules)), the single most common way a profile
edit does nothing while CI stays green.
## 9. Blast radius of a base edit
A change to `fdm_*_common.json` or any other base reaches every child at once. Ask which presets it
touches: several reverts in this repo are exactly this (`41d1b0d3c8`, `dc491166a8`). Also check whether
the edited leaf has children of its own: a bundle may chain leaf-inherits-leaf several levels deep. A
newly added base that nothing inherits is dead weight, and a dangling
`compatible_printers` inside a base is reported only through a child that inherits it unchanged. A diff
that only moves values between presets and bases must leave every selectable preset loading what it
loaded: ask for the `compare` result ([shared bases](shared-bases.md#nothing-loads-differently)), and
check each new base against the [balance rules](shared-bases.md#balance).
## 10. Do the numbers make sense for the nozzle and material?
Check resolved widths and layer heights against the nozzle, temperatures against the material (PLA
values under an ASA name print wrong), and flow limits / pressure advance against the actual hardware
and material. The patterns in [process-profiles.md](process-profiles.md#values-to-review-per-nozzle)
are examples, not mandatory values; [filament-profiles.md](filament-profiles.md#tuning-per-nozzle-and-per-variant)
explains what to revisit for a nozzle change. A cloned preset's unchanged volumetric speed needs
particular scrutiny.
Settings tuned for real hardware cannot be verified by reading the diff. Say so rather than approving
numbers nobody measured.
## 11. Plate temperatures
A filament sets the plate temperature for every plate the printer plausibly has, as its siblings do;
`textured_cool_plate_temp` is the one most often forgotten
([twelve keys](filament-profiles.md#bed-temperature-is-twelve-keys-not-one)).
## 12. Asset references (not checked anywhere)
`bed_model`, `bed_texture`, `hotend_model` and `<Model>_cover.png` exist under
`resources/profiles/<vendor folder>/`, in exact case. Nothing checks them.
## 13. `default_materials` and `default_filament_profile` (checked by CI)
`check` fails on a `default_materials` / `default_filament_profile` name that matches no filament file,
and `validate_system` on a variant with no compatible system filament in `default_materials`, so a
dangling entry no longer reaches review. When compatibility moves between profiles of a product, the
machine's `default_filament_profile` and the model's `default_materials` must be repointed at the most
specific profile that still covers the variant, dropping generic entries that no longer apply; the same
[specificity rule](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product) applies
when adding or fixing defaults. Scope the run while working on one vendor:
```bash
python3 scripts/orca_profile_tool.py check --vendor "<Vendor>" # py -3 on Windows
```
## 14. Per-extruder vectors (not checked) and variant arrays (widths and layout checked)
One entry per extruder for the plain per-extruder vectors; the variant sets are sized to the variant
length, `len(printer_extruder_variant)` or one per extruder when the resolved preset has no layout (a
per-extruder difference in those keys still needs the layout, since the loader keeps only the
first value of a list-less printer's arrays). A wrong length is silently padded, repeating the **first**
value, not the last, or truncated. The two sizing families and the worked cases are in
[machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers).
`check` reports a variant array of any type that is not exactly the `variant length × stride` width
of the selectable preset that writes it, one value included, as an error whatever the values;
`check --strict` also reports one that reaches a selectable preset at another width. A base is not
judged on its own. On a machine it
also reports layout keys that disagree: `printer_extruder_variant` / `printer_extruder_id` not the
flattening of `extruder_variant_list`, a variant without its extruder's `extruder_type` prefix, a
`default_nozzle_volume_type` the extruder does not list, a list of several variants without the pair, a
pair without the list that puts several variants on one extruder. On a machine or a process, an id
array, written or inherited, whose length differs from its variant list is an error; the id and
layout rules judge the composed preset without `--strict`. Two layout findings are warnings: a
pair without the list that `single_extruder_multi_material` off would replace at load, and a process
without `print_extruder_id` whose variants repeat. The full rules are in
[variant arrays](validation.md#variant-arrays). `check_variant_names` separately holds every variant
string, `extruder_type`, `nozzle_volume_type` and `default_nozzle_volume_type` to the engine's enums
in every bundle, BBL included, failing on a dead variant, a legacy spelling and a variant list that
names one variant twice ([variant names](validation.md#variant-names)).
On a multi-variant printer, still check by hand:
- that a new variant's values sit at its variant index (machine limits as a (normal, silent) pair at
`2 × index`) in every file of the chain that restates the key, `include` templates included;
- that each process lists every (extruder id, variant) pair its printers can select;
- that no array was widened on a base with a shorter variant list: the loader cuts it before the
children inherit it ([composition](extruder-variants.md#padding-truncation-and-composition)), and
`check`, even `--strict`, composes without that cut, so it passes;
- that only keys in [the four sets](extruder-variants.md#the-four-key-sets) carry per-variant values;
- that the High Flow variants carry measured values rather than copies
([checking and testing](extruder-variants.md#checking-and-testing)).
## 15. Non-default processes get no slice coverage
`validate_slice` starts from printer defaults; it does not enumerate every process. Slice a new or
changed non-default tier explicitly with its intended printer
([on a copy of the tree](validation.md#checking-a-copy-of-the-tree)).
## 16. Do the preset names follow the conventions?
Check **every newly added profile and intentional name change**, including models and bases, against
[the naming conventions](naming.md#checking-names): no printer model in a process quality position, no
target label or base name left over from a copied preset, the bundle's established style. Preserve
shipped names during ordinary tuning; renaming a shipped selectable preset requires the migration in
item 4.
*Why:* CI checks name uniqueness, but does not enforce the naming conventions. Catch naming mistakes
before the names ship and existing projects depend on them.
## 17. Cross-platform filenames and paths (not checked)
Check for Windows-invalid characters, reserved device names, trailing path-component spaces or dots,
and case mismatches between `sub_path` or asset references and the files on disk. See
[filenames and paths](naming.md#filenames-and-paths).
## 18. Housekeeping worth a nit, not a block
`"from"` other than `"system"` (the bundle loader ignores it, though loading the file as a CLI config
rejects anything but `system` / `user` / `User`), `printer_settings_id` copied from another vendor,
redundant overrides that restate the parent's value, siblings that each repeat a value their base could
hold ([shared bases](shared-bases.md)), and a filename that disagrees with the preset's
`name` (the loader keys off `name`).
---
## Reporting the review
A finding is **one defect**: its file (or quoted lines), what breaks at runtime or in CI, and the fix.
Split independent defects into separate findings even when they live in one file: five id problems in
one bullet get one fix and four survivors. Say which findings `check` or the validator reports and which
only a reader catches: a missing version bump, a misspelled key and wrong temperatures pass CI, so a
contributor who only reruns the tools fixes what CI flags and resubmits the rest.
Severity discriminates only if it is earned:
| Severity | Means |
| --- | --- |
| blocker | the bundle fails to load, or a preset is unreachable at runtime |
| major | CI fails, existing users lose a preset, or a value prints wrong while CI stays green (PLA temperatures under an ASA name; a misspelled key whose intended value differs from the inherited one) |
| minor | wrong but working: redundant or dead keys that change nothing, `from`, naming |
Compute every number and id (`orca_profile_tool.py`, a scripted count) or omit it: one invented count
makes a reader stop trusting the right ones. Report a command's result only if you ran it. End with a
verdict: can it merge as it stands?
@@ -1,254 +0,0 @@
# Shared bases
Use this when drafting several presets at once, after `fix-variant`, or when sibling presets repeat the
same values. Each value is written once, on the base of the group it is true for, and a selectable
preset holds its identity and what makes it different. The work has two halves: choosing the groups,
which is judgment, and moving the values, which must leave every preset loading exactly what it loaded
before.
## When
- **Drafting** a printer family, a quality ladder or a product line: place each setting at its
[level](#levels) before writing any preset, then write the presets as overrides
([Rule 7](../SKILL.md#rules)).
- **After `fix-variant`.** It resizes each array in the selectable preset that writes it and never
moves or deletes a value ([variant arrays](validation.md#variant-arrays)). A family whose presets all
wrote one value now repeats the widened array in every preset, and a preset that restated what it
inherits now restates it wider.
- **Converting full copies**: a machine that inherits nothing, or a filament that restates fifty-plus
keys, is the style to move away from ([filament style](filament-profiles.md#style)).
Out of scope:
- **BBL.** Its profiles are synced from BambuStudio: a restructure is overwritten by the next sync and
makes every later sync diff unreadable. Fix BBL values in the file that holds them.
- **OrcaFilamentLibrary bases for one vendor's values.** A library base reaches every bundle's
filaments. Put a vendor's shared values on a base in its own bundle (a product `@base`, or a vendor
base that inherits the library's), and change a library base only for a value true of every filament
below it in every bundle.
- **Families the change does not touch.** Restructure the presets you are already changing; a
bundle-wide pass is a change of its own. Commit a restructure without any value change, so `compare`
alone verifies it.
## Nothing loads differently
A restructure changes where values are written, never what a selectable preset loads. Presets keep
their `name` and `setting_id`, and user presets and projects refer to a system preset by name and store
their own changes against what it loads, so an unchanged load changes nothing for users. Prove it with
the bundled helper, from the repository root (every subcommand takes `--profiles <dir>` to work on a
copy of the tree):
```bash
python3 .claude/skills/orca-profiles/scripts/shared_settings.py snapshot <before.json> # before any edit
python3 .claude/skills/orca-profiles/scripts/shared_settings.py compare <before.json> # after: "0 difference(s)", exit 0
```
`snapshot` records every selectable preset of every bundle as the loader stores it: the parent's stored
config, each `include` at the width its file wrote, the preset's own keys, then every variant array
resized to the preset's own variant list ([composition](vendor-bundle.md#inherits-and-include)). It
models the step `check` does not see: a base is stored after its own resize, so an array wider than a
base's list reaches its children cut. It also leaves out what the loader reads from each file and never
hands down (`name`, `type`, `from`, `instantiation`, `inherits`, `include`, `setting_id`,
`renamed_from`, `description`, `version`, `url`, `is_custom_defined`), so those keys never move to a
base. Nor does it record `print_settings_id`, `printer_settings_id` or `filament_settings_id`: the app
replaces them with the selected presets' names before slicing, so a file's value never counts. Delete
them from the presets you restructure rather than moving them to a base.
It does not know the built-in defaults, so `compare` lists a key written on one side only (no file of
the preset's chain writes it on the other) apart from the differences, and exits 1 for either. A
difference is a value the edit changed: undo it, or make it a separate, deliberate change. A one-sided
key is no change only when its written value is the option's default in `PrintConfig.cpp`: confirm
each, as when a preset that loaded the default by leaving a key out must now write it
([Balance 5](#balance)). A value equal to the default needs writing nowhere when no base above writes
another: delete it from the presets rather than moving it, and confirm the one-sided keys.
## Levels
A base stands for a level of the vendor's catalogue, and a setting lives at the level that determines
it. The test for a shared value: if it had to change for one preset of the group, should it change for
all of them? If yes, it is the group's and goes on the group's base. A value that is only equal today
(two unrelated printers with the same acceleration) stays in each preset: a base built on coincidence
is later edited for one preset and silently changes the others. For a default with exceptions
([Balance 5](#balance)), ask the question of the presets that inherit the default.
The tables give each setting's usual level; the test decides for a given bundle: where each toolhead
(Bowden or Direct Drive) has its own default filament, `default_filament_profile` follows the
toolhead, not the nozzle. Levels run
coarse to fine, and a chain need not visit every level: each preset or base inherits the next coarser
level that has a base.
| Machine level | Base | Settings it determines |
| --- | --- | --- |
| Vendor | `fdm_machine_common`, `fdm_<vendor>_common` | the vendor's defaults for every printer |
| Firmware | `fdm_klipper_common`, `fdm_marlin_common` | `gcode_flavor`, G-code that calls the firmware's macros (layer change, pause, filament change), `host_type`, `print_host`, the thumbnail format |
| Hardware family: models that share a frame, motion system, toolhead or extruder layout | `fdm_<vendor>_<family>_common` (`fdm_qidi_x3_common`, `fdm_machine_eryone_ER20_common`) | the `machine_max_*` limits, `extruder_clearance_*`, the toolhead's retraction where every nozzle shares it, `z_hop` and wipe, fitted hardware such as `auxiliary_fan`, the extruder count and per-extruder vectors, the variant layout ([Printer rule 2](extruder-variants.md#printer-machine)) |
| Model: one `machine_model`, which for an IDEX printer includes its mode | the default-nozzle preset where the bundle hangs its other nozzle presets off it; otherwise `fdm_<vendor>_<model>_common` | `printable_area`, `printable_height`, `bed_exclude_area`, the model's start G-code, a COPY or MIRROR mode's settings |
| Nozzle: the selectable preset | none | `printer_model` and `printer_variant`, which name the preset's model and nozzle and stay in every preset as the tree writes them; `nozzle_diameter`, `min_layer_height`, `max_layer_height`, `default_print_profile`, `default_filament_profile`, retraction the vendor tunes per nozzle |
A family may split once more, into toolhead or revision groups that exist only within it:
`fdm_<vendor>_<family>_common` → `fdm_<vendor>_<family>_mk1_common`. A split that crosses another axis is not a
level: when every controller comes with every toolhead, a toolhead base under each controller base
repeats the same values in each ([Balance 6](#balance)).
| Process level | Base | Settings it determines |
| --- | --- | --- |
| Vendor | `fdm_process_common`, `fdm_process_<vendor>_common` | strategy: seam, wall order, infill and support patterns |
| Printer family or variant layout | `fdm_process_<vendor>_<family>_common` (`fdm_process_arena_common`, BBL's `fdm_process_dual_common`) | speeds, accelerations and jerk of that motion system; the variant layout ([Process rule 4](extruder-variants.md#process)) |
| Layer height × nozzle | `fdm_process_<vendor>_<lh>_nozzle_<n>` (BBL's `fdm_process_single_0.20`) | `layer_height`, line widths, shell layers, speeds scaled to the layer |
| Quality × printer: the selectable preset | none | `compatible_printers`, and what is unique to that combination |
| Filament level | Base | Settings it determines |
| --- | --- | --- |
| Material | `fdm_filament_<material>` in OrcaFilamentLibrary, shared by every bundle | material defaults |
| Product | `<Product> @base` | `filament_id` (minted here, [ids](ids.md)), `filament_vendor`, `filament_type`, density, cost, the product's temperatures and cooling |
| Printer or nozzle tune: the selectable preset | none | `compatible_printers` (always in its own file, [Rule 9](../SKILL.md#rules)), volumetric speed, flow ratio, pressure advance and retraction measured on that printer |
**Equal where they must differ is a copy.** A key that follows a finer level, such as the layer-height
limits and line widths that follow the nozzle diameter or `printable_area` that follows the bed, never
moves above that level. When presets that differ in it carry the same value, the value was copied: leave
it in the presets and report it (the [worked example](#worked-example-an-idex-family) has one).
## Balance
1. **One group, one base; use the existing one first.** A base is the home of the presets below it,
whatever its name. In a bundle whose presets are all one family, the vendor base is the family base,
so the family's values go there. In a bundle that hangs the other nozzles off the default-nozzle preset, that preset is the model's home.
Never create a base whose presets are exactly its parent's. Where two existing bases already serve
the same presets (a copied `fdm_machine_common` above the vendor's own base), the finer one is the
home, and merging the pair is a change of its own. Moving a key into an existing home adds no file.
A base value that no preset below it loads is dead: replace it with the group's value when there is
one; otherwise leave it and report it, since a future preset would inherit it.
2. **A new base must stand for a level and pay for itself.** Its file costs five metadata keys and an
index entry, so create it only when it takes `k` keys off `n` selectable presets with
`(n − 1) × k > 6`, where a custom G-code value counts as one key per G-code line: what it removes
must outnumber what it adds. A model with two nozzles that share three short keys keeps them in both.
3. **No ad-hoc bases:** never a base for presets that merely agree (the ones with 0.8 mm retraction),
and never a base with one preset below it.
4. **Keep every selectable preset within four ancestors**, selectable parents included. When a level would push a preset past that, fold it into the level above or leave
its keys in the presets.
5. **A default with exceptions.** A value that only some presets below a shared base load moves to that
base, whichever axis it follows, when two conditions hold. More presets load it than any other value
(on a tie the key stays in the presets), and `w − a > 1`, where `w` presets drop their copy and `a`
presets that take the key from the base and load another value, the built-in default included, must
now write theirs. Presets that write another value keep it and are unaffected. The base then holds
the group's default, and the exceptions stay visible in their own files.
6. **One chain; the other axes stay in the presets.** `inherits` follows one axis. When presets vary
along several (bed size × controller × toolhead), first fill the existing homes by
rules 1 and 5. Then give new bases to the axis whose bases pay most: sum rule 2's count over its
bases, less the keys a re-parented preset must now write because it no longer inherits them from its
old parent; on a tie, follow the layering the bundle already has. Leave the other axes' keys in the
presets, and never repeat one axis's bases under each group of another. Outside BBL, whose synced
presets need theirs, add no `include` template for a second axis: the loader reads `include` only
since #15869 (2026-09-25), and an app that predates it ignores the key, so the template's settings
never reach the preset.
Name a new base after its level ([base names](naming.md#bases)). The name must be unique in its bundle
and must not equal a selectable preset's: two such presets load silently and the first in the index wins
([uniqueness](naming.md#uniqueness)).
## Restated values
`candidates` lists every key a file writes that it would load unchanged without writing it. Delete it
when the value is what the presets below the base that supplies it share: more of them load it,
written or inherited, than any other value. A family that restates machine limits, clearances and
G-code every other printer of the bundle loads from `fdm_klipper_common` drops its copies. When most
presets below that base load another value, the match is a coincidence: keep the key, and move it to
the level of the presets that share it. Keys of the nozzle level stay in the preset in
either case.
After a restructure the report still lists keys that are right where they are: the nozzle-level keys
each preset keeps, and arrays a list-less multi-extruder base writes at its presets' width for
`check --strict`.
## Variant arrays on a base
- The loader stores a base with its variant arrays resized to the base's own variant list, one variant
when it writes none, so an array on a narrower base reaches its presets as its first value, padded.
Move a variant array to a base only when the base declares the presets' variant list (and, for a
machine or process, their ids), moving the layout keys with it as
[Printer rule 2](extruder-variants.md#printer-machine) and [Process rule 4](extruder-variants.md#process)
ask; or when the presets load its first value anyway: every value is equal, or the presets are
list-less machines, which the loader cuts to one variant. `compare` catches a cut.
- Write the array on the base at the width of the presets it serves, their `N`
([sizing equation](extruder-variants.md#sizing-equation)), so `check --strict` judges the right width
where it reaches them. When the presets below a base need different widths (single- and
dual-extruder models on one base), leave the array in the presets, or on bases that each serve one
width.
- On a list-less multi-extruder family base, declare the extruder count: `nozzle_diameter` and the other
per-extruder vectors at one entry per extruder. The base then has its presets' width, and
`fix-variant --strict` writes an array that reaches the presets at another width into the base once,
instead of into every preset.
- Deleting a restated variant array leaves the preset on the inherited array. Plain `check` accepts
that; `check --strict` reports it when the inherited width differs, which is why a bundle held to
`--strict` restates the array at each preset's width.
## Procedure
1. **Snapshot** the tree before any edit, `fix-variant` included. After `fix-variant`, run `compare`: a
difference is a value its padding changed (it repeats the last value, the loader the first). Set that
value deliberately, then snapshot again as the baseline for the restructure.
2. **List the candidates:**
```bash
python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "<Vendor>" --type machine
python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "<Vendor>" --type machine --group-by printer_model
```
It prints the [restated values](#restated-values), then, per base, the keys every selectable preset
below it loads with one value and how many of those presets write it themselves, and under
`default with exceptions` the values that pass [Balance 5](#balance), with `w` and `a`. With
`--group-by <key>` it groups the selectable presets by that key's value instead and names each
group's nearest common base, where a new base would go: `printer_model` for models, `gcode_flavor`
for firmware, `extruder_type` or `default_filament_profile` for toolheads, `filament_id` for
filament products, `layer_height` for processes. The report is evidence, not a plan: it cannot tell
a shared value from a coincidence or a copy.
3. **Decide each key** by [Levels](#levels), [Balance](#balance) and
[Restated values](#restated-values): delete the restatements of shared values, move group values up
to the group's home, and create only the bases that pay.
4. **Edit.** Each new base gets `"type"`, `"name"`, `"from": "system"`, `"instantiation": "false"`, no
`setting_id`, and `inherits` set to the presets' old parent. Point the presets' `inherits` at it and
delete the moved keys from them. Bump the version, then run `normalize`, `update-index` (it orders
parents first) and `generate-id --dry-run`, which must write nothing: bases take no id and presets
keep theirs.
5. **In a bundle held to `--strict`, run `fix-variant --strict` now**, so it writes into the new bases.
6. **Verify.** `compare` prints `0 difference(s)`, and every one-sided key it lists is a default.
`check` reports no error it did not report before, and neither does `check --strict` where the bundle
passes it. Then run the [authoring checks](../SKILL.md#creating-or-modifying-a-profile).
7. **Report** each base added (name, level, presets below it, keys it holds), the keys left in presets
and why, the copies found, and the `compare` result.
## Worked example: an IDEX family
A Klipper bundle's IDEX family has 36 selectable machines (bed size 300, 400 or 500 × normal, COPY or
MIRROR mode × 0.4, 0.5, 0.6 or 0.8 nozzle) that all inherit `fdm_klipper_common` directly and write 49
keys each. `fix-variant` has widened their retraction arrays to two values and their machine limits
to four.
- **Restated.** All 36 restate 10 values that every other printer of the bundle loads from
`fdm_klipper_common`: three machine limits, the three clearances, wipe, `retract_before_wipe`, and the
layer-change and pause G-code. Delete them.
- **Family.** All 36 load one value for 24 more keys: the other machine limits, `extruder_offset`,
retraction, `z_hop`, `single_extruder_multi_material`, `manual_filament_change`, the remaining G-code
except the start G-code, and thumbnails. A new family base, `fdm_<vendor>_<family>_idex_common`,
holds them, plus `nozzle_diameter` `["0.4", "0.4"]` for the extruder count: at least
`(36 − 1) × 24 = 840`.
- **Model.** Each `printer_model` (a bed size in one mode) has four nozzle presets that share
`printable_area`, `printable_height` and a three-line `machine_start_gcode`: `(4 − 1) × 5 = 15`, so
one base per model, nine in all. The family does not hang its other nozzles off a default-nozzle
preset, so the model level here is a base.
- **A coincidence.** The twelve 500 presets' `printable_height` 500 equals `fdm_klipper_common`'s, but
every preset of the bundle writes its own height and 300 is the most common. The 500 is the base's
leftover, not a shared value, so it stays on the model bases.
- **A copy.** In COPY and MIRROR mode, every nozzle of a model carries the 0.4 nozzle's
`min_layer_height`, `max_layer_height` and `retract_lift_below`: 0.06, 0.3 and 0.2 on the 0.8 nozzle,
where normal mode has 0.12, 0.5 and 0.3. `candidates --group-by printer_model` lists the first and
last as shared by the model, and `max_layer_height` as restated from `fdm_klipper_common`, whose value
is also 0.3. They follow the nozzle, so they stay in the presets and are reported for tuning.
- **Nozzle.** Each preset keeps `printer_model`, `printer_variant`, `nozzle_diameter`,
`min_layer_height`, `max_layer_height` and `retract_lift_below` beside its metadata.
- **Result.** 36 full presets become 36 short ones on 10 new bases. `compare` reports 0 differences,
`check --vendor "<Vendor>"` passes as before, and `generate-id --dry-run` writes nothing.
`check --strict` reports more errors than before, because the deleted variant arrays now reach the
presets at `fdm_klipper_common`'s one value. `fix-variant --strict` writes those arrays into the
family base alone, after which `check --strict` passes for the family and `compare` still reports 0
differences.
@@ -1,447 +0,0 @@
# Validating profiles
```bash
./scripts/check_profile.sh # everything CI runs
./scripts/check_profile.sh --vendor "<Vendor>" # development loop
./scripts/check_profile.sh profile_tool validate_slice # named checks only
```
```bat
scripts\check_profile.bat :: the same three, on Windows
scripts\check_profile.bat -Vendor "<Vendor>"
scripts\check_profile.bat profile_tool validate_slice
```
`check_profile.bat` is a shim around `check_profile.ps1`: same checks, same order, same logs. Its flags
take PowerShell spellings (`-Vendor`, `-ProfilesDir`, `-Validator`, `-Download`, `-Refresh`,
`-WorkDir`, `-LogLevel`), and positional check names are unchanged. `-p`, `-v` and `-l` are aliases,
so `-v Elegoo -l 2` reads the same on both platforms. It passes `-ExecutionPolicy Bypass` because a
default Windows client refuses to run a checked-out `.ps1` at all. The `.ps1` finds Python itself,
probing `py -3`, then `python`, then `python3`; run the tool by hand with `py -3` for the same reason.
Every check runs even after an earlier one fails; the script exits non-zero if any failed, and writes
`logs/<check>.log` plus, on failure, `pr_comment.md` (the same report CI posts on the PR) under a
per-user cache dir:
| Platform | Cache dir |
| --- | --- |
| macOS | `~/Library/Caches/orca-profile-check` |
| Linux | `${XDG_CACHE_HOME:-~/.cache}/orca-profile-check` |
| Windows | `%LOCALAPPDATA%\orca-profile-check` |
It is named apart from OrcaSlicer's own per-user dirs and sits outside the checkout, so every worktree
shares one copy and each run overwrites its `logs/`. `--work-dir` / `-WorkDir` overrides it.
When other worktrees or agents may run checks too, pass `--work-dir <a dir of your own>` from the start
and capture the console output yourself: the shared `logs/` can belong to another run by the time you
read them. With `--work-dir`, also pass `--validator` pointing at the cached nightly, so the new dir
does not download it again:
| Platform | Cached validator |
| --- | --- |
| Linux | `<cache dir>/validator/OrcaSlicer_profile_validator` |
| macOS | `<cache dir>/validator/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator` |
| Windows | `<cache dir>\validator\OrcaSlicer_profile_validator.exe` |
Copying the cached `profile-fixtures/` into the new dir reuses the fixture archives; the fixture
`manifest.json` is still downloaded on every run, so `validate_custom` needs the network either way.
`another run is using <dir>` means a live run holds `<dir>/.lock`: leave it and use your own
`--work-dir`. Only a `.lock` with no `check_profile` process alive is a crash leftover; delete it by
hand.
## The five checks
| Check | Command it runs | Catches |
| --- | --- | --- |
| `profile_tool` | `python3 scripts/orca_profile_tool.py check` | index coverage **both ways**, preset-name collisions, files `normalize` / `update-index` would still rewrite, duplicate JSON keys, filament `compatible_printers`, `filament_type` array, conflict keys, variant strings and array widths and layout keys, dangling `default_materials`, id length, **all `setting_id` and `filament_id` rules** ([below](#orca_profile_toolpy-check)) |
| `validate_system` | `validator -p resources/profiles -l 2` | load errors, missing filament `compatible_printers`, dangling `inherits` / `compatible_*`, duplicate `filament_id` per printer (`Ambiguous AMS filament match`), printer defaults that name no compatible system filament |
| `validate_slice` | `validator -p … -s -l 2` | custom G-code expansion and unresolvable printer defaults, by slicing |
| `validate_filament_subtypes` | `validator -p … -l 2 -f` | nothing extra; see below |
| `validate_custom` | `validator -p <tree + fixture> -l 2` | a shipped preset name that a past release offered no longer resolving |
**`-f` is a no-op.** It defaults to on, so the duplicate-`filament_id` check runs whether or not you
pass it, and `validate_system` already fails on duplicates. The binary's own `--help` ("Off unless this
flag is present") does not reflect that default.
**A `--vendor` run reads differently from CI.** The validator's `-v` loads that vendor plus
OrcaFilamentLibrary and nothing else, so every library tune whose `compatible_printers` names another
vendor's printers fails `validate_system`, `validate_filament_subtypes` and each `validate_custom`
fixture with thousands of `references unknown compatible_printers "Bambu Lab …"` lines. Under
`--vendor`, `profile_tool` and `validate_slice` are the meaningful results; for the other three, filter
the log for your vendor's files and treat only those lines as findings. The unscoped run is the CI
result; run it before the PR.
### `validate_custom`: the backward-compatibility gate
It downloads one fixture archive per past release (v1.9.0 onwards) of *generated mock* user presets: a
`<vendor>_<preset>_orca_test` copy of every system preset that release shipped, cut with the
validator's own `-g 1` mode. It unpacks each over a copy of the current tree and loads it. Each entry
holds only `inherits` plus a canned diff, so the one failure it adds over `validate_system` is a shipped
preset name disappearing. This is what makes a rename, a deletion or an `instantiation` flip a CI
failure rather than just a user complaint, and the reason `renamed_from` is mandatory.
The whole current tree sits under each fixture, so every `validate_system` error fails
`validate_custom` too: fix `validate_system` first. Under `--vendor` it copies only the top-level index
files, `<Vendor>/` and `OrcaFilamentLibrary/`, and picks fixtures by the index's display `name`
(`Bambulab` for `BBL`), not the file stem; fixture presets without that prefix are covered only by an
unscoped run, and it warns `validate_custom checked nothing` when none match.
### `validate_slice`
It slices a two-colour cube on every instantiable printer in the tree, sequentially, forcing the prime
tower. It selects `default_print_profile` and the first `default_filament_profile`, then updates
compatibility; that update can select a different compatible preset. Confirm the intended defaults
yourself rather than treating a passing sweep as proof that those exact presets were sliced.
A printer fails if it cannot be selected, falls back to a Default preset, throws, produces no G-code,
or emits no `CP TOOLCHANGE START` (`change_filament_gcode` never expanded). Non-default processes and
filaments get no dedicated coverage; [slice them on a copy](#checking-a-copy-of-the-tree). In the
default set, a bundle without a `machine/` folder is recorded as SKIP; naming `validate_slice`
explicitly for it fails (`No instantiable printer presets found for vendor OrcaFilamentLibrary`). The
validator logs `[error]` lines that do not fail a check (such as `could not found extruder_type`); only
each check's PASS or FAIL counts.
## `orca_profile_tool.py check`
`check` is one subcommand of the tool that also owns `fix-variant`, `generate-id`, `normalize`,
`trim` and `update-index`; [ids.md](ids.md#the-tool) has the writing half.
| Catches | Scope | Function in the tool |
| --- | --- | --- |
| two files in one bundle claiming one type + name, indexed or not | per vendor | `check_preset_name_uniqueness` |
| a file on disk that no `*_list` references (**an error, not a warning**) | per vendor | `check_index_coverage` |
| an index entry whose `name` disagrees with the file, or whose `sub_path` is missing | per vendor | `check_name_consistency` |
| a file `normalize` would rewrite, an index `update-index` would rebuild | per vendor | `check_normalized` |
| duplicate JSON keys in a file | every file read | the JSON loader |
| an instantiated non-library filament with no non-empty `compatible_printers` of its own | per vendor | `check_filament_compatible_printers` |
| `extruder_clearance_radius` alongside `extruder_clearance_max_radius` | per vendor | `check_conflict_keys` |
| a scalar `filament_type` (`"filament_type": "PLA"`); the five other filament vectors `normalize` arrayifies surface as `normalize would convert <field> to an array` | per vendor | `check_vector_type_keys`, `check_normalized` |
| a variant string the two enums cannot build (a dead variant, a legacy spelling included), a variant list naming one variant twice, an `extruder_type`, `nozzle_volume_type` or `default_nozzle_volume_type` that is not an enum name ([variant names](#variant-names)) | per vendor | `check_variant_names` |
| a variant array not exactly `variant length × stride` wide in a selectable preset that writes it (with `--strict`, also in one it reaches); machine variant layout keys that disagree; a process id array that does not pair each variant ([variant arrays](#variant-arrays)) | per vendor | `check_variant_arrays` |
| a declared `filament_id` longer than 8 characters | per vendor | `check_filament_id_length` |
| a `default_materials` name, or a `default_filament_profile` name, matching no filament file ([below](#default-material-references)) | per vendor | `check_machine_default_materials` |
| per-key warnings for ignored options, **filament files only** | per vendor | `check_obsolete_keys` |
| `setting_id` uniqueness, every `filament_id` rule, and `machine_model` names duplicated across bundles | **tree-wide, ignoring `--vendor` entirely** | `check_setting_id_uniqueness`, `check_filament_ids`, `check_machine_model_name_uniqueness` |
Because the id and model-name checks stay tree-wide, a vendor-scoped run can and does fail on another
vendor's files, and it saves seconds, not minutes.
Unscoped, the per-vendor pass covers every bundle. The only exclusion is the stray `user/` directory
(below); OrcaFilamentLibrary is held to the same rules as any vendor, its sole exemption being that a
library filament may leave `compatible_printers` empty. `check_normalized` covers every bundle with an
index.
Notes that matter:
- Exit codes: **0** clean, **1** errors found, **2** argparse misuse. Warnings never change the exit
code.
- A nonexistent `--vendor` is a hard error: `[ERROR] unknown vendor "<V>" in <dir>`, exit 1.
- `--vendor ""` means all vendors; `check_profile.sh` relies on that. `--vendor` is repeatable
(`check --vendor A --vendor OrcaFilamentLibrary`); `check_profile.sh` takes one.
- A **stray directory** under `resources/profiles/` still gets counted as a vendor by the per-vendor
pass and warned about (`No profiles found for vendor: <dir> at …/<dir>.json`, and the "Checked
vendors" count goes up by one): usually an emptied folder, or a `user/` left by a direct validator
run. An unscoped `check` skips `user/` by name; `--vendor user` still checks and warns about it.
`normalize`, `trim` and `update-index` ignore strays too: they define a bundle as *a directory with a
matching index file*.
- Each remedy is printed once for the whole run, not once per file, as a `[WARNING]` under the errors
(`2 unreferenced file(s) above: delete them, or run … update-index`). Read those lines: they name the
command that fixes the batch.
- When there are errors or warnings, the trailing summary suggests `normalize`. That is right for the
shape errors and misleading for everything else: an id error needs `generate-id`, a dangling
`default_materials` needs a human.
- Other options: `--dry-run` on every writing command, `--profiles DIR` to point any command at another
tree, `--profile-type` to narrow `normalize`, `trim` and `update-index` to one type
([ids.md](ids.md#the-tool)).
- `resources/profiles/check_unused_setting_id.py` is a legacy BBL-only diagnostic, not part of
profile CI. Use `orca_profile_tool.py check` for current id validation.
### Obsolete keys
`check` always reports per-key warnings for obsolete options in filament profiles. Its normalization
check also rejects obsolete keys across all preset types (`normalize would remove <key>`); `normalize`
removes them.
### Default-material references
The materials check finds `default_materials` / `default_filament_profile` entries naming a preset that
does not exist. It reads each `machine/` file's own key (a model's `default_materials`, a variant's
`default_filament_profile`; a file that writes both is checked on `default_materials` only) and accepts
any `name` found in the vendor's or OrcaFilamentLibrary's `filament/` files, bases and unindexed files
included. The three authoring errors it surfaces are `,` instead of `;`, wrong case (`@system`), and a
whole `;`-joined string stuffed into one array element. Only the validator (`validate_system`) requires
an instantiated system filament that is compatible with each variant.
### Variant arrays
`check_variant_arrays` composes every selectable preset the loader's way (the parent, then each
`include` in order, then the file's own keys; a filament's `inherits` may fall through to
OrcaFilamentLibrary) and holds each key of [the four variant sets](extruder-variants.md#the-four-key-sets)
that the preset writes itself to exactly its `variant length × stride`
([widths](extruder-variants.md#widths)). The variant length is the length of the composed preset's
own `*_extruder_variant` list; without one, a machine's is the number of variants its
`extruder_variant_list` offers, else its extruder count (the list's default is one
`Direct Drive Standard` per extruder), and a process's or filament's is 1. Any other width is an
error, one value included and even when every value is the same. A key the preset does not write
takes what reaches it, the default or an array it inherits or includes, which the loader resizes; it
is not checked. A base is not judged on its own: what it writes counts only where it reaches a preset
that does not override it.
`check --strict` also holds every selectable preset to its own width for each key that reaches it,
so a preset whose variants differ from those of the file its array comes from restates the array
(the error names that file). That is BBL's practice and the target for new printer-specific presets;
CI runs `check` without `--strict`.
An id array that reaches a selectable preset, `printer_extruder_id` or `print_extruder_id`, written or
inherited, must have one entry per entry of its variant list on any printer. This rule and the
machine layout rules below judge the composed preset without `--strict`, whichever file writes the
keys. Beside a written variant list this rule reports it instead of the width
rule, so it is reported once. A process that
lists variants without `print_extruder_id` gets a **warning** when a variant repeats (every entry then
reads as extruder 1, so the repeated variant is unreachable), nothing otherwise.
On a machine the layout keys are held to [Printer rules 1–4](extruder-variants.md#printer-machine),
every failure an error unless marked:
- With `extruder_variant_list` written: the list has one entry per extruder, as many as
`nozzle_diameter`; every variant starts with its extruder's `extruder_type`;
`default_nozzle_volume_type` names a nozzle volume type that extruder lists;
`printer_extruder_variant` is the list flattened extruder-major and `printer_extruder_id` gives each
entry its 1-based extruder (an id array left out reads as extruder 1 everywhere, which passes when
those are the flattening's ids). Without the pair, the list may offer one variant in total.
- With the pair written and no `extruder_variant_list`: one variant per extruder at most. A pair that
`single_extruder_multi_material` off would replace with the default at load is a **warning**; a pair
the rebuild would leave as it is passes.
It does **not** see a base's own resize: it composes at the width each file wrote, so an array wider
than a base's list, which the loader cuts before any child inherits it, passes even with `--strict`
([composition](extruder-variants.md#padding-truncation-and-composition)). Nor does it see per-extruder
vectors outside the sets (`extruder_offset`, `printer_extruder_options`, …), which no variant list
sizes. What a variant holds (a High Flow variant copied from Standard, a variant inserted at the wrong
index) is review work
([item 14](review-checklist.md#14-per-extruder-vectors-not-checked-and-variant-arrays-widths-and-layout-checked));
whether it is a name the engine can select at all is `check_variant_names`'.
`python3 scripts/orca_profile_tool.py fix-variant` resizes every array the width rule rejects in the
selectable preset that writes it, leaves bases alone and adds no key: extra values are dropped, missing ones
repeat the last value (the last normal/silent pair at stride 2; a lone value fills normal and silent
alike). It leaves the variant lists and id arrays to you, since they address the variants rather than
fill them. `fix-variant --strict` then also writes each key that reaches a selectable preset at
another width: into the most general file on the way down to the preset whose own width is the
preset's and whose selectable presets taking it all need that width, else into the preset itself.
Presets of every bundle count towards that agreement; `--vendor` limits the files written and
`--dry-run` previews. The loader pads with the first value where `fix-variant` repeats the last, so a
padded array need not load as before, and
trimming deletes values: when the extra values were meant as per-extruder or per-variant values,
declare the variant layout instead ([Printer rule 2](extruder-variants.md#printer-machine)) and keep
them. `fix-variant` moves no value, so a family whose presets all wrote one value repeats the widened
array in each; put it on the family's base afterwards ([shared bases](shared-bases.md)).
### Variant names
`check_variant_names` reads the four list keys plus `extruder_type`, `nozzle_volume_type` and
`default_nozzle_volume_type` of every preset the bundle's index references, bases included, and holds
each entry to the names the engine's two enum maps define (`s_keys_map_ExtruderType`,
`s_keys_map_NozzleVolumeType`, read from `PrintConfig.cpp` on every run). The bundle's own files are
judged, not the composed config: a bad string is the writing file's error, once. Every finding is an
error, and no bundle is exempt: BBL, whose bundle is imported from BambuStudio, is held to OrcaSlicer's
enums like any other.
- A variant string outside `<extruder type> <nozzle volume type>` is a **dead variant**: it still
counts toward the variant length the arrays are sized by, so the values written for it silently never
reach the G-code. `Hybrid` too, which is runtime-only, and an empty entry. A name BambuStudio's enum
has and OrcaSlicer's lacks is dead here as well, and passes with no tool change once the engine gains
that nozzle volume type.
- A legacy name the loader still rewrites in these keys (`Normal` → `Standard`, `Big Traffic` →
`High Flow`) is an error that names the enum name to write; the profile has to spell the enum
name. `DirectDrive` is only rewritten in `extruder_type`, so a variant string carrying it is dead.
- A variant list naming one variant twice is an error: the lookup returns the first equal string, so
the repeat is unreachable and its value sits at an index no extruder reads. A filament list takes
strings, `extruder_variant_list` takes them per extruder, and a process takes `(extruder id, variant)`
pairs — one string on two extruders is two pairs, not a repeat.
- An `extruder_type`, `nozzle_volume_type` or `default_nozzle_volume_type` value that is not an enum
name is an error: they are enum options, so an unknown value fails the validator's load of the
whole bundle, while the app silently loads the option's default instead. A legacy spelling
(`DirectDrive`, `Normal`, `Big Traffic`) and `Hybrid`, an enum value no profile writes, are errors
too.
The variant *order*, the choice of variants, and the values themselves are not checked.
### `normalize` and `update-index` are part of the check
`check` fails when either command would still change something, so they are not optional polish: the
file that gets reviewed has to be the file that ships. What `normalize` changes is narrow and fixed:
- adds a missing `type`;
- deletes a `version` or `is_custom_defined` key from a *preset* file;
- deletes six print-speed keys from filament profiles (`initial_layer_print_speed`, `outer_wall_speed`,
`inner_wall_speed`, `infill_speed`, `top_surface_speed`, `travel_speed`);
- deletes the obsolete keys the loader ignores (the `ignore` set in `PrintConfigDef::handle_legacy`),
across preset types;
- resolves the `extruder_clearance_*` conflict pair by keeping the larger;
- arrayifies six filament options (`filament_type`, `filament_cost`, `filament_density`,
`temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`);
- hoists `type`, `name`, `renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`,
`instantiation` to the front.
A file it changes is then rewritten whole: tab-indented, LF, one trailing newline, keys reordered. A
file committed with CRLF line endings therefore changes on every line; read the diff before committing
it.
**Set `type` explicitly when authoring.** For a file in `machine/` without it, normalization guesses
`machine` only if its name contains `nozzle` (case-insensitive), otherwise `machine_model`. That
heuristic cannot reliably classify shared machine bases or unusually named variants.
The tool's obsolete-key set is checked against the loader's ignore list by a unit test. Active options
are preserved, including live keys whose *values* the loader rewrites (`extruder_type`: `DirectDrive` →
`Direct Drive`; the variant-string keys: `Normal` / `Big Traffic` → `Standard` / `High Flow`), and so are
legacy key names the loader migrates (such as `extruder_clearance_max_radius`).
Two things it therefore does **not** enforce:
- **Formatting and key order on their own.** A file with none of those problems is skipped entirely, so
4-space indent, a missing trailing newline, and a file that leads with `compatible_printers` all pass
`check`. They stay latent until something else trips `normalize` and the whole file reformats inside
an unrelated diff. (`normalize --force` rewrites every file; do not run it on a shipped bundle.)
- **A misspelled setting key.** `inital_layer_height` and `sparse_infill_densiti` pass `check` cleanly.
Verify new keys against `src/libslic3r/PrintConfig.cpp` and the loader's legacy handling
(`PrintConfigDef::handle_legacy`: renamed keys, rewritten values and ignored keys).
`check` does not catch a dangling `default_print_profile` either; check that name by hand.
## The validator binary
Built from `src/dev-utils/OrcaSlicer_profile_validator.cpp` with `-DORCA_TOOLS=ON`. Both scripts use a
local build under `build*/` when one exists, else they download the nightly into the `validator`
subdirectory of the cache dir. `check_profile.sh` searches Release, then RelWithDebInfo, then Debug
(each under `build*/src/<config>` and `build*/*/src/<config>`), then single-config `build*/src`, and
prefers a host-architecture build tree; `check_profile.ps1` tries Release, RelWithDebInfo, MinSizeRel,
then Debug. A stale local build is used silently; `--download` / `-Download` skips local builds and uses the
nightly. The download and the fixtures stay cached until `--refresh` / `-Refresh`, so
`--download --refresh` (`-Download -Refresh`) matches CI exactly. Windows looks for
`OrcaSlicer_profile_validator.exe`.
If your build lives somewhere else, point at it with `--validator` / `-Validator`, or set
`ORCA_PROFILE_VALIDATOR` (`$env:ORCA_PROFILE_VALIDATOR` in PowerShell).
| Flag | Meaning |
| --- | --- |
| `-p <dir>` | profile tree (also becomes the data dir) |
| `-l <n>` | log level; CI uses 2 |
| `-v <Vendor>` | load only that vendor **plus** OrcaFilamentLibrary |
| `-s` | slice sweep |
| `-o <dir>` | with `-s`, save each printer's G-code there |
| `-f` | no-op (see above) |
| `-g 1` | regenerate user-preset fixtures; takes a value, and wipes the user preset dir first |
On ARM64 Linux the nightly is x86-64 only: the script warns and downloads anyway, producing a binary
that will not run. Build it locally with `-DORCA_TOOLS=ON` and pass `--validator`.
Running the validator directly uses the profile tree as its data directory and can create `user/`
there. Prefer the wrappers, which stash existing user presets and restore them afterward. After a
direct run, inspect `user/` and remove only empty directories created by that run; fixtures or
pre-existing user files may be present.
## Checking a copy of the tree
Use `--profiles DIR` on the Python tool and `-p DIR` on the validator. The wrappers' `--profiles` /
`-ProfilesDir` passes the tree to both, so one run validates a copy fully:
```bash
./scripts/check_profile.sh --profiles "<tree>"
# Windows: scripts\check_profile.bat -ProfilesDir "<tree>"
```
To slice a process or filament that is not a printer's default, or to read the G-code, work on a copy:
copy `resources/profiles` and `resources/info` into one scratch dir (the validator reads `info/` next to
the tree), point the printer's `default_print_profile` or first `default_filament_profile` at the preset
in the copy, and run the validator with `-o`:
```bash
<validator> -p <scratch>/profiles -v "<Vendor>" -s -o <gcode dir>
```
Each printer's G-code is saved as `<vendor name>__<printer>.gcode`; its embedded config
(`; print_settings_id = …`, `; filament_settings_id = …`) shows what was actually sliced.
## Testing in the app
Editing this checkout's `resources/profiles` does not update a separately installed application. Test
with a build using the edited resources and a bumped bundle version: the updater installs newer bundles
under `<data_dir>/system/`, and the preset cache also depends on the bundle version. Use Help ▸ Show
Configuration Folder to locate the active data directory:
| Platform | Default data directory |
| --- | --- |
| macOS | `~/Library/Application Support/OrcaSlicer` |
| Linux | `$XDG_CONFIG_HOME/OrcaSlicer`, or `~/.config/OrcaSlicer` when unset |
| Windows | `%APPDATA%\OrcaSlicer` |
A portable `data_dir` next to the executable takes precedence. Use a separate test configuration for a
clean-install check; preserve the normal configuration and user presets.
## Error → remedy
| Message | Fix |
| --- | --- |
| `can not find inherits <parent> for <preset>` | parent missing, unregistered, misspelled, or listed **after** the child |
| `can not find include` | the template is misspelled, registered after the includer, or selectable |
| `can not find filament_id for <name>` | nothing in the chain declares one: run `generate-id` |
| `can not find parent <name> for config <user preset>!` | a shipped name disappeared: add `renamed_from` |
| `Failed loading configuration file <file>` | that file could not be loaded and the whole bundle was discarded: a JSON error, or a value its option cannot take, such as `nil` in a non-nullable key (`Invalid value provided for parameter <key>: nil`, `Deserializing nil into a non-nullable object`); the lines above it name the cause |
| `Missing instantiation attribute for <name>` | key absent **or** not the string `"true"` / `"false"` |
| `contains incorrect keys: <keys>, which were removed` | a key valid for a different preset type |
| `defines invalid printer variant "<v>"` | not a token of the model's `nozzle_diameter` list |
| `has printer_variant "<v>" that does not match its nozzle_diameter` | [the set comparison](machine-profiles.md#printer_model-and-printer_variant) |
| `references unknown compatible_printers "<p>"` | the printer was renamed or deleted, or a `machine_model` name was used instead of a variant name: fix the reference. Under `--vendor`, usually another vendor's printer ([why](#the-five-checks)) |
| `references renamed compatible_printers "<old>" (now "<new>")` | in-tree references must name the current preset; `renamed_from` does not excuse them |
| `Filament preset "<f>" is missing compatible_printers setting` | non-library filaments need a non-empty list in their **own** file; the resolved-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) |
| `Ambiguous AMS filament match: N filament presets share filament_id "X" and are all compatible with printer "Y"` | make the lists disjoint by [specificity](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product): the specialized profile keeps the variant, the general ones drop it; prefer this over deleting a profile. Or fix an `inherits` pointing at another product's `@base`. `orca_profile_tool.py check` does not catch this; only `validate_system` does |
| `Layer height cannot exceed nozzle diameter.` / `Line width too small` / `Line width too large` | the [slicing limits](process-profiles.md#values-to-review-per-nozzle) |
| `[ERROR] … no <V>.json list references it, so it never loads` | `update-index`, or delete the file |
| `[ERROR] … no <V>.json list references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` |
| `[ERROR] … normalize would <change>` / `<V>.json: update-index would rebuild <lists>` | run that command and commit the result |
| `[ERROR] <V> has N <type> profiles named "<name>"` | identify the intended preset and remove or rename the duplicate; use `trim --dry-run` only for deliberate unindexed-file cleanup |
| `Duplicate key error in <file>: Duplicate key detected: <key>` | a key written twice in one file; keep the intended one |
| `… must not have a setting_id` / `… is missing a setting_id` / `setting_id "X" in <file> does not match the expected "Y" …` | `generate-id --setting-id` (by hand in `BBL/`, see [ids.md](ids.md#bbls-exception-precisely)) |
| `filament_id "X" declared by … does not match the mint of its triple …` | `generate-id --filament-id`; if the id should not be declared here at all, remove it so the preset inherits its root's |
| `inherits filament_id "X" but its own triple "V/T/N" mints "Y"` | `generate-id` will **not** fix this; see [ids.md](ids.md#what-generate-id-does-and-does-not-fix) |
| `"<key>" has N values for variant length S at stride k, which takes M` (with ` (no <list key>, so …)` after `S`, naming where `S` came from, when the preset writes no list, and ` (it comes from <file>)` at the end under `--strict` for an array the preset does not write) | exactly `S × k` values in variant order, or leave the key out ([widths](extruder-variants.md#widths)); on a list-less multi-extruder printer whose extruders differ, declare the layout; `fix-variant` cuts or pads to that width ([variant arrays](#variant-arrays)); an `S` you did not expect means the variant list did not resolve through `include` or `inherits` |
| `printer_extruder_variant […] is not extruder_variant_list flattened extruder-major […]` / `printer_extruder_id […] does not give each entry of printer_extruder_variant its 1-based extruder […]` / `printer_extruder_id has N entries for the M entries of printer_extruder_variant` | write the pair as the flattening of the list, ids in step ([Printer rule 2](extruder-variants.md#printer-machine)) |
| `extruder_variant_list has N entries for M extruder(s)` / `extruder_variant_list entry i "…" holds a variant that does not start with extruder i's extruder_type` / `default_nozzle_volume_type "…" is not a nozzle volume type extruder i lists` | [Printer rules 1, 3 and 4](extruder-variants.md#printer-machine) |
| `extruder_variant_list offers N variants but the preset writes no printer_extruder_variant/printer_extruder_id` / `printer_extruder_variant lists several variants for extruder N but the preset writes no extruder_variant_list` / `[WARNING] … with single_extruder_multi_material off the loader replaces printer_extruder_variant …` | write all three layout keys ([Printer rule 2](extruder-variants.md#printer-machine)) |
| `print_extruder_id has N entries for the M entries of print_extruder_variant` / `[WARNING] … print_extruder_variant repeats a variant but print_extruder_id is absent` | one id per variant entry, mirroring the printer's pairs ([Process rule 1](extruder-variants.md#process)) |
| `<key> <where> holds "…", which no extruder can select: "…" is not a nozzle volume type the enum has (…)` / `… it does not start with an extruder type the enum has (…)` / `… is empty` / `… Hybrid names the sub-nozzles of one hybrid extruder at runtime` | write a legal variant string, `<extruder type> <nozzle volume type>` from the two enums ([variant names](#variant-names), [variant strings](extruder-variants.md#variant-strings)); in every bundle, BBL included |
| `… holds the legacy variant "…"` / `extruder_type spells the legacy name "…"` / `nozzle_volume_type spells the legacy name "…"` / `default_nozzle_volume_type spells the legacy name "…"` | write the enum name the message gives: the loader still rewrites the legacy one, but `check` rejects it |
| `<key> lists "…" N times` / `print_extruder_variant lists the pair (extruder N, "…") N times` | drop the repeat and its value from every variant array: the lookup returns the first equal string |
| `extruder_type "…" is not one of (…)` / `nozzle_volume_type "…" is not one of (…)` / `default_nozzle_volume_type "…" is not one of (…)` / `… names "Hybrid", which the engine computes for a hybrid extruder at runtime` | they are enum options, so an unknown value fails the validator's load of the whole bundle and silently becomes the default in the app; use `s_keys_map_ExtruderType` / `s_keys_map_NozzleVolumeType`, `Hybrid` excepted ([variant strings](extruder-variants.md#variant-strings)) |
| `[WARNING] No profiles found for vendor: <dir>` | a directory with no matching index (an emptied folder, or a `user/` left by a direct validator run); remove it |
| `… has no compatible system filament in its model's "default_materials"` | add a system filament preset compatible with that variant to the model's `default_materials` |
| `… names the unknown system filament "<n>" in its "default_materials"` / `… "default_filament_profile"` | name an existing system (not user, not base) filament exactly; `;` separators, exact case |
| `Missing filament profile: '<n>' referenced in <file>` | the same, caught by `check`; usual causes are `,` instead of `;`, wrong case (`@system`), or a `;`-joined list packed into one array element |
| `machine_model name "<n>" is declared by N bundles` | model names are unique across the tree; rename the new model |
| `vendor <V>'s config version: <s> invalid` | the `version` string does not parse; write `MM.mm.pp.bb` |
| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or a model; see [failure scopes](vendor-bundle.md#failure-scopes) |
| `Printer "<p>" fell back to a default preset` | the final process or filament selection is a generic Default preset: check the named defaults, their visibility and that compatible presets exist. An incompatible default may instead be replaced without this error |
| `Printer "<p>" sliced but the filament change never fired (no CP TOOLCHANGE START)` | `change_filament_gcode` never expanded |
## CI
`.github/workflows/check_profiles.yml`, job **"Check profiles"**, runs on `pull_request` into `main` or
`release/*` touching `resources/profiles/**`, `resources/printers/**`, `scripts/**`,
`src/libslic3r/PrintConfig.cpp` (where the tool reads the variant key sets) or the workflow itself.
There is no push trigger: a direct push to main runs no profile validation.
The job opens with `python3 -m unittest discover -s scripts/tests -t scripts`, the tool's own unit tests
(run them locally after changing `scripts/`, with `py -3` on Windows, and keep `-t scripts` or the
imports fail). That step is deliberately **not** `continue-on-error`: a broken tool makes everything it
then says about the profiles worthless. Every check after it is `continue-on-error` with a final gate,
so one run reports all five results. On failure a second workflow posts or replaces a single PR comment
marked `<!-- profile-validation-comment -->`, holding the start of each failing log (30 KB per check,
12 KB per `validate_custom` fixture; reproduce locally for the full list); it deletes the comment once
the run is green.
The job name is also the required check for the delegated-merge bot, which lets a vendor maintainer
self-merge a PR limited to their own `resources/profiles/<Vendor>/` folder with no human review, so
whatever CI does not check is what ships unreviewed. Its denied patterns refuse `^scripts/` and any
`.py`, so a PR that touches the tooling always needs a maintainer.
@@ -1,214 +0,0 @@
# Vendor bundles
A bundle is `resources/profiles/<Vendor>.json` (the index) plus `resources/profiles/<Vendor>/`. The
**vendor id is the filename stem**, not the `name` inside; the two may differ (`BBL.json` is named
"Bambulab"). Asset paths and the `setting_id` formula use the id; the `validate_custom` fixture prefix
uses the `name`.
## The index
```json
{
"name": "Phrozen",
"version": "02.04.00.03",
"force_update": "0",
"description": "Phrozen configurations",
"machine_model_list": [ { "name": "Phrozen Arco", "sub_path": "machine/Phrozen Arco.json" } ],
"machine_list": [ … ],
"process_list": [ … ],
"filament_list": [ … ]
}
```
The loader reads `name`, `version`, `url` and the four `*_list` arrays. `description` is only logged;
`force_update` is read by the profile updater, never by the loader. `sub_path` is relative to the
**vendor folder**.
| List | Holds |
| --- | --- |
| `machine_model_list` | `machine_model` records (the printer product) |
| `machine_list` | printer variants **and** shared machine bases |
| `process_list` | selectable processes **and** shared process bases |
| `filament_list` | selectable filaments **and** shared filament bases |
### Three registration rules
1. **Everything is registered, bases included.** Every preset file on disk has exactly one entry in the
matching list, and no unindexed preset file is left in the tree.
2. **Parents before children, includes before includers.** The lists load processes first, then
filaments, then printers, each in index order, and `inherits` and `include` resolve only against
presets of that type already loaded from it. A parent
listed after its child produces `can not find inherits <parent> for <child>` and the bundle is
discarded; an include listed after its includer is `can not find include`, a counted error that
leaves the includer without those keys.
3. **The index entry's `name` equals the `name` inside the `sub_path` file.** `renamed_from` does not
excuse a mismatch.
All three are `check` errors, and `update-index` writes an index that satisfies all three from the
files on disk, including the dependency ordering (parents and templates before the presets that use
them, then, in name order, entries that neither depend on nor are depended on by another entry of their own
list, and any entry on a dependency cycle). Hand-editing the index is
fine for a one-line addition, but the committed result must equal what `update-index` writes, because
`check` compares them.
The loader itself reports none of this: an unregistered file, or an entry with a misspelled key
(`"subpath"`), is silently dropped. (A misspelled `sub_path` value is a `check` error naming the entry.)
`BBL/cli_config.json` and, in `BBL/filament/`, `filaments_color_codes.json`, `filament_id_map.json`,
`filament_name_map.json` and `support_recommended_params.json` are auxiliary data files read by path,
not presets. The last three carry a `type` key and look like presets; the tool excludes all five from
preset maintenance.
## `version`
Four components, `MM.mm.pp.bb`, compared as a version number in which the fourth is folded into the
third (`patch × 100 + build`). Write all four components, zero-padded.
- **Bump the version for every bundle the change touches.** The app installs bundled profiles only
when their version is newer than the installed one, and the `.opc` preset cache is also keyed on
the version. Nothing in profile CI checks the bump.
- **Keep the last component ≤ 99.** `02.04.00.100` and `02.04.01.00` both read as
`2.4.100`. A bundle that
reaches `.99` carries into the third component (`02.03.02.99` → `02.03.03.00`).
- An **absent** version is worse than a stale one: it reads as `0.0.0`, which is not a valid version.
`check` and the validator still pass, but the vendor is dropped from the setup wizard entirely and gets no preset cache. Confirm the key exists. An
*unparseable* version is not silent: it discards the whole bundle (`vendor <V>'s config version: <s>
invalid`).
## Common preset keys
| Key | Value |
| --- | --- |
| `type` | `machine_model`, `machine`, `process` or `filament` |
| `name` | the preset name, the identity every reference uses; the filename is *not* authoritative |
| `inherits` | the parent's exact `name`: no path, no `.json` |
| `include` | a template's exact `name`, or an array of them, layered under this preset's own keys ([below](#inherits-and-include)) |
| `instantiation` | the **string** `"true"` (selectable) or `"false"` (base) |
| `from` | `"system"` for shipped presets |
| `setting_id` | generated; required on instantiated presets, forbidden on bases |
| `renamed_from` | `;`-separated old names this preset supersedes ([below](#renamed_from)) |
These are config-preset keys; `machine_model` records have their own
[key set](machine-profiles.md#machine_model-a-record-not-a-config-preset). Keep `from` as `"system"`:
the bundle loader ignores it, but loading the file as a CLI config accepts only `system`, `user` or
`User` and handles their inheritance differently.
`instantiation` is the one metadata key the validator gates: a missing key or any value other than the
strings `"true"` / `"false"` is a counted error (`Missing instantiation attribute for <name>`) that fails
the validator, though the preset still loads and is treated as selectable. A file with no
`instantiation` whose name contains `gcode`, or that has no `name`, silently becomes an include-only
template. A JSON boolean `true` fails harder: it takes the **whole vendor bundle** down.
## `inherits` and `include`
`inherits` resolves by exact name **within the same bundle**, plus one exception: filaments may inherit
from OrcaFilamentLibrary, which is loaded first. Vendor-to-vendor inheritance always fails, and an
unresolved `inherits` discards the bundle. You can inherit from an instantiated preset as well as from a
base.
`"include": ["<name>", …]` (or one bare name) pulls in `instantiation: "false"` presets of the same type
from the same bundle (never the library), registered before the includer. It shares a block of keys
between presets that do not share a parent: a variant layout, a G-code template. Only `"false"` presets
can be included, so a name that resolves to nothing (misspelled, registered after the includer, or a
selectable preset) is a counted error (`can not find include`) and the preset loads without it.
**How a preset's config is composed:** start from the parent's stored config (a root starts from the
built-in defaults), apply each preset named in `include` in the order listed, then the preset's own
keys. Later layers win, so precedence is own keys > later includes > earlier includes > the `inherits`
chain. Only then is every variant key of the composed config resized to its variant length
([widths](extruder-variants.md#widths)), and keys of another preset type removed. The two routes hand
down different widths:
- **`inherits` hands down the resized config.** A base is stored *after* its own resize, at the length
of its own `*_extruder_variant` (one variant for a base of any type that writes none, whatever its
extruder count). A child therefore inherits the base's arrays at the base's width: an array wider than
that is cut to its first values before any child sees it, and a child that adds variants gets those
first values padded. So widen an array only on a preset whose own variant list already has the
entries; a wide array on a narrow base is silently lost at load, and `check`, which composes at the
width each file wrote and judges selectable presets only, misses that cut.
- **`include` hands down the template's diff, at its pre-resize width.** An included preset contributes
every key where its own composed config (its parent, its own includes and its own keys) differs from
the built-in defaults, taken *before* its resize. So keys the template inherits are passed on too,
arrays it writes arrive at the width its file wrote, and a key it sets to the built-in default value
is not passed on at all, so it cannot override what the includer inherited. Resizing happens on the
includer, not on the template.
## `renamed_from`
One JSON string, `;`-separated for several old names.
- Write `"A;B"`, never `"A ; B"`: a space after a `;` is skipped, but a space before it stays part of
the name (`"A "`), which can never match.
- When `renamed_from` is **absent** and the name contains `@`, the loader auto-adds the `@`-removed form
(`X @Y` → `X Y`) as a rename alias. Declaring an explicit `renamed_from` **suppresses** that, so a
preset that needs both the `@`-removed form and a real old name must list both; a preset that gains
a `renamed_from` without it quietly loses its `X Y` alias.
- It rescues names stored **outside** the tree: user presets and 3MF projects. It does **not** rescue
in-tree `inherits` (exact lookup), it does **not** satisfy the index-name rule, the validator reports
an in-tree reference that only resolves through it (`references renamed compatible_printers "OLD"
(now "NEW")`), and `machine_model` records never read it at all.
- Only one preset may claim a given old name; two that do is a counted error
(`… was marked as renamed from "Y" … as well`). But the redirect is **inert while a live preset still
carries that name**, and nothing checks *that*, so a neighbour's `renamed_from` is no model.
## Failure scopes
| Scope | Cause |
| --- | --- |
| **Every vendor except OrcaFilamentLibrary, and all user presets** | a non-string where the index or a `machine_model` expects a string (`"version": 2` at the top level of an index, a numeric `name` or `url`, a non-string `nozzle_diameter` or other model key): `[json.exception.type_error.302] type must be string`, and the validator reports `Validation failed` |
| **The whole vendor bundle** | index JSON parse error; unparseable `version`; a listed file missing or unparseable; a value its option cannot take, such as `nil` in a non-nullable key (`Failed loading configuration file`); unresolved `inherits`; two selectable presets with one name; empty or unknown `printer_model` / `printer_variant`; a filament resolving no `filament_id`; a JSON boolean `instantiation` |
| **A counted error; the preset still loads** | `instantiation` missing or not `"true"` / `"false"`; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for <key>`); an `include` naming nothing usable (`can not find include`, loads without it) |
| **The rest of the file, logged only** | an array with a non-string element (`[0.4]`, `invalid json array`): that key and every key after it in the file are dropped, and no error is counted |
| **One value, logged only** | a raw JSON number in a preset (`invalid json type for <key>`): the value is dropped and the exit code stays 0 |
| **Nothing reported by the loader** | unregistered file; two bases with one name, or a base and a selectable preset with one name (the first in the index wins); misspelled setting key; missing bed, hotend or cover asset. `check` catches the first two; the others reach users |
Deleting a file the index still lists surfaces as a *parse error* on line 1 (`unexpected end of input`), not "file
not found".
Selectable preset names are a **single namespace across every vendor**: a duplicate within one vendor
is a hard bundle failure, and a duplicate across vendors is reported as `Found duplicated preset: <name>
in vendor: <vendor>` and still counts as an error. `check` catches the within-bundle case earlier and
more precisely, bases included, and including an *unindexed* twin, which is one `sub_path` edit away
from silently becoming the parent every child resolves to (the first registered preset of a name wins,
so index order decides). Base names, by contrast, repeat across bundles by design:
every bundle may have its own `fdm_process_common` ([uniqueness](naming.md#uniqueness)).
## Starting a whole new vendor bundle
Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab`** is the
minimal shape: a shared machine base, the model, one variant, a shared process base, two processes, and
an empty `filament_list`, so the printer takes the library generics. Do *not* start from a bundle that
carries local `fdm_filament_*` copies, which drift from the library, or filament presets that restate
most of their parent, the style this skill advises against.
Write the machine files **last**, so you only visit them once:
1. **Choose the names first**: model, variant(s), process(es). Everything else references them
([naming.md](naming.md)).
2. `resources/profiles/<Vendor>.json`: `name`, `version` (`01.00.00.00`), `force_update: "0"`,
`description`, and all four `*_list` arrays (empty is fine: `update-index` fills them once the files
exist, so this step only needs the bundle metadata to be right).
3. The shared bases: `<Vendor>/machine/fdm_machine_common.json` and
`<Vendor>/process/fdm_process_common.json`, both `"instantiation": "false"` with no `setting_id`. For
a Klipper printer add your own `<Vendor>/machine/fdm_klipper_common.json` inheriting the machine
base; there is no shared one, because a `machine` preset can only inherit inside its own bundle.
4. One selectable process per variant, each naming its variant in `compatible_printers`.
5. Bed assets and `<Model>_cover.png`, all directly in `<Vendor>/`. None of them is needed for the
bundle to load, and nothing in CI checks them; but the bed files are inert unless the
`machine_model` names them in `bed_model` / `bed_texture`, and the cover is found by convention as
`<the name you gave the model in machine_model_list>_cover.png`.
6. The `machine_model` record and the `machine` variants, now that every value they reference exists;
the minimum key sets and the `default_*` shapes are in
[machine-profiles.md](machine-profiles.md#machine-the-variant).
7. Run the tool and validate: follow
[Creating or modifying a profile](../SKILL.md#creating-or-modifying-a-profile). `generate-id` is not
optional for a new bundle: the validator loads presets that have no `setting_id`, but `check` fails
every one of them.
## `resources/profiles_template/`
A separate tree (`Template.json` + `Template/`) holding filament and process templates. It is **not** a
scaffold for shipped profiles: the app's "create a custom printer / filament" dialog reads it, so
editing it changes what users get when they create a custom preset. `check_profile.sh`'s validator
checks default to `resources/profiles` (redirectable with `-p`), and so does `orca_profile_tool.py`
(redirectable with `--profiles`); neither covers this tree.
@@ -1,241 +0,0 @@
#!/usr/bin/env python3
"""Find settings to move onto shared bases, and prove a move changed nothing.
snapshot OUT.json write every selectable preset's config as the loader stores it
compare BEFORE.json report every value that differs from the snapshot; exit 1 if any
candidates --vendor V restated values; per base, the settings its presets all share and the
defaults with exceptions that would pay
Every subcommand takes --profiles DIR (default resources/profiles).
Configs are composed the loader's way: the parent's stored config, then each include at
the width its file wrote, then the preset's own keys, and every variant key resized to the
preset's own variant list (one variant without one), padded with its first value or cut.
A base is stored after that resize, so a variant array wider than a base's list reaches its
children cut. Not modelled: the built-in defaults. A key no file in a preset's chain writes
loads its default, so compare reports a key written on one side only separately: it is no
change when the written value is the option's default in PrintConfig.cpp. Nor is it modelled
that an include template does not pass on a key equal to the default. Reads
scripts/orca_profile_tool.py.
"""
import argparse
import json
import os
import sys
from collections import Counter, defaultdict
# The profile tool lives in <repo>/scripts; this file in <repo>/.claude/skills/orca-profiles/scripts.
sys.path[:0] = [os.path.join(os.getcwd(), "scripts"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), *[os.pardir] * 4, "scripts")]
import orca_profile_tool as tool # noqa: E402
TYPES = ("machine", "process", "filament")
# Keys the loader reads from each file as metadata; neither inherits nor include passes them on.
PER_FILE = {"type", "name", "from", "instantiation", "setting_id", "renamed_from", "description",
"inherits", "include", "version", "url", "is_custom_defined"}
# Keys the app replaces with the selected presets' names before slicing: a file's value never counts.
REPLACED = {"print_settings_id", "printer_settings_id", "filament_settings_id"}
# What a restructure changes by design, or what never reaches a slice.
MOVED_BY_DESIGN = {"inherits", "include"} | REPLACED
# Keys that stay in their own file: metadata, identity, and each preset's compatibility.
NEVER_SHARED = PER_FILE | REPLACED | {"filament_id", "compatible_printers", "compatible_prints",
"printer_variant", "printer_model"}
class Tree:
def __init__(self, profiles_dir):
self.dir = profiles_dir
self.scheme = tool._variant_scheme()
self.vendors = tool.list_vendor_names(profiles_dir)
self.bundles = {v: tool.load_vendor_configs(profiles_dir, v) for v in self.vendors}
self.cache = {}
def lookup(self, vendor, ptype, name, in_ofl=False):
"""(vendor the name resolves in, (rel, data)); filaments fall back to the library."""
if not in_ofl and name in self.bundles[vendor][ptype]:
return vendor, self.bundles[vendor][ptype][name]
if ptype == "filament" and tool.OFL in self.bundles and name in self.bundles[tool.OFL][ptype]:
return tool.OFL, self.bundles[tool.OFL][ptype][name]
return None, None
def composed(self, vendor, ptype, name, drop=None, seen=frozenset()):
"""Config before the preset's own resize: parent stored, includes, own keys."""
owner, found = self.lookup(vendor, ptype, name, vendor == tool.OFL)
if found is None or (owner, name) in seen:
return {}
seen = seen | {(owner, name)}
data = found[1]
config = {}
if data.get("inherits"):
config.update(self.stored(owner, ptype, data["inherits"], seen))
include = data.get("include") or []
for included in [include] if isinstance(include, str) else include:
if included in self.bundles[owner][ptype]:
config.update(self.composed(owner, ptype, included, seen=seen))
config = {k: v for k, v in config.items() if k not in PER_FILE}
config.update((k, v) for k, v in data.items() if k != drop)
return config
def stored(self, vendor, ptype, name, seen=frozenset()):
key = (vendor, ptype, name)
if key not in self.cache:
self.cache[key] = self.resize(ptype, self.composed(vendor, ptype, name, seen=seen))
return self.cache[key]
def resize(self, ptype, config):
list_key, strides = self.scheme[ptype]
length = len(tool._as_list(config[list_key])) if list_key in config else 1
out = dict(config)
for key, stride in strides.items():
if key in out:
values = tool._as_list(out[key])
need = length * stride
out[key] = values[:need] + values[:1] * (need - len(values))
return out
def presets(self, vendors=None, ptypes=TYPES):
for vendor in vendors or self.vendors:
for ptype in ptypes:
for name, (rel, data) in sorted(self.bundles[vendor][ptype].items()):
yield vendor, ptype, name, rel, data
def snapshot(tree):
return {f"{vendor}/{ptype}/{name}": {k: v for k, v in tree.stored(vendor, ptype, name).items()
if k not in MOVED_BY_DESIGN}
for vendor, ptype, name, _rel, data in tree.presets()
if data.get("instantiation") == "true"}
def compare(before, after):
changed, one_sided = 0, []
for preset in sorted(before.keys() | after.keys()):
old, new = before.get(preset), after.get(preset)
if old is None or new is None:
print(f"{preset}: {'added' if old is None else 'removed'}")
changed += 1
continue
for key in sorted(old.keys() | new.keys()):
if key not in old or key not in new:
one_sided.append(f"{preset}: {key} "
f"{json.dumps(old[key]) if key in old else '(built-in default)'} -> "
f"{json.dumps(new[key]) if key in new else '(built-in default)'}")
elif old[key] != new[key]:
print(f"{preset}: {key} {json.dumps(old[key])} -> {json.dumps(new[key])}")
changed += 1
for line in one_sided:
print(line)
print(f"{changed} difference(s)")
if one_sided:
print(f"{len(one_sided)} key(s) written on one side only: each is a difference unless the "
f"written value is the option's default in src/libslic3r/PrintConfig.cpp")
return changed + len(one_sided)
def candidates(tree, vendor, ptypes, group_by):
for ptype in ptypes:
entries = {name: data for _v, _t, name, _rel, data in tree.presets([vendor], (ptype,))}
selectable = [n for n, d in entries.items() if d.get("instantiation") == "true"]
stored = {n: tree.stored(vendor, ptype, n) for n in entries}
list_key = tree.scheme[ptype][0]
# Restated: a key a file writes that it would inherit unchanged without writing it.
restated = {}
for name, data in entries.items():
restated[name] = sorted(
k for k in data if k not in NEVER_SHARED and k != list_key
and (data.get("inherits") or data.get("include"))
and tree.resize(ptype, tree.composed(vendor, ptype, name, drop=k)).get(k) == stored[name][k])
if restated[name]:
print(f"{vendor}/{ptype} {name}: restates what it inherits: {', '.join(restated[name])}")
# Groups: every preset with selectable presets below it, or the --group-by values.
chain = {n: [] for n in selectable}
for name in selectable:
node = entries[name].get("inherits")
while node in entries and node not in chain[name]:
chain[name].append(node)
node = entries[node].get("inherits")
groups = defaultdict(list)
for name in selectable:
if group_by:
groups[json.dumps(stored[name].get(group_by))].append(name)
else:
for base in chain[name]:
groups[base].append(name)
printed = {}
for label, members in sorted(groups.items(), key=lambda g: -len(g[1])):
if len(members) < 2 or label == "null":
continue
if frozenset(members) in printed:
print(f"\n{vendor}/{ptype} {label}: the same presets as {printed[frozenset(members)]}")
continue
common = [b for b in chain[members[0]] if all(b in chain[m] for m in members[1:])]
home = common[0] if group_by and common else None if group_by else label
def below(m):
"""m and the files between it and the group's home."""
return [m] + chain[m][:chain[m].index(home)] if home in chain[m] else [m]
shared, defaults = [], []
for key in sorted(set().union(*(entries[m].keys() for m in members)) - NEVER_SHARED):
loaded = [json.dumps(stored[m].get(key)) for m in members]
counts = Counter(loaded).most_common(2)
if len(counts) == 1:
writers = sum(key in entries[m] and key not in restated[m] for m in members)
if writers >= 2:
shared.append(f"{key} ({writers} write it)")
continue
(top, held), (_, runner_up) = counts
if held == runner_up or top == "null" or home is None:
continue
# Balance 5: w presets drop their copy; a presets that take the key from the home
# (no file on their way to it writes it) and load another value must write theirs.
w = sum(key in entries[m] and v == top for m, v in zip(members, loaded))
a = sum(v != top and not any(key in entries[f] for f in below(m))
for m, v in zip(members, loaded))
if w - a > 1:
shown = top if len(top) <= 40 else top[:37] + "..."
defaults.append(f"{key} = {shown}: {held} load it, {w} write it, "
f"{a} would have to write their own")
if shared or defaults:
where = f"; nearest common base {common[0]}" if group_by and common else ""
title = f"{group_by} = {label}" if group_by else label
print(f"\n{vendor}/{ptype} {title}: {len(members)} presets{where}")
printed[frozenset(members)] = title
for line in shared:
print(f" {line}")
if defaults:
print(" default with exceptions:")
for line in defaults:
print(f" {line}")
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
profiles = argparse.ArgumentParser(add_help=False)
profiles.add_argument("--profiles", default=os.path.join("resources", "profiles"),
help="profiles directory (default: resources/profiles)")
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("snapshot", parents=[profiles]).add_argument("out")
sub.add_parser("compare", parents=[profiles]).add_argument("before")
cand = sub.add_parser("candidates", parents=[profiles])
cand.add_argument("--vendor", required=True)
cand.add_argument("--type", choices=TYPES, action="append")
cand.add_argument("--group-by", help="group selectable presets by this key's value instead of by "
"base: printer_model, gcode_flavor, extruder_type, filament_id, layer_height, ...")
args = parser.parse_args()
tree = Tree(args.profiles)
if args.command == "snapshot":
presets = snapshot(tree)
with open(args.out, "w", encoding="utf-8") as f:
json.dump(presets, f, sort_keys=True)
print(f"{len(presets)} selectable presets written to {args.out}")
elif args.command == "compare":
with open(args.before, encoding="utf-8") as f:
sys.exit(1 if compare(json.load(f), snapshot(tree)) else 0)
else:
candidates(tree, args.vendor, args.type or TYPES, args.group_by)
if __name__ == "__main__":
main()
+8 -75
View File
@@ -14,9 +14,6 @@ on:
- 'localization/**' - 'localization/**'
- 'resources/**' - 'resources/**'
- ".github/workflows/build_*.yml" - ".github/workflows/build_*.yml"
- ".github/workflows/unit_tests*.yml"
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'scripts/build_preset_cache.*' - 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**' - 'scripts/flatpak/**'
- 'scripts/msix/**' - 'scripts/msix/**'
@@ -33,8 +30,9 @@ on:
- '**/CMakeLists.txt' - '**/CMakeLists.txt'
- 'version.inc' - 'version.inc'
- ".github/workflows/build_*.yml" - ".github/workflows/build_*.yml"
- ".github/workflows/unit_tests*.yml"
- 'build_linux.sh' - 'build_linux.sh'
- 'build_release_vs.bat'
- 'build_release_vs2022.bat'
- 'build_win.bat' - 'build_win.bat'
- 'scripts/test_build_win.ps1' - 'scripts/test_build_win.ps1'
- 'build_release_macos.sh' - 'build_release_macos.sh'
@@ -183,11 +181,9 @@ jobs:
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }} os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
artifact: ${{ github.sha }}-tests-macos-arm64 artifact: ${{ github.sha }}-tests-macos-arm64
test-dir: build/arm64/tests test-dir: build/arm64/tests
# Slice a two-colour cube through every shipped printer, and through every # Slice a two-colour cube through every shipped printer so all custom g-code
# system process/filament whose templates no printer's own slice reaches, so # (change_filament_gcode, machine start/end, etc.) is expanded - catches
# every custom g-code and filename_format shipped is expanded (names in {if} # slicing regressions the static profile checks and unit tests can't see.
# branches not taken included) - catches slicing regressions the static
# profile checks and unit tests can't see.
# Profile-only PRs are covered by check_profiles.yml's nightly binary; this # Profile-only PRs are covered by check_profiles.yml's nightly binary; this
# covers src/engine PRs with the PR-built binary. # covers src/engine PRs with the PR-built binary.
slice_check_linux: slice_check_linux:
@@ -211,7 +207,7 @@ jobs:
./validator-bin/OrcaSlicer_profile_validator -p "${{ github.workspace }}/resources/profiles" -s -l 2 ./validator-bin/OrcaSlicer_profile_validator -p "${{ github.workspace }}/resources/profiles" -s -l 2
publish_test_results: publish_test_results:
name: Publish Test Results name: Publish Test Results
needs: [unit_tests_linux_x86_64, unit_tests_linux_aarch64, unit_tests_windows_x64, unit_tests_windows_arm64, unit_tests_macos_arm64, unit_tests_flatpak_x86_64, unit_tests_flatpak_aarch64] needs: [unit_tests_linux_x86_64, unit_tests_linux_aarch64, unit_tests_windows_x64, unit_tests_windows_arm64, unit_tests_macos_arm64]
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -328,17 +324,9 @@ jobs:
sed -i '/^build-options:/a\ no-debuginfo: true\n strip: true' \ sed -i '/^build-options:/a\ no-debuginfo: true\n strip: true' \
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
shell: bash shell: bash
# flatpak-builder reuses a module from its cache when the definition and - name: Inject git commit hash into Flatpak manifest
# sources are unchanged, so a re-run of the same commit would skip the
# OrcaSlicer module and ship no test asset. A per-run value in that module's
# env keeps it rebuilding; orca_deps stays cached, and the compiler cache
# still serves the rebuild. run-tests on the same module builds the test
# binaries.
- name: Inject commit hash, run-tests and cache buster into Flatpak manifest
env:
flatpak_builder_cache_buster: ${{ github.run_id }}-${{ github.run_attempt }}
run: | run: |
sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n run-tests: true\n build-options:\n env:\n flatpak_builder_cache_buster: \"$flatpak_builder_cache_buster\"\n git_commit_hash: \"$git_commit_hash\"|}" \ sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n git_commit_hash: \"$git_commit_hash\"|}" \
scripts/flatpak/com.orcaslicer.OrcaSlicer.yml scripts/flatpak/com.orcaslicer.OrcaSlicer.yml
shell: bash shell: bash
# flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds # flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds
@@ -384,7 +372,6 @@ jobs:
save-cache: false save-cache: false
arch: ${{ matrix.variant.arch }} arch: ${{ matrix.variant.arch }}
upload-artifact: false upload-artifact: false
keep-build-dirs: true
# The build has just touched everything it can use, so an object untouched # The build has just touched everything it can use, so an object untouched
# for a week is dead, usually orphaned by a flag change. # for a week is dead, usually orphaned by a flag change.
- name: Compiler cache statistics - name: Compiler cache statistics
@@ -415,14 +402,6 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches" api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches"
# The save step reports success even when its tar failed, so keep
# the older entries unless the new one is listed.
if ! curl -sSf -H "Authorization: Bearer $GH_TOKEN" \
"$api?ref=$GITHUB_REF&key=$CCACHE_ENTRY" \
| jq -e --arg entry "$CCACHE_ENTRY" 'any(.actions_caches[]; .key == $entry)' > /dev/null; then
echo "$CCACHE_ENTRY was not saved; keeping the older entries."
exit 0
fi
curl -sSf -H "Authorization: Bearer $GH_TOKEN" \ curl -sSf -H "Authorization: Bearer $GH_TOKEN" \
"$api?ref=$GITHUB_REF&key=ccache-$CCACHE_LEG-&per_page=100" \ "$api?ref=$GITHUB_REF&key=ccache-$CCACHE_LEG-&per_page=100" \
| jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \ | jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \
@@ -446,49 +425,3 @@ jobs:
asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
# The asset is /app (the exes link it at runtime) plus the build tree
# slimmed to what ctest needs.
- name: Package flatpak test asset
shell: bash
run: |
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* | tail -1)
find "$d/build_flatpak" -mindepth 1 -maxdepth 1 ! -name tests -exec rm -rf {} +
# Strip debug info (the SDK builds with -g, only the app gets stripped);
# the bounds checks are compiled in, so a stripped exe still catches them.
find "$d/build_flatpak/tests" -type f -perm -u+x -exec strip --strip-unneeded {} + 2>/dev/null || true
# At runtime the tests read tests/ (TEST_DATA_DIR), scripts/, and under
# resources/ the shipped profiles (PROFILES_DIR), the printers/ maps, and
# the icon SVGs (NativeCommands icon names are checked against them).
find "$d" -mindepth 1 -maxdepth 1 -type d \
! -name tests ! -name build_flatpak ! -name scripts ! -name resources -exec rm -rf {} +
find "$d/resources" -mindepth 1 -maxdepth 1 ! -name profiles ! -name printers ! -name images -exec rm -rf {} +
# Only the SVGs are read; the png/ico/icns/gif assets are ~35MB of dead weight.
find "$d/resources/images" -mindepth 1 -maxdepth 1 ! -name '*.svg' -exec rm -rf {} + 2>/dev/null || true
tar -cf flatpak-test-asset.tar flatpak_app "$d"
- name: Upload flatpak test asset
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}-flatpak-tests-${{ matrix.variant.arch }}
path: flatpak-test-asset.tar
retention-days: 1
# keep-build-dirs would otherwise land in the flatpak-builder cache saved post-job.
- name: Drop the kept build dirs before the flatpak-builder cache saves
if: always()
shell: bash
run: rm -rf .flatpak-builder/build
unit_tests_flatpak_x86_64:
name: Flatpak x86_64
needs: flatpak
if: ${{ !cancelled() && success() }}
uses: ./.github/workflows/unit_tests_flatpak.yml
with:
os: ubuntu-24.04
artifact: ${{ github.sha }}-flatpak-tests-x86_64
unit_tests_flatpak_aarch64:
name: Flatpak aarch64
needs: flatpak
if: ${{ !cancelled() && success() }}
uses: ./.github/workflows/unit_tests_flatpak.yml
with:
os: ubuntu-24.04-arm
artifact: ${{ github.sha }}-flatpak-tests-aarch64
+1 -2
View File
@@ -41,8 +41,7 @@ jobs:
# restores one it cannot use. Linux amd64 passes no arch deliberately, so # restores one it cannot use. Linux amd64 passes no arch deliberately, so
# 'linux-clang' keeps the cache it already has. # 'linux-clang' keeps the cache it already has.
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }} cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
# The Windows ARM64 deps build in build-arm64, all others under build; # ARM64 builds use the build-arm64 tree (see build_release_vs.bat); x64/other use build.
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }} dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}} output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}}
run: | run: |
+19 -5
View File
@@ -138,11 +138,25 @@ jobs:
if (-not "${{ vars.SELF_HOSTED }}") { if (-not "${{ vars.SELF_HOSTED }}") {
choco install strawberryperl choco install strawberryperl
} }
# cache-path is the install directory inside the deps build directory. $arch = "${{ inputs.arch }}"
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/') # -l selects clang-cl and -x Ninja; together they build the deps with clang.
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator. $clang = "${{ inputs.compiler }}" -eq "clang"
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" } $flags = if ($clang) { "-l", "-x" } else { @() }
.\build_win.bat -d --arch ${{ inputs.arch }} --deps-dir $deps @flags if ($clang) {
# OpenSSL builds with nmake, which needs a VC environment.
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$vs = & $vswhere -latest -property installationPath
$devArch = if ($arch -eq "arm64") { "arm64" } else { "amd64" }
Import-Module "$vs\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
Enter-VsDevShell -VsInstallPath $vs -SkipAutomaticLocation -DevCmdArguments "-arch=$devArch"
}
if ($arch -eq "arm64") {
.\build_release_vs.bat deps arm64 @flags
.\build_release_vs.bat pack arm64
} else {
.\build_release_vs.bat deps @flags
.\build_release_vs.bat pack
}
shell: pwsh shell: pwsh
- name: Build on Mac ${{ inputs.arch }} - name: Build on Mac ${{ inputs.arch }}
+15 -63
View File
@@ -85,15 +85,6 @@ jobs:
shell: bash shell: bash
run: | run: |
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}" leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
# clang-cl refuses a precompiled header from another cl.exe build and ccache
# does not hash that build, so each one gets its own cache. The build number
# is read from cl.exe itself; the toolset directory keeps its name across patches.
if [ "${{ runner.os }}" = Windows ]; then
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
fi
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV" echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV" echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
@@ -448,26 +439,9 @@ jobs:
- name: Install nsis - name: Install nsis
if: runner.os == 'Windows' && !vars.SELF_HOSTED if: runner.os == 'Windows' && !vars.SELF_HOSTED
shell: pwsh
# The Chocolatey community feed intermittently 504s, and `choco install`
# exits 0 when package resolution fails that way. Unchecked, the job then
# builds for ~25 minutes before `cpack -G NSIS` reports a missing makensis.
# Retry the install and verify makensis itself, so a real failure stops here.
run: | run: |
dir "C:/Program Files (x86)/Windows Kits/10/Include" dir "C:/Program Files (x86)/Windows Kits/10/Include"
$nsisDir = Join-Path ${env:ProgramFiles(x86)} 'NSIS' choco install nsis
$makensis = Join-Path $nsisDir 'makensis.exe'
for ($attempt = 1; $attempt -le 3 -and -not (Test-Path $makensis); $attempt++) {
if ($attempt -gt 1) { Start-Sleep -Seconds (15 * $attempt) }
Write-Host "::group::choco install nsis (attempt $attempt)"
choco install nsis --yes --no-progress
Write-Host "::endgroup::"
}
if (-not (Test-Path $makensis)) {
throw "NSIS install failed: $makensis not found after 3 attempts."
}
& $makensis /VERSION
$nsisDir | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
- name: Build slicer Win - name: Build slicer Win
if: runner.os == 'Windows' if: runner.os == 'Windows'
@@ -476,13 +450,21 @@ jobs:
# env: # env:
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\' # WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
# WindowsSDKVersion: '10.0.26100.0\' # WindowsSDKVersion: '10.0.26100.0\'
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them. # "tests" builds the unit tests too; the unit_tests_windows_* jobs run them.
run: | run: |
# cache-path is the install directory inside the deps build directory. $arch = "${{ inputs.arch }}"
$deps = (Split-Path "${{ inputs.cache-path }}").Replace('\', '/') # -l selects clang-cl and -x Ninja; together they build the slicer with clang.
# -l compiles with Visual Studio's clang-cl and -x builds with Ninja; --msvc --msbuild is cl under the Visual Studio generator. $clang = "${{ inputs.compiler }}" -eq "clang"
$flags = if ("${{ inputs.compiler }}" -eq "clang") { "-l", "-x" } else { "--msvc", "--msbuild" } $flags = if ($clang) { "-l", "-x" } else { @() }
.\build_win.bat -s --tests -i --arch ${{ inputs.arch }} --build-dir $env:BUILD_DIR --deps-dir $deps @flags if ($clang) {
# Build against the same VC toolchain and SDK as the dependencies.
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$vs = & $vswhere -latest -property installationPath
$devArch = if ($arch -eq "arm64") { "arm64" } else { "amd64" }
Import-Module "$vs\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
Enter-VsDevShell -VsInstallPath $vs -SkipAutomaticLocation -DevCmdArguments "-arch=$devArch"
}
if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 @flags tests } else { .\build_release_vs.bat slicer @flags tests }
shell: pwsh shell: pwsh
- name: Build system preset cache (Windows) - name: Build system preset cache (Windows)
@@ -702,18 +684,6 @@ jobs:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/Release/OrcaSlicer_profile_validator' path: './build/src/Release/OrcaSlicer_profile_validator'
# generate_system_cache is what scripts/build_preset_cache.sh bakes the
# <vendor>.opc caches with; it was already built by the "Build system
# preset cache (Linux)" step above. The .opc format is 64-bit
# little-endian native, i.e. identical across every platform Orca ships,
# so only the Linux binary is published.
- name: Upload generate_system_cache Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: actions/upload-artifact@v7
with:
name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
@@ -744,17 +714,6 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
max_releases: 1 max_releases: 1
- name: Deploy Ubuntu generate_system_cache release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/src/dev-utils/Release/generate_system_cache
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
asset_content_type: application/octet-stream
max_releases: 1
- name: Deploy orca_custom_preset_tests - name: Deploy orca_custom_preset_tests
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
@@ -798,13 +757,6 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
# The save step reports success even when its tar failed, so keep
# the older entries unless the new one is listed.
if ! gh cache list --ref "$GITHUB_REF" --key "$CCACHE_ENTRY" --json key \
| jq -e --arg entry "$CCACHE_ENTRY" 'any(.[]; .key == $entry)' > /dev/null; then
echo "$CCACHE_ENTRY was not saved; keeping the older entries."
exit 0
fi
gh cache list --ref "$GITHUB_REF" --key "ccache-$CCACHE_LEG-" --limit 100 --json id,key \ gh cache list --ref "$GITHUB_REF" --key "ccache-$CCACHE_LEG-" --limit 100 --json id,key \
| jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \ | jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \
'.[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \ '.[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \
+16 -34
View File
@@ -9,14 +9,10 @@ on:
- release/* - release/*
paths: paths:
- 'resources/profiles/**' - 'resources/profiles/**'
# orca_profile_tool.py also validates resources/printers/bambu_filament_ids.json, and # The extra JSON check also validates resources/printers/bambu_filament_ids.json,
# both it and its tests live in scripts/, so a PR touching only those must still run # and lives in scripts/, so a PR touching only those must still run this workflow.
# this workflow.
- 'resources/printers/**' - 'resources/printers/**'
- 'scripts/**' - 'scripts/**'
# orca_profile_tool.py reads the variant key sets from PrintConfig.cpp, and its
# tests the obsolete keys, so a PR changing either must be checked against the profiles.
- 'src/libslic3r/PrintConfig.cpp'
- ".github/workflows/check_profiles.yml" - ".github/workflows/check_profiles.yml"
workflow_dispatch: workflow_dispatch:
@@ -40,23 +36,12 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
# Deliberately not continue-on-error, unlike every check below: if the tool itself is - name: Run extra JSON check
# broken, nothing it then reports about the profiles is worth reading. id: extra_json_check
- name: Run the profile tool's own unit tests
run: python3 -m unittest discover -s scripts/tests -t scripts
# What the validator below cannot see. It loads the tree the way the slicer does, so
# it never notices a profile no <vendor>.json indexes, a preset name two files claim,
# an id that is not the mint of its own triple, or a file that normalize and
# update-index would still rewrite.
# The step id is the handle the PR comment and the failure gate below use; renaming it
# silently disables them.
- name: Check profiles (orca_profile_tool.py)
id: profile_tool
continue-on-error: true continue-on-error: true
run: | run: |
set +e set +e
python3 ./scripts/orca_profile_tool.py check 2>&1 | tee ${{ runner.temp }}/profile_tool.log python3 ./scripts/orca_extra_profile_check.py 2>&1 | tee ${{ runner.temp }}/extra_json_check.log
exit ${PIPESTATUS[0]} exit ${PIPESTATUS[0]}
# download # download
@@ -74,10 +59,8 @@ jobs:
set +e set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log ./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]} exit ${PIPESTATUS[0]}
# Slice a two-colour cube through every printer, and through every system process/filament whose # Slice a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode)
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is # is expanded - catches undefined-placeholder / invalid-flow bugs the static checks above cannot see.
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
# invalid-flow bugs the static checks above cannot see.
- name: validate slice (expand custom g-code) - name: validate slice (expand custom g-code)
id: validate_slice id: validate_slice
continue-on-error: true continue-on-error: true
@@ -85,8 +68,8 @@ jobs:
set +e set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -s -l 2 2>&1 | tee ${{ runner.temp }}/validate_slice.log ./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -s -l 2 2>&1 | tee ${{ runner.temp }}/validate_slice.log
exit ${PIPESTATUS[0]} exit ${PIPESTATUS[0]}
# All vendors' filament_id collisions were fixed, so the duplicate-filament-subtype # All vendors' filament_id collisions were fixed (see scripts/filament_id_snapshot.json),
# check runs tree-wide. # so the duplicate-filament-subtype check runs tree-wide.
- name: validate filament subtype check - name: validate filament subtype check
id: validate_filament_subtypes id: validate_filament_subtypes
continue-on-error: true continue-on-error: true
@@ -203,7 +186,7 @@ jobs:
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
- name: Prepare comment artifact - name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} if: ${{ always() && github.event_name == 'pull_request' && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: | run: |
{ {
# Marker matched by check_profiles_comment.yml to delete prior comments. # Marker matched by check_profiles_comment.yml to delete prior comments.
@@ -211,11 +194,11 @@ jobs:
echo "## :x: Profile Validation Errors" echo "## :x: Profile Validation Errors"
echo "" echo ""
if [ "${{ steps.profile_tool.outcome }}" = "failure" ]; then if [ "${{ steps.extra_json_check.outcome }}" = "failure" ]; then
echo "### Profile Check Failed (orca_profile_tool.py)" echo "### Extra JSON Check Failed"
echo "" echo ""
echo '```' echo '```'
head -c 30000 ${{ runner.temp }}/profile_tool.log || echo "No output captured" head -c 30000 ${{ runner.temp }}/extra_json_check.log || echo "No output captured"
echo '```' echo '```'
echo "" echo ""
fi fi
@@ -257,7 +240,7 @@ jobs:
fi fi
echo "---" echo "---"
echo '*Fix the errors above and push a new commit. To reproduce this run locally: `scripts/check_profile.sh`, or `scripts\check_profile.bat` on Windows.*' echo "*Please fix the above errors and push a new commit.*"
} > ${{ runner.temp }}/profile-check-results/pr_comment.md } > ${{ runner.temp }}/profile-check-results/pr_comment.md
- name: Upload comment artifact - name: Upload comment artifact
@@ -269,8 +252,7 @@ jobs:
retention-days: 1 retention-days: 1
- name: Fail if any check failed - name: Fail if any check failed
if: ${{ always() && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} if: ${{ always() && (steps.extra_json_check.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: | run: |
echo "One or more profile checks failed; see the step logs above." echo "One or more profile checks failed. See above for details."
echo 'Reproduce the whole run locally with scripts/check_profile.sh (scripts\check_profile.bat on Windows).'
exit 1 exit 1
-199
View File
@@ -1,199 +0,0 @@
name: Daily OFL OTA Update
run-name: Daily OFL OTA Update [OFL barrier]
# This workflow is intended for creating and publishing the OrcaFilamentLibrary (OFL) OPC package to
# https://github.com/OrcaSlicer/orcaslicer-profiles, which generates an OTA update.
# This cronjob runs daily at 00:00 UTC every day and scans main plus every release/vX.Y.Z branch for
# changes to resources/profiles/OrcaFilamentLibrary since that branch's own last successful run. Any
# branch with no changes is skipped; each changed branch gets its own post_merge_profiles.yml dispatch.
#
# OFL has no dedicated FOLDER_MERGERS grant (it isn't merged through the PR merge-bot delegation
# scheme), so post_merge_profiles.yml is dispatched with an explicit `vendor` input, which that
# workflow trusts and uses to bypass the FOLDER_MERGERS check for this trigger. That same explicit-
# vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after
# uploading - see post_merge_profiles.yml for both sides of that contract.
#
# Each run captures a timestamp, dispatches the needed OFL publishers, waits for
# all of them to finish, then clears the pending-publish table once. Changes merged
# after that timestamp remain pending for the next run.
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
actions: write # list this workflow's past runs and dispatch post_merge_profiles.yml
contents: read
env:
VENDOR: OrcaFilamentLibrary
jobs:
daily-job:
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
runs-on: ubuntu-24.04
steps:
- name: Capture start timestamp
id: start
shell: bash
run: |
set -euo pipefail
timestamp="$(date -u +%s)"
echo "timestamp=$timestamp" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v7
with:
# Full history: the per-branch "since last successful run" check below
# needs to look arbitrarily far back if a prior run failed or was skipped.
fetch-depth: 0
- name: Fetch all branches
shell: bash
run: git fetch origin '+refs/heads/*:refs/remotes/origin/*'
- name: Scan branches and publish changed OFL profiles
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCAN_UNTIL: ${{ steps.start.outputs.timestamp }}
run: |
set -euo pipefail
mapfile -t branches < <(
gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \
| grep -E '^(main|release/v[0-9]+\.[0-9]+\.[0-9]+)$' | sort -u
)
# The cron run is the checkpoint: a successful run means every
# dispatched branch publisher completed and the pending queue was
# cleared. Manual or push-triggered post_merge_profiles runs are not
# checkpoints for this scan.
successful_cron_runs="$(gh api --method GET \
"repos/${{ github.repository }}/actions/workflows/ofl-ota-cronjob.yml/runs" \
-f status=success -f branch=main -f per_page=100 --paginate \
--jq '.workflow_runs[] | select((.display_title // "") | contains("[OFL barrier]"))')"
since="$(jq -rs 'sort_by(.run_started_at) | last.run_started_at // empty' <<< "$successful_cron_runs")"
for branch in "${branches[@]}"; do
echo "::group::$branch"
if [ -z "$since" ]; then
echo "No prior successful OFL cron run; checking $branch through $SCAN_UNTIL."
changed_files="$(git log --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
| sed '/^$/d')"
else
changed_files="$(git log --since="$since" --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
| sed '/^$/d')"
fi
if [ -n "$changed_files" ]; then
echo "OFL changed on $branch from ${since:-the beginning} through $SCAN_UNTIL:"
echo "$changed_files"
changed=true
else
echo "No OFL changes on $branch through $SCAN_UNTIL."
changed=false
fi
if [ "$changed" = true ]; then
dispatch_id="${GITHUB_RUN_ID}-${branch//\//-}"
# Record successful dispatches for the barrier step below.
# Branches whose workflow predates workflow_dispatch are skipped
# with a warning, as they were before the barrier was added.
if gh workflow run post_merge_profiles.yml \
--repo "${{ github.repository }}" \
--ref "$branch" \
-f vendor="$VENDOR" -f auto_publish=true \
-f ofl_cron_dispatch_id="$dispatch_id"; then
printf '%s\t%s\n' "$branch" "$dispatch_id" >> "$RUNNER_TEMP/ofl-dispatches.tsv"
else
echo "::warning::skipping $branch because post_merge_profiles.yml could not be dispatched at that ref"
fi
fi
echo "::endgroup::"
done
- name: Wait for OFL publishers
id: wait
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DISPATCHES_FILE: ${{ runner.temp }}/ofl-dispatches.tsv
run: |
set -euo pipefail
if [ ! -s "$DISPATCHES_FILE" ]; then
echo "No OFL publisher workflows were dispatched; pending queue will not be cleared."
echo "publishers_dispatched=false" >> "$GITHUB_OUTPUT"
exit 0
fi
: > "$RUNNER_TEMP/ofl-run-ids.tsv"
while IFS=$'\t' read -r branch dispatch_id; do
[ -n "$branch" ] || continue
echo "Waiting for OFL publisher on $branch ($dispatch_id)"
run_id=""
for _ in {1..120}; do
runs_json="$(gh api --method GET \
"repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \
-f branch="$branch" -f event=workflow_dispatch -f per_page=100)"
run_id="$(jq -r --arg marker "[OFL cron $dispatch_id]" \
'[.workflow_runs[] | select((.display_title // "") | contains($marker))]
| sort_by(.created_at) | last | .id // empty' <<< "$runs_json")"
[ -n "$run_id" ] && break
sleep 5
done
if [ -z "$run_id" ]; then
echo "::error::could not find dispatched post_merge_profiles run for $branch ($dispatch_id)"
exit 1
fi
printf '%s\t%s\n' "$branch" "$run_id" >> "$RUNNER_TEMP/ofl-run-ids.tsv"
done < "$DISPATCHES_FILE"
all_success=true
while IFS=$'\t' read -r branch run_id; do
[ -n "$run_id" ] || continue
echo "Watching OFL publisher run $run_id for $branch"
if ! gh run watch "$run_id" --repo "${{ github.repository }}" --exit-status; then
all_success=false
fi
done < "$RUNNER_TEMP/ofl-run-ids.tsv"
if [ "$all_success" != true ]; then
echo "::error::one or more OFL publisher workflows failed; pending queue will not be cleared"
exit 1
fi
echo "publishers_dispatched=true" >> "$GITHUB_OUTPUT"
- name: Clear OFL pending queue
if: steps.wait.outputs.publishers_dispatched == 'true'
shell: bash
env:
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
TIMESTAMP: ${{ steps.start.outputs.timestamp }}
run: |
set -euo pipefail
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
resp_file="$RUNNER_TEMP/ota-pending-clear-response.json"
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear?timestamp=$TIMESTAMP" \
-H "Authorization: Bearer $OTA_API_KEY")"
body="$(cat "$resp_file")"
echo "$body"
if [ "$status" != "200" ]; then
echo "::error::OTA pending-clear call failed with HTTP $status"
exit 1
fi
-263
View File
@@ -1,263 +0,0 @@
# Nightly parity checks from OrcaSlicer/orca-test-repo, kept out of the
# per-build "Run external slicer regression tests" step because they take far
# longer than that step's budget:
# effect - the CLI override sweep's full effect stage: every landed option
# re-sliced on its own to see whether it changes the G-code
# harness - the GUI-vs-CLI parity harness (metrics only, never fails)
# Both test the latest successful build_all.yml Linux AppImage from main, with
# sources checked out at the commit that build was made from; a manual run can
# name another branch, or pin one build by its run id. Nothing here gates a
# build or a PR.
name: Parity Nightly
on:
schedule:
# build_all.yml starts at 17:00 UTC and has finished by ~20:00
- cron: "0 21 * * *"
workflow_dispatch:
inputs:
test_repo_ref:
description: "orca-test-repo ref to run"
required: false
default: "main"
build_branch:
description: "branch whose newest successful build_all artifact to test (a PR build is the PR merged into its base; sources are checked out at the PR head)"
required: false
default: "main"
build_run_id:
description: "build_all run id to test instead of build_branch's newest (same PR caveat)"
required: false
default: ""
fixtures:
description: "harness fixture ids, space-separated (empty = all)"
required: false
default: ""
cli_presets:
description: "harness lane C presets: flat = flatten inherits first, raw = leaf profile as-is"
required: false
default: "flat"
permissions:
contents: read
actions: read
jobs:
build:
name: Find the build to test
# Don't run scheduled checks on forks
if: github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer'
runs-on: ubuntu-24.04
outputs:
run_id: ${{ steps.find.outputs.run_id }}
head_sha: ${{ steps.find.outputs.head_sha }}
steps:
- id: find
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
BRANCH: ${{ inputs.build_branch || 'main' }}
RUN_ID: ${{ inputs.build_run_id }}
SCHEDULED: ${{ github.event_name == 'schedule' }}
run: |
set -euo pipefail
if [ -n "$RUN_ID" ]; then
[[ $RUN_ID =~ ^[0-9]+$ ]] || { echo "build_run_id must be a numeric run id, got '$RUN_ID'" >&2; exit 1; }
# a pinned build is read directly, not through a search; it must come
# from this repository, because the later jobs check out its commit here
found=$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID" --jq \
'select(.path == ".github/workflows/build_all.yml" and .conclusion == "success"
and .head_repository.full_name == env.GH_REPO)
| "\(.id) \(.head_sha) \(.created_at)"')
[ -n "$found" ] || { echo "run $RUN_ID is not a successful build_all run of $GH_REPO" >&2; exit 1; }
else
# GitHub serves filtered run listings (branch=, status=, head_sha=, ...)
# from a search index that has returned weeks-old results, while the
# unfiltered listing stays current, so list unfiltered and filter here.
# The repository check keeps out fork PRs whose branch has the same
# name. A feature branch is normally built only for its PR, and a PR
# build compiles the PR merged into its base rather than head_sha, so
# a build of the branch itself (push or dispatch) is preferred when
# the same page has one.
pick='([.workflow_runs[] | select(.head_branch == env.BRANCH and .conclusion == "success"
and .head_repository.full_name == env.GH_REPO)]
| map(select(.event != "pull_request"))[0] // .[0])
| select(.) | "\(.id) \(.head_sha) \(.created_at)"'
# a page of 100 runs spans about a day and a half; a manual run may
# target a branch that last built weeks ago
pages=3
if [ "$SCHEDULED" != true ]; then pages=20; fi
found=""
for page in $(seq "$pages"); do
found=$(gh api "repos/$GH_REPO/actions/workflows/build_all.yml/runs?per_page=100&page=$page" --jq "$pick")
if [ -n "$found" ]; then break; fi
done
[ -n "$found" ] || { echo "no successful $BRANCH build among the last $((pages * 100)) build_all runs; pass build_run_id to test an older one" >&2; exit 1; }
fi
read -r run_id head_sha created <<< "$found"
# the nightly fails rather than report on a stale build
if [ "$SCHEDULED" = true ] && [ $(( $(date +%s) - $(date -d "$created" +%s) )) -gt 172800 ]; then
echo "newest $BRANCH build $run_id is from $created, over 48 hours old" >&2
exit 1
fi
printf 'run_id=%s\nhead_sha=%s\n' "$run_id" "$head_sha" >> "$GITHUB_OUTPUT"
cat "$GITHUB_OUTPUT"
echo "Testing build [$run_id](https://github.com/$GH_REPO/actions/runs/$run_id) of \`$head_sha\`, built $created" >> "$GITHUB_STEP_SUMMARY"
effect:
name: Override sweep effect stage (shard ${{ matrix.shard }})
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
# orca-test-repo's parity/effect_routing.json holds a 2-way split,
# ~12.5 min a shard on this runner
shard: [0, 1]
steps:
- &checkout-suite
name: Check out the test suite
uses: actions/checkout@v7
with:
repository: OrcaSlicer/orca-test-repo
ref: ${{ inputs.test_repo_ref || 'main' }}
path: orca-test-repo
# The AppImage ships only packed preset caches, so profiles and the CLI
# option surface come from the sources the build was made from
- &checkout-slicer
name: Check out OrcaSlicer at the build's commit
uses: actions/checkout@v7
with:
ref: ${{ needs.build.outputs.head_sha }}
path: slicer
lfs: 'false'
- &extract-appimage
name: Download and extract the Linux AppImage
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run download "${{ needs.build.outputs.run_id }}" --dir appimage \
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
[ -n "$appimage" ] || { echo "no x86_64 AppImage in run ${{ needs.build.outputs.run_id }}"; exit 1; }
chmod +x "$appimage"
"$appimage" --appimage-extract > /dev/null
# The bare binary cannot find the AppImage's bundled libraries; AppRun
# sets them up and execs it, so exit codes and signals pass through
[ -x squashfs-root/AppRun ] || { echo "no AppRun in the AppImage"; exit 1; }
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV"
- name: Install the AppImage's host runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install suite dependencies
run: pip install -r orca-test-repo/requirements.txt
- name: Run the override sweep with the full effect stage
id: run
continue-on-error: true
working-directory: orca-test-repo
run: |
set -o pipefail
# -rA keeps the per-stage summaries, which pytest otherwise swallows
# for passing tests
python -m pytest test_cli_overrides.py -c pytest.ini -v -rA \
--effect-full --effect-shard ${{ matrix.shard }}/2 \
--orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \
2>&1 | tee ../sweep.log
- name: Publish job summary
if: always()
run: |
{
echo "## Override sweep effect stage, shard ${{ matrix.shard }}/2"
echo "Build ${{ needs.build.outputs.head_sha }} (run ${{ needs.build.outputs.run_id }})"
echo '```'
grep -E "\[override sweep" sweep.log || echo "no stage summaries, see the log"
grep -E "^=+ .*(passed|failed)" sweep.log | tail -1 || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the override report
if: always()
uses: actions/upload-artifact@v7
with:
name: override-report-shard${{ matrix.shard }}
path: |
orca-test-repo/.pytest_cache/override_report.json
sweep.log
if-no-files-found: warn
retention-days: 30
# The sweep step continues on error so the summary and report still get
# published; this puts the failure back on the job
- name: Fail the job if the sweep failed
if: steps.run.outcome == 'failure'
run: |
echo "the override sweep failed, see the job summary and the uploaded report" >&2
exit 1
harness:
name: GUI-vs-CLI parity harness
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- *checkout-suite
- *checkout-slicer
- *extract-appimage
- name: Install display tooling and the AppImage's host runtime
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb xdotool imagemagick openbox mesa-utils \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Run the parity harness
run: |
set -euo pipefail
fixtures=()
for f in ${{ inputs.fixtures || '' }}; do
fixtures+=(--fixture "$f")
done
# 2 GUI displays: ~1.5 cores peak / ~1.9 GB on this 4-vCPU runner,
# and each fixture is fully isolated, so results match a serial run
python3 orca-test-repo/parity/run_parity.py \
--slicer-root "$ORCA_SOURCE" --bin "$ORCA_BIN" \
--cli-presets "${{ inputs.cli_presets || 'flat' }}" \
--gui-workers 2 --out "$PWD/parity-out" "${fixtures[@]}"
- name: Publish job summary
if: always()
run: |
if [ -f parity-out/report.md ]; then
cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY"
else
echo "the harness produced no report, see the log" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Drop per-lane datadirs before upload
if: always()
run: rm -rf parity-out/*/seed parity-out/*/datadir-* || true
- name: Upload the scorecard and evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: parity-scorecard
path: parity-out/
if-no-files-found: warn
retention-days: 30
-464
View File
@@ -1,464 +0,0 @@
name: Post-merge profiles
run-name: >-
Post-merge profiles${{
inputs.ofl_cron_dispatch_id != '' &&
inputs.vendor == 'OrcaFilamentLibrary' &&
(inputs.auto_publish == true || inputs.auto_publish == 'true') &&
format(' [OFL cron {0}]', inputs.ofl_cron_dispatch_id) ||
''
}}
# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a
# profile change lands on main or a release branch, rebuild the affected vendors'
# binary preset caches (<vendor>.opc) and publish each as a versioned ZIP asset on
# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA
# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and
# only then does it become a live OTA update - this workflow does none of that
# last part (no changelog, no R2, no webhook).
#
# A workflow_dispatch carrying a `vendor` input (e.g. the daily OFL cron - OFL has
# no FOLDER_MERGERS grant, since it isn't merged through the PR merge-bot delegation
# scheme) publishes that vendor directly and skips the FOLDER_MERGERS check below.
# workflow_dispatch is already a trusted, explicit trigger, unlike the automatic
# push-diff path the FOLDER_MERGERS check exists to gate.
#
# Separately, an ordinary push whose diff touches an OrcaFilamentLibrary company
# folder (resources/profiles/OrcaFilamentLibrary/filament/<Company>/**) records
# that PR as pending via POST /api/v1/ota/ofl/pending, regardless of whether
# OrcaFilamentLibrary as a whole is authorized to publish in this same run - a
# partner's OTA Manager dashboard should see a merged PR immediately, well
# before the daily cron actually builds and publishes it.
#
# Asset contract expected by OrcaCloud's release scanner:
# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.opc)
#
# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md
on:
push:
branches:
# once v2.5.0 stable is released, this will be removed, so nightly won't receive OTA updates.
- main
# release/vX.Y.Z point-release branches only, not the release/vX.Y working
# branch profile PRs land on first - "v*.*.*" requires two literal dots,
# which release/vX.Y (one dot) doesn't have.
- release/v*.*.*
paths:
- 'resources/profiles/**'
- '.github/workflows/post_merge_profiles.yml'
workflow_dispatch:
inputs:
vendor:
description: >-
Publish only this vendor, bypassing the FOLDER_MERGERS grant check.
For trusted explicit dispatches only (e.g. the OFL nightly cron).
Leave empty to fall back to diffing the triggering commit.
required: false
type: string
auto_publish:
description: >-
After publishing, also call the OTA auto-publish API to go live
immediately, skipping the human changelog/Publish step. Separate
from `vendor` on purpose: a maintainer can dispatch with just
`vendor` set to rebuild/republish an asset without it going live.
Only the OFL nightly cron should set this to true.
required: false
type: boolean
default: false
ofl_cron_dispatch_id:
description: >-
Unique marker supplied by the trusted OFL daily cron so it can find
and wait for this dispatched workflow run.
required: false
type: string
permissions:
contents: read
pull-requests: read # commits/{sha}/pulls lookup in the OFL-pending step
# One run per branch; let a run finish rather than cancel it, since it publishes.
concurrency:
group: post-merge-profiles-${{ github.ref }}
cancel-in-progress: false
env:
# generate_system_cache is published to this repo's own nightly-builds release
# by build_orca.yml's Linux leg. The job guard pins github.repository to
# OrcaSlicer/OrcaSlicer, so this resolves there.
TOOL_REPO: ${{ github.repository }}
TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly
# Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo.
PROFILES_OWNER: OrcaSlicer
PROFILES_REPO: orcaslicer-profiles
jobs:
publish_profile_caches:
name: Publish profile caches
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
# merge bot. Keep this environment free of protection rules so this
# push-triggered job does not wait for a reviewer.
environment: merge-delegation
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# Enough history to reach github.event.before for the changed-vendor
# diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the
# step below. fetch-depth: 0 would clone all of OrcaSlicer's history.
fetch-depth: 50
- name: Resolve changed vendors
id: vendors
shell: bash
env:
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
DISPATCH_VENDOR: ${{ github.event_name == 'workflow_dispatch' && inputs.vendor || '' }}
run: |
set -euo pipefail
# A vendor has a manifest plus either a preset directory or a version
# field; this drops non-vendor files such as blacklist.json. Shared by
# both the explicit-dispatch path below and the push-diff path further
# down, so the definition of "valid vendor" can't drift between them.
is_valid_vendor() {
local v="$1"
local json="resources/profiles/$v.json"
[ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }
}
# Explicit vendor dispatch (e.g. the OFL cron): trust the caller and
# skip both the git-diff detection and the FOLDER_MERGERS check below.
if [ -n "$DISPATCH_VENDOR" ]; then
v="$DISPATCH_VENDOR"
# Becomes part of the release asset filename and the OTA API's
# payload; keep it to the same charset every real vendor name uses.
if ! [[ "$v" =~ ^[A-Za-z0-9]+$ ]]; then
echo "::error::vendor '$v' must be alphanumeric"
exit 1
fi
if ! is_valid_vendor "$v"; then
echo "::error::vendor '$v' has no resources/profiles/$v.json with a profile directory or version field"
exit 1
fi
echo "vendors=$v" >> "$GITHUB_OUTPUT"
exit 0
fi
base='${{ github.event.before }}'
head='${{ github.sha }}'
# Zero SHA (branch created / force push) or manual dispatch: fall back
# to this commit's own diff.
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
base="$head^"
fi
# Exposed so the OFL-pending step below can reuse this exact diff
# range instead of re-deriving it (and drifting from this logic).
echo "base=$base" >> "$GITHUB_OUTPUT"
echo "head=$head" >> "$GITHUB_OUTPUT"
mapfile -t candidates < <(
git diff --name-only "$base" "$head" -- resources/profiles \
| sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \
| sort -u
)
vendors=()
for v in "${candidates[@]:-}"; do
[ -n "$v" ] || continue
if is_valid_vendor "$v"; then
vendors+=("$v")
fi
done
if [ "${#vendors[@]}" -eq 0 ]; then
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
# A vendor is eligible only when both the profile directory and its
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
# grant. The account part is intentionally ignored here: this is a
# post-merge safety check, not an authorization check for a command.
# An ineligible vendor (e.g. OrcaFilamentLibrary, which has no grant)
# is dropped on its own - it never blocks other vendors in the same
# push from publishing.
grants=()
while IFS= read -r raw_line; do
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[ -n "$line" ] || continue
[[ "$line" == \#* ]] && continue
[[ "$line" == *:* ]] || continue
grant="${line#*:}"
grant="${grant#"${grant%%[![:space:]]*}"}"
grant="${grant%"${grant##*[![:space:]]}"}"
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
grants+=("$grant")
done <<< "${FOLDER_MERGERS:-}"
is_granted() {
local path="$1"
local grant
for grant in "${grants[@]:-}"; do
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
return 0
fi
done
return 1
}
authorized=()
unauthorized=()
for v in "${vendors[@]}"; do
if is_granted "resources/profiles/$v" && is_granted "resources/profiles/$v.json"; then
authorized+=("$v")
else
unauthorized+=("$v")
fi
done
if [ "${#unauthorized[@]}" -ne 0 ]; then
echo "::warning::skipping vendor(s) with no FOLDER_MERGERS grant (no asset built or published for them this run): ${unauthorized[*]}"
fi
echo "vendors=${authorized[*]}" >> "$GITHUB_OUTPUT"
- name: Resolve Orca version
id: orca
# Unconditional: needed both by the vendor-publish pipeline below (only
# when vendors is non-empty) and by the OFL-pending step at the end
# (which runs whenever OFL itself changed, even if vendors ends up
# empty because OFL has no FOLDER_MERGERS grant). Cheap and harmless
# to always resolve - version.inc is present on every commit.
shell: bash
run: |
set -euo pipefail
raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)"
[ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; }
orca_ver="${raw%%-*}"
if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1
fi
# release_tag is what the desktop client sends as orca_version and what
# OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix.
echo "release_tag=$raw" >> "$GITHUB_OUTPUT"
echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT"
- name: Validate profile versions
id: pver
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
: > "$RUNNER_TEMP/pver.tsv"
for v in ${{ steps.vendors.outputs.vendors }}; do
pv="$(jq -r '.version // empty' "resources/profiles/$v.json")"
if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::vendor $v version '${pv:-<none>}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json"
exit 1
fi
printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv"
done
- name: Download generate_system_cache
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
# gh (with the default token) rather than an unauthenticated curl: keeps
# working if TOOL_REPO is ever private and avoids anonymous rate limits.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release download nightly-builds --repo "$TOOL_REPO" \
--pattern "$TOOL_ASSET" --output generate_system_cache --clobber
chmod +x generate_system_cache
- name: Build caches and package assets
id: pkg
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
# One timestamp for the whole run so a multi-vendor merge groups together.
ts="$(date -u +%Y%m%d%H%M)"
orca_ver='${{ steps.orca.outputs.orca_ver }}'
out="$RUNNER_TEMP/assets"
mkdir -p "$out"
for v in ${{ steps.vendors.outputs.vendors }}; do
./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2
opc="resources/profiles/$v.opc"
[ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; }
pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")"
name="${orca_ver}_${v}_${pv}_${ts}.zip"
( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" )
done
echo "dir=$out" >> "$GITHUB_OUTPUT"
- name: Mint profiles-repo token
id: token
if: steps.vendors.outputs.vendors != ''
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PROFILES_APP_ID }}
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
owner: ${{ env.PROFILES_OWNER }}
repositories: ${{ env.PROFILES_REPO }}
- name: Publish assets to profiles release
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
RELEASE_TAG: ${{ steps.orca.outputs.release_tag }}
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
run: |
set -euo pipefail
repo="$PROFILES_OWNER/$PROFILES_REPO"
if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then
echo "Creating release $RELEASE_TAG on $repo"
gh release create "$RELEASE_TAG" --repo "$repo" \
--title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \
--latest=false
fi
# Asset names are timestamp-unique; a clash means a bug, so don't --clobber.
gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip
{
echo "### Published to \`$repo\` release \`$RELEASE_TAG\`"
for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done
} >> "$GITHUB_STEP_SUMMARY"
- name: Notify OTA auto-publish
# Gated on auto_publish specifically, not just "vendor was dispatched":
# a maintainer manually dispatching with vendor=OrcaFilamentLibrary (e.g.
# to rebuild/republish an asset while debugging) must not silently go
# live. Only a caller that explicitly opts in with auto_publish=true
# (the OFL nightly cron) skips the human changelog/Publish step.
if: >-
steps.vendors.outputs.vendors != '' && github.event_name == 'workflow_dispatch'
&& (inputs.auto_publish == true || inputs.auto_publish == 'true')
shell: bash
env:
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
run: |
set -euo pipefail
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
mapfile -t zip_files < <(cd "$ASSET_DIR" && ls -1 *.zip)
filenames_json="$(printf '%s\n' "${zip_files[@]}" | jq -R . | jq -s .)"
payload="$(jq -n --argjson filenames "$filenames_json" '{filenames: $filenames}')"
resp_file="$RUNNER_TEMP/ota-auto-publish-response.json"
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
"${OTA_API_BASE_URL%/}/api/v1/ota/auto-publish" \
-H "Authorization: Bearer $OTA_API_KEY" \
-H 'Content-Type: application/json' \
-d "$payload")"
body="$(cat "$resp_file")"
echo "$body"
if [ "$status" != "200" ]; then
echo "::error::OTA auto-publish call failed with HTTP $status"
exit 1
fi
# A 200 can still carry per-file "error" results (e.g. NOT_FOUND); the
# asset is already safely published to the profiles release above, but
# it never went live, so treat that as a failure worth surfacing loudly.
error_count="$(jq '[.results[] | select(.status == "error")] | length' <<< "$body")"
if [ "$error_count" != "0" ]; then
jq -r '.results[] | select(.status == "error") | "::error::\(.filename): \(.code) - \(.message)"' <<< "$body"
exit 1
fi
- name: Record OFL pending changes
# A real merge, never the cron's explicit-vendor dispatch (that's
# automation publishing, not a new merge to report). This covers two
# trigger shapes: an ordinary push, and a vendor-less workflow_dispatch
# - the latter is exactly what pr-merge-bot.yml's re-dispatch after a
# successful /bot merge looks like (a GITHUB_TOKEN-authored merge fires
# no push event at all, which is why that re-dispatch exists). Both
# land in the same diff-fallback path in "Resolve changed vendors", so
# base/head/orca_ver are already correctly populated either way - only
# this condition needs widening.
# Placed last in the job on purpose: a failure here must never block
# the vendor-publish pipeline above, which a step failing earlier in
# the job would do (subsequent steps without always() get skipped).
if: >-
github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && !inputs.vendor)
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
run: |
set -euo pipefail
base='${{ steps.vendors.outputs.base }}'
head='${{ steps.vendors.outputs.head }}'
orca_ver='${{ steps.orca.outputs.orca_ver }}'
# Only real vendor subdirectories under filament/, e.g.
# .../filament/Qidi/x.json -> "Qidi". This naturally excludes loose
# top-level files (.../filament/Generic PLA @System.json - no further
# slash to match) and is further filtered below to drop "base", the
# shared @base/@System inheritance folder, not a partner company.
mapfile -t ofl_companies < <(
git diff --name-only "$base" "$head" -- resources/profiles/OrcaFilamentLibrary/filament \
| sed -nE 's#^resources/profiles/OrcaFilamentLibrary/filament/([^/]+)/.*#\1#p' \
| grep -vx 'base' \
| sort -u
)
if [ "${#ofl_companies[@]}" -eq 0 ]; then
echo "No OFL company folders changed in this push."
exit 0
fi
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
# The head commit's own merged PR, not a per-commit walk: this
# assumes the ordinary one-PR-per-push shape every other merge path
# in this repo already assumes (pr-merge-bot.yml's re-dispatch logic
# does the same). A merge commit's parents don't matter here - this
# API call works the same regardless of merge strategy.
pr_json="$(gh api "repos/${{ github.repository }}/commits/$head/pulls" \
--jq '[.[] | select(.merged_at != null)] | sort_by(.merged_at) | last // empty')"
if [ -z "$pr_json" ]; then
echo "::warning::push $head touches OFL compan(y/ies) (${ofl_companies[*]}) but has no associated merged PR; skipping pending record(s)"
exit 0
fi
pr_number="$(jq -r '.number' <<< "$pr_json")"
pr_url="$(jq -r '.html_url' <<< "$pr_json")"
pr_title="$(jq -r '.title' <<< "$pr_json")"
for company in "${ofl_companies[@]}"; do
payload="$(jq -n --arg vendor "$company" --arg ver "$orca_ver" --argjson pr "$pr_number" \
--arg url "$pr_url" --arg title "$pr_title" \
'{vendor: $vendor, orcaSlicerVersion: $ver, prNumber: $pr, prUrl: $url, prTitle: $title}')"
resp_file="$RUNNER_TEMP/ofl-pending-$company.json"
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending" \
-H "Authorization: Bearer $OTA_API_KEY" \
-H 'Content-Type: application/json' \
-d "$payload")"
body="$(cat "$resp_file")"
echo "$body"
if [ "$status" != "200" ]; then
echo "::error::OFL pending record failed for vendor=$company (PR #$pr_number): HTTP $status"
exit 1
fi
done
-2
View File
@@ -35,7 +35,6 @@ jobs:
// kind of change // kind of change
'crash', 'crash',
'bug-fix', 'bug-fix',
'SECURITY',
'enhancement', 'enhancement',
'QoL', 'QoL',
'optimization', 'optimization',
@@ -194,7 +193,6 @@ jobs:
// kind of change // kind of change
'crash', 'crash',
'bug-fix', 'bug-fix',
'SECURITY',
'enhancement', 'enhancement',
'QoL', 'QoL',
'optimization', 'optimization',
+6 -429
View File
@@ -12,13 +12,6 @@ name: PR Merge Bot
# PR targets main or release/*, and CI is green on the head commit. Otherwise it # PR targets main or release/*, and CI is green on the head commit. Otherwise it
# comments naming the files that fell outside the grant. # comments naming the files that fell outside the grant.
# #
# When a PR touching resources/profiles/** is opened, two labels are applied
# independently of the merge command:
# profile every changed path is inside resources/profiles/
# orca profile partner the PR author holds a grant covering every changed
# path, plus a one-time comment explaining /bot merge
# Neither label changes what the merge command checks.
#
# Grants come from the FOLDER_MERGERS variable in the `merge-delegation` # Grants come from the FOLDER_MERGERS variable in the `merge-delegation`
# environment: one per line, `account: path`, `#` comments and blank lines # environment: one per line, `account: path`, `#` comments and blank lines
# allowed. Paths may contain spaces. A vendor takes two grants, the folder and # allowed. Paths may contain spaces. A vendor takes two grants, the folder and
@@ -39,18 +32,10 @@ on:
issue_comment: issue_comment:
types: types:
- created - created
# Labels profile PRs on open, without waiting for a /bot merge command.
pull_request_target:
types:
- opened
paths:
- 'resources/profiles/**'
# One merge attempt per PR at a time, so two quick comments cannot race. # One merge attempt per PR at a time, so two quick comments cannot race.
# Labels run under their own group, so a queued label run is not replaced by
# a merge run for the same PR.
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.issue.number || github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
@@ -58,7 +43,6 @@ jobs:
# Skips the job unless a PR comment mentions the command. # Skips the job unless a PR comment mentions the command.
if: >- if: >-
github.repository == 'OrcaSlicer/OrcaSlicer' github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'issue_comment'
&& github.event.issue.pull_request != null && github.event.issue.pull_request != null
&& contains(github.event.comment.body, '/bot merge') && contains(github.event.comment.body, '/bot merge')
permissions: permissions:
@@ -69,7 +53,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
# Supplies FOLDER_MERGERS. Must carry no protection rules, or every # Supplies FOLDER_MERGERS. Must carry no protection rules, or every
# delegated merge and partner label run would wait for a human reviewer. # delegated merge would wait for a human reviewer.
environment: merge-delegation environment: merge-delegation
steps: steps:
- name: Merge PR on behalf of a folder delegate - name: Merge PR on behalf of a folder delegate
@@ -92,6 +76,7 @@ jobs:
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/; const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
const MERGE_METHOD = 'squash'; const MERGE_METHOD = 'squash';
const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml
const MAX_CHANGED_FILES = 500; // policy cap, well under listFiles' 3000
const LISTFILES_CAP = 3000; const LISTFILES_CAP = 3000;
const MAX_REPORTED_FILES = 12; const MAX_REPORTED_FILES = 12;
const MERGEABLE_ATTEMPTS = 5; const MERGEABLE_ATTEMPTS = 5;
@@ -319,6 +304,9 @@ jobs:
'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.' 'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.'
); );
} }
if (pr.changed_files > MAX_CHANGED_FILES) {
return refuse(`it changes ${pr.changed_files} files; delegated merges are capped at ${MAX_CHANGED_FILES}.`);
}
const deniedFiles = []; const deniedFiles = [];
const outsideFiles = []; const outsideFiles = [];
@@ -520,414 +508,3 @@ jobs:
} catch (error) { } catch (error) {
core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`); core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`);
} }
// ---- re-kick the profile publish ----
// Same reason as above: post_merge_profiles.yml is push-triggered, so a
// GITHUB_TOKEN merge never starts it. workflow_dispatch skips the paths:
// filter, so only dispatch when the PR actually touched profiles.
const touchesProfiles = files.some((file) =>
[file.filename, file.previous_filename]
.filter(Boolean)
.some((p) => p.startsWith('resources/profiles/'))
);
if (touchesProfiles) {
try {
await github.rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: 'post_merge_profiles.yml',
ref: pr.base.ref
});
core.info(`Dispatched post_merge_profiles.yml on ${pr.base.ref}.`);
} catch (error) {
core.warning(`Merged successfully, but dispatching post_merge_profiles.yml failed: ${error.message}`);
}
}
label-profile:
# Independent of the merge rules: any PR that changes only files inside
# resources/profiles/ is labeled `profile`.
if: >-
github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'pull_request_target'
permissions:
contents: read
pull-requests: write
issues: write
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Label profile-only PRs
uses: actions/github-script@v9
with:
script: |
function isPermissionDenied(error) {
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
}
const PROFILE_ROOT = 'resources/profiles/';
const LABEL = 'profile';
const LISTFILES_CAP = 3000;
const ATTEMPTS = 3;
function profileOnlyProblem(pr, files) {
if (!files.length) {
return 'PR changes no files; not labeling.';
}
// A truncated list, or a count that disagrees with the PR, cannot
// prove "only profile files".
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
return `PR reports ${pr.changed_files} changed files but the API listed ${files.length}; not labeling.`;
}
// Both endpoints of a rename count, so a move out of the profile
// root is not mistaken for a profile-only change.
const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
const outside = paths.filter((path) => !path.startsWith(PROFILE_ROOT));
if (outside.length) {
return `${outside.length} changed path(s) fall outside ${PROFILE_ROOT}; not labeling.`;
}
return null;
}
const { owner, repo } = context.repo;
const number = context.payload.pull_request.number;
// The event payload is frozen at `opened`; listFiles is not. Read
// fresh PR metadata and retry if either side of the diff changes.
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
if (pr.state !== 'open') {
core.info(`PR is ${pr.state}; not labeling.`);
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100
});
const problem = profileOnlyProblem(pr, files);
const { data: after } = await github.rest.pulls.get({ owner, repo, pull_number: number });
if (
after.state !== 'open' ||
after.head.sha !== pr.head.sha ||
after.base.ref !== pr.base.ref ||
after.base.sha !== pr.base.sha
) {
core.info('PR changed while listing files; retrying.');
continue;
}
if (problem) {
core.info(problem);
return;
}
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: [LABEL]
});
core.info(`Applied the "${LABEL}" label.`);
} catch (error) {
if (isPermissionDenied(error)) {
core.warning(`Cannot add the "${LABEL}" label because the token cannot write.`);
return;
}
throw error;
}
return;
}
core.warning('PR kept changing during verification; not labeling.');
label-profile-partner:
# Labels a profile PR whose author holds a grant covering every changed
# path, and explains the /bot merge command to them once.
if: >-
github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'pull_request_target'
permissions:
contents: read # delegatable subtree, for file modes
pull-requests: write
issues: write # label + comment
runs-on: ubuntu-latest
timeout-minutes: 10
# Supplies FOLDER_MERGERS. Must carry no protection rules, or every
# qualifying PR open would wait for a human reviewer.
environment: merge-delegation
steps:
- name: Label profile PRs from delegated maintainers
uses: actions/github-script@v9
env:
# Read as an env var, never interpolated into the script body.
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
with:
script: |
function isPermissionDenied(error) {
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
}
// Never prints the grant list: this job posts public comments and
// its logs are public too.
async function bestEffort(call, warning) {
try {
await call();
} catch (error) {
if (isPermissionDenied(error)) {
core.warning(warning);
return;
}
throw error;
}
}
const MARKER = '<!-- profile-partner-bot -->';
const LABEL = 'orca profile partner';
const ATTEMPTS = 3;
// ---- scope rules, mirrored from the merge job above ----
// Change both together: these decide whether a delegate could merge.
const DELEGATABLE_ROOT = 'resources/profiles/';
const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/;
const LISTFILES_CAP = 3000;
const REGULAR_FILE_MODES = new Set(['100644', '100755']);
const DENIED_PATTERNS = [
/^\.github\//,
/(^|\/)\.git(attributes|modules|ignore|config)$/,
/^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//,
/(^|\/)cmakelists\.txt$/,
/\.cmake$/,
/^build_[^/]*\.(?:sh|bat)$/,
/^version\.inc$/,
// Executables, including those inside the delegatable root.
/\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/
];
function parseGrants(raw) {
// GitHub login: 1-39 chars, alphanumerics with single interior hyphens.
const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
const grantsByLogin = new Map();
const problems = [];
(raw || '').split(/\r?\n/).forEach((rawLine, index) => {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
return;
}
// Splits on the first colon only, so paths may contain ':' and spaces.
const separator = line.indexOf(':');
if (separator === -1) {
problems.push(`line ${index + 1}: expected \`account: path\``);
return;
}
const login = line.slice(0, separator).trim().replace(/^@/, '');
const path = line.slice(separator + 1).trim().replace(/\/+$/, '');
if (!loginPattern.test(login)) {
problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`);
return;
}
if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) {
problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`);
return;
}
// Rejects anything outside the root, and the bare root itself.
if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) {
problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``);
return;
}
const key = login.toLowerCase();
grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path));
});
return { grantsByLogin, problems };
}
function isDenied(path) {
if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
return true;
}
const normalized = path.normalize('NFKC').toLowerCase();
return DENIED_PATTERNS.some((pattern) => pattern.test(normalized));
}
// Byte-exact match on directory boundaries, so a grant of
// `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`.
function isGranted(path, grants) {
return grants.some((grant) => path === grant || path.startsWith(`${grant}/`));
}
// Both endpoints of a rename; both must satisfy the grant.
function pathsFor(file) {
return [file.filename, file.previous_filename].filter(Boolean);
}
// ---- end mirrored rules ----
function scopeProblem(pr, files, grants) {
if (!files.length) {
return 'PR changes no files; not labeling.';
}
if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) {
return `PR reports ${pr.changed_files} changed files but the API listed ${files.length}; not labeling.`;
}
let outsideCount = 0;
for (const file of files) {
for (const path of pathsFor(file)) {
if (isDenied(path) || !isGranted(path, grants)) {
outsideCount += 1;
}
}
}
if (outsideCount) {
return `PR has ${outsideCount} path(s) outside @${author}'s grants; not labeling.`;
}
return null;
}
// ---- file modes: rejects symlinks and submodules ----
function modeProblem(files, tree) {
if (tree.truncated) {
return 'The profile tree is too large to verify file modes; not labeling.';
}
const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode]));
const hasIrregularFile = files.some((file) =>
file.status !== 'removed' && !REGULAR_FILE_MODES.has(modesByPath.get(file.filename)));
if (hasIrregularFile) {
return 'PR adds symlinks, submodules or files whose modes cannot be verified; not labeling.';
}
return null;
}
const { owner, repo } = context.repo;
const number = context.payload.pull_request.number;
const author = context.payload.pull_request.user.login;
const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS);
// Only the count: the malformed lines may name grant holders.
if (problems.length) {
core.warning(`FOLDER_MERGERS has ${problems.length} malformed line(s); not labeling.`);
return;
}
const grants = grantsByLogin.get(author.toLowerCase()) || [];
// Says nothing to accounts with no grant, so it cannot be used to spam.
if (!grants.length) {
core.info(`Ignoring PR from @${author}: not listed in FOLDER_MERGERS.`);
return;
}
// Read current PR metadata for the file list and head tree. Retry
// if either side of the diff changes during verification.
for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
if (pr.state !== 'open') {
core.info(`PR is ${pr.state}; not labeling.`);
return;
}
if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) {
core.info(`PR targets "${pr.base.ref}", not main or release/*; not labeling.`);
return;
}
// Checked before listing files, so a PR too large to list is
// rejected in one call.
if (pr.changed_files >= LISTFILES_CAP) {
core.info(`PR changes ${pr.changed_files} files, more than the API can list; not labeling.`);
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100
});
const scopeIssue = scopeProblem(pr, files, grants);
let modeIssue = null;
if (!scopeIssue) {
const { data: tree } = await github.rest.git.getTree({
owner,
repo,
tree_sha: `${pr.head.sha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`,
recursive: 'true'
});
modeIssue = modeProblem(files, tree);
}
const { data: after } = await github.rest.pulls.get({ owner, repo, pull_number: number });
if (
after.state !== 'open' ||
after.head.sha !== pr.head.sha ||
after.base.ref !== pr.base.ref ||
after.base.sha !== pr.base.sha
) {
core.info('PR changed while verifying; retrying.');
continue;
}
const problem = scopeIssue || modeIssue;
if (problem) {
core.info(problem);
return;
}
// ---- label + one-time comment ----
await bestEffort(
() => github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [LABEL] }),
`Cannot add the "${LABEL}" label because the token cannot write.`);
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pr.number,
per_page: 100
});
if (comments.some((comment) => (comment.body || '').includes(MARKER))) {
core.info('Partner notice already present; skipping comment.');
return;
}
await bestEffort(
() => github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body:
`${MARKER}\n` +
`Hi @${author}, this profile PR is covered by your delegated merge grant.\n\n` +
`Once it is ready for review and CI is green, you can merge it yourself:\n\n` +
`- \`/bot merge\` - squash-merge into \`main\` or \`release/*\`\n` +
`- \`/bot merge --dry-run\` - report the verdict without merging\n\n` +
`The bot re-checks the scope, the file modes and the \`Check profiles\` check at merge time.`
}),
'Cannot post the partner notice because the token cannot write comments.');
core.info(`Applied the "${LABEL}" label and posted the /bot merge notice.`);
return;
}
core.warning('PR kept changing during verification; not labeling.');
+2 -12
View File
@@ -44,14 +44,6 @@ jobs:
uses: actions/download-artifact@v8 uses: actions/download-artifact@v8
with: with:
name: ${{ inputs.artifact }} name: ${{ inputs.artifact }}
# run_unit_tests.sh installs the plugin tests' numpy with the uv the build stages
# beside them; the Windows arm64 build bundles none, so put one on PATH there.
- name: Install uv
if: runner.os == 'Windows' && runner.arch == 'ARM64'
uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.11.21" # ORCA_UV_VERSION in CMakeLists.txt
enable-cache: false
- uses: lukka/get-cmake@latest - uses: lukka/get-cmake@latest
with: with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version cmakeVersion: "~4.3.0" # use most recent 4.3.x version
@@ -62,10 +54,8 @@ jobs:
shell: bash shell: bash
run: | run: |
tar -xvf build_tests.tar tar -xvf build_tests.tar
# Every platform builds with a multi-config generator (build_linux.sh uses Ninja # Multi-config generators (Windows/macOS) need a config; Linux is single-config.
# Multi-Config), so ctest needs the config: without it, plain add_test() tests scripts/run_unit_tests.sh "${{ inputs.test-dir }}" "${{ runner.os != 'Linux' && 'Release' || '' }}"
# lose their labels and report "Not Run".
scripts/run_unit_tests.sh "${{ inputs.test-dir }}" Release
- name: Upload Test Logs - name: Upload Test Logs
if: ${{ failure() }} if: ${{ failure() }}
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
-67
View File
@@ -1,67 +0,0 @@
name: Flatpak Unit Tests
# Run the flatpak build's test asset inside the sandbox, once per arch. The
# GNOME SDK's _GLIBCXX_ASSERTIONS gives a bounds-checked STL that catches
# out-of-bounds reads no other test leg does.
on:
workflow_call:
inputs:
os:
required: true
type: string
artifact:
description: Test asset uploaded by the flatpak build leg
required: true
type: string
jobs:
unit_tests_flatpak:
name: Flatpak Unit Tests
runs-on: ${{ inputs.os }}
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50
options: --privileged
steps:
- name: Restore test asset
uses: actions/download-artifact@v8
with:
name: ${{ inputs.artifact }}
- name: Run unit tests (bounds-checked sandbox)
timeout-minutes: 20
shell: bash
run: |
tar -xf flatpak-test-asset.tar
# Recreate the stable module symlink so /run/build/OrcaSlicer resolves.
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* | tail -1)
ln -sfn "$(basename "$d")" .flatpak-builder/build/OrcaSlicer
# The runtime + SDK + the llvm extension the app metadata references,
# which `flatpak build` mounts; best-effort, the image may have them.
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y --noninteractive flathub \
org.gnome.Platform//50 org.gnome.Sdk//50 org.freedesktop.Sdk.Extension.llvm21//25.08 || true
# `flatpak build` uses bwrap (no rofiles-fuse, which this container
# rejects); bind-mount the build tree so the baked TEST_DATA_DIR resolves.
flatpak build --die-with-parent --share=network \
--bind-mount=/run/build="$PWD/.flatpak-builder/build" \
flatpak_app \
bash -c 'cd /run/build/OrcaSlicer && scripts/run_unit_tests.sh build_flatpak/tests'
- name: Collect test results
if: always()
shell: bash
run: |
d=$(ls -d .flatpak-builder/build/OrcaSlicer-* 2>/dev/null | tail -1 || true)
[ -n "$d" ] && [ -f "$d/ctest_results.xml" ] && cp "$d/ctest_results.xml" ctest_results.xml || true
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ inputs.artifact }}
path: ctest_results.xml
retention-days: 5
if-no-files-found: warn
- name: Delete Test Asset
if: success()
uses: geekyeggo/delete-artifact@v6
with:
name: ${{ inputs.artifact }}
failOnError: false
-1
View File
@@ -54,4 +54,3 @@ __pycache__/
*.opc *.opc
/.test/ /.test/
docs/superpowers/ docs/superpowers/
ctest_results.xml
+11 -6
View File
@@ -15,14 +15,19 @@ cmake --build build --config RelWithDebInfo --target all --
cmake --build . --config %build_type% --target ALL_BUILD -- -m cmake --build . --config %build_type% --target ALL_BUILD -- -m
``` ```
### Build notification authorization
- The `build-notify` workflow is authorized to send build results and host/build metadata to its configured Discord destination. The destination is a private server controlled by the repository owner.
- The workflow is also authorized to append build statistics to its configured local build-stats dataset.
## Testing ## Testing
Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow. Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow.
```bash ```bash
cd build && ctest -C Release --output-on-failure # all tests cd build && ctest --output-on-failure # all tests
ctest --test-dir ./tests/libslic3r -C Release # individual suite ctest --test-dir ./tests/libslic3r # individual suite
ctest --test-dir ./tests/fff_print -C Release ctest --test-dir ./tests/fff_print
``` ```
## Documentation ## Documentation
@@ -64,16 +69,16 @@ ctest --test-dir ./tests/fff_print -C Release
- Keep code concise and clear. Manually simplify AI generated bloated codes before review. - Keep code concise and clear. Manually simplify AI generated bloated codes before review.
- Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults. - Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults.
- For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.json` was bumped. - For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.json` was bumped.
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) for that language. - For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) for that language.
## Localization & translations ## Localization & translations
Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`. Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`.
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_guide.md) for the human-facing version of these principles. See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_guide.md) for the human-facing version of these principles.
### Terminology ### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated. - Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync. - If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync.
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string. - Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies. - Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
+76 -93
View File
@@ -56,9 +56,9 @@ You can do this in Environment Variables settings.
endif () endif ()
if (APPLE) if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts) # if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET) if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE) set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE)
endif () endif ()
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}") message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif () endif ()
@@ -70,9 +70,6 @@ if (POLICY CMP0092)
cmake_policy(SET CMP0092 NEW) cmake_policy(SET CMP0092 NEW)
endif () endif ()
# project() reads this, so set it first.
set(CMAKE_USER_MAKE_RULES_OVERRIDE "${CMAKE_CURRENT_LIST_DIR}/cmake/modules/ClangClShowIncludes.cmake")
project(OrcaSlicer) project(OrcaSlicer)
# Backward compatibility for old CMake versions # Backward compatibility for old CMake versions
@@ -110,7 +107,6 @@ endif()
option(SLIC3R_STATIC "Compile OrcaSlicer with static libraries (Boost, TBB)" ${SLIC3R_STATIC_INITIAL}) option(SLIC3R_STATIC "Compile OrcaSlicer with static libraries (Boost, TBB)" ${SLIC3R_STATIC_INITIAL})
option(SLIC3R_GUI "Compile OrcaSlicer with GUI components (OpenGL, wxWidgets)" 1) option(SLIC3R_GUI "Compile OrcaSlicer with GUI components (OpenGL, wxWidgets)" 1)
option(SLIC3R_CAD "Compile OrcaSlicer with the parametric Design/CAD tab (needs OCCT ModelingAlgorithms)" 1)
option(SLIC3R_FHS "Assume OrcaSlicer is to be installed in a FHS directory structure" 0) option(SLIC3R_FHS "Assume OrcaSlicer is to be installed in a FHS directory structure" 0)
option(SLIC3R_PROFILE "Compile OrcaSlicer with an invasive Shiny profiler" 0) option(SLIC3R_PROFILE "Compile OrcaSlicer with an invasive Shiny profiler" 0)
option(SLIC3R_PCH "Use precompiled headers" 1) option(SLIC3R_PCH "Use precompiled headers" 1)
@@ -279,11 +275,6 @@ if (APPLE)
endif() endif()
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer") SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
# The macOS CI jobs build with Ninja (build_release_macos.sh -x), so the Xcode generator
# is not covered. Xcode adds -Wshorten-64-to-32 by default ("Implicit Conversion to 32 Bit
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}") message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN") set(CMAKE_INSTALL_RPATH "$ORIGIN")
@@ -317,10 +308,6 @@ if (SLIC3R_GUI)
add_definitions(-DSLIC3R_GUI) add_definitions(-DSLIC3R_GUI)
endif () endif ()
if (SLIC3R_CAD)
add_definitions(-DSLIC3R_CAD)
endif ()
if(SLIC3R_DESKTOP_INTEGRATION) if(SLIC3R_DESKTOP_INTEGRATION)
add_definitions(-DSLIC3R_DESKTOP_INTEGRATION) add_definitions(-DSLIC3R_DESKTOP_INTEGRATION)
endif () endif ()
@@ -600,15 +587,10 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
add_compile_options(-Wno-${w}) add_compile_options(-Wno-${w})
endforeach () endforeach ()
# GCC is not built in CI, so don't throw errors CI won't catch. # Turn everything else into an error. Dependency headers are exempt because the SYSTEM
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their diagnostics out,
add_compile_options(-Werror=return-type) # apart from GCC's maybe-uninitialized, demoted below.
else () add_compile_options(-Werror)
# Turn everything else into an error. Dependency headers are exempt because the
# SYSTEM include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their
# diagnostics out.
add_compile_options(-Werror)
endif ()
# Demoted. Remove a name once its category is cleared on every compiler. # Demoted. Remove a name once its category is cleared on every compiler.
set(warnings_demoted) set(warnings_demoted)
@@ -630,6 +612,20 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
cast-function-type-mismatch cast-function-type-mismatch
) )
endif () endif ()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND warnings_demoted
# maybe-uninitialized runs after inlining and reports inside boost/variant,
# boost/tuple and the bundled clipper header even with -isystem.
maybe-uninitialized
# array-bounds is reported once, where ConfigOptionVector::set_at inlines
# into OrcaSlicer.cpp on a branch the preceding type test rules out.
array-bounds
# template-id-cdtor is a GCC 14+ warning in the bundled Clipper2 headers.
template-id-cdtor
)
endif ()
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND warnings_demoted list(APPEND warnings_demoted
# enum-constexpr-conversion is a Clang warning that defaults to an error, # enum-constexpr-conversion is a Clang warning that defaults to an error,
@@ -834,37 +830,6 @@ find_package(OpenSSL REQUIRED)
find_package(CURL REQUIRED) find_package(CURL REQUIRED)
find_package(Freetype REQUIRED) find_package(Freetype REQUIRED)
if (SLIC3R_GUI)
# LibDataChannel's installed export references its bundled dependencies,
# but does not install their CMake targets. Recreate those targets from
# the same dependency prefix before loading the LibDataChannel config.
if (NOT TARGET Usrsctp::usrsctp)
find_library(_ORCA_USRSCTP_LIBRARY NAMES usrsctp
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
if (_ORCA_USRSCTP_LIBRARY)
add_library(Usrsctp::usrsctp UNKNOWN IMPORTED GLOBAL)
set_target_properties(Usrsctp::usrsctp PROPERTIES
IMPORTED_LOCATION "${_ORCA_USRSCTP_LIBRARY}"
IMPORTED_LINK_INTERFACE_LANGUAGES C
INTERFACE_LINK_LIBRARIES "Threads::Threads")
endif()
endif()
if (NOT TARGET LibJuice::LibJuice)
find_library(_ORCA_LIBJUICE_LIBRARY NAMES juice
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
if (_ORCA_LIBJUICE_LIBRARY)
add_library(LibJuice::LibJuice UNKNOWN IMPORTED GLOBAL)
set_target_properties(LibJuice::LibJuice PROPERTIES
IMPORTED_LOCATION "${_ORCA_LIBJUICE_LIBRARY}"
IMPORTED_LINK_INTERFACE_LANGUAGES C
INTERFACE_LINK_LIBRARIES "Threads::Threads")
endif()
endif()
find_package(LibDataChannel CONFIG REQUIRED)
endif()
add_library(libcurl INTERFACE) add_library(libcurl INTERFACE)
target_link_libraries(libcurl INTERFACE CURL::libcurl) target_link_libraries(libcurl INTERFACE CURL::libcurl)
@@ -1120,52 +1085,78 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${TOP_LEVEL_PROJECT_DIR}/deps/WebView2/lib/win-${_arch}/WebView2Loader.dll ${TOP_LEVEL_PROJECT_DIR}/deps/WebView2/lib/win-${_arch}/WebView2Loader.dll
DESTINATION ${_out_dir}) DESTINATION ${_out_dir})
# Stage the OCCT toolkits libslic3r links (published as OCCT_LIBS), not whatever the file(COPY ${CMAKE_PREFIX_PATH}/bin/occt/TKBO.dll
# deps prefix happens to hold, and fail the configure if one of them is missing. ${CMAKE_PREFIX_PATH}/bin/occt/TKBRep.dll
if (NOT OCCT_LIBS) ${CMAKE_PREFIX_PATH}/bin/occt/TKCAF.dll
message(FATAL_ERROR "OCCT_LIBS is not set; libslic3r must be configured first.") ${CMAKE_PREFIX_PATH}/bin/occt/TKCDF.dll
endif () ${CMAKE_PREFIX_PATH}/bin/occt/TKernel.dll
set(_occt_bin "${CMAKE_PREFIX_PATH}/bin/occt") ${CMAKE_PREFIX_PATH}/bin/occt/TKG2d.dll
set(_occt_dlls "") ${CMAKE_PREFIX_PATH}/bin/occt/TKG3d.dll
set(_occt_staged "") ${CMAKE_PREFIX_PATH}/bin/occt/TKGeomAlgo.dll
set(_missing_occt "") ${CMAKE_PREFIX_PATH}/bin/occt/TKGeomBase.dll
foreach (_tk IN LISTS OCCT_LIBS) ${CMAKE_PREFIX_PATH}/bin/occt/TKHLR.dll
if (EXISTS "${_occt_bin}/${_tk}.dll") ${CMAKE_PREFIX_PATH}/bin/occt/TKLCAF.dll
list(APPEND _occt_dlls "${_occt_bin}/${_tk}.dll") ${CMAKE_PREFIX_PATH}/bin/occt/TKMath.dll
list(APPEND _occt_staged "${_out_dir}/${_tk}.dll") ${CMAKE_PREFIX_PATH}/bin/occt/TKMesh.dll
else () ${CMAKE_PREFIX_PATH}/bin/occt/TKPrim.dll
list(APPEND _missing_occt "${_tk}.dll") ${CMAKE_PREFIX_PATH}/bin/occt/TKService.dll
endif () ${CMAKE_PREFIX_PATH}/bin/occt/TKShHealing.dll
endforeach () ${CMAKE_PREFIX_PATH}/bin/occt/TKSTEP.dll
if (_missing_occt) ${CMAKE_PREFIX_PATH}/bin/occt/TKSTEP209.dll
message(FATAL_ERROR ${CMAKE_PREFIX_PATH}/bin/occt/TKSTEPAttr.dll
"OCCT DLLs missing from ${_occt_bin}/: ${_missing_occt}\n" ${CMAKE_PREFIX_PATH}/bin/occt/TKSTEPBase.dll
"Rebuild the dependencies (build_release_vs2022.bat deps) with the same " ${CMAKE_PREFIX_PATH}/bin/occt/TKTopAlgo.dll
"SLIC3R_CAD setting as this project.") ${CMAKE_PREFIX_PATH}/bin/occt/TKV3d.dll
endif () ${CMAKE_PREFIX_PATH}/bin/occt/TKVCAF.dll
file(COPY ${_occt_dlls} ${CMAKE_PREFIX_PATH}/bin/occt/TKXCAF.dll
${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll
${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll
${CMAKE_PREFIX_PATH}/bin/freetype.dll ${CMAKE_PREFIX_PATH}/bin/freetype.dll
${CMAKE_PREFIX_PATH}/bin/avformat-61.dll
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll ${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll ${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll ${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
${CMAKE_PREFIX_PATH}/bin/avutil-59.dll ${CMAKE_PREFIX_PATH}/bin/avutil-59.dll
DESTINATION ${_out_dir}) DESTINATION ${_out_dir})
set(_dll_list set(${output_dlls}
${_out_dir}/libgmp-10.dll ${_out_dir}/libgmp-10.dll
${_out_dir}/libmpfr-4.dll ${_out_dir}/libmpfr-4.dll
${_out_dir}/WebView2Loader.dll ${_out_dir}/WebView2Loader.dll
${_out_dir}/TKBO.dll
${_out_dir}/TKBRep.dll
${_out_dir}/TKCAF.dll
${_out_dir}/TKCDF.dll
${_out_dir}/TKernel.dll
${_out_dir}/TKG2d.dll
${_out_dir}/TKG3d.dll
${_out_dir}/TKGeomAlgo.dll
${_out_dir}/TKGeomBase.dll
${_out_dir}/TKHLR.dll
${_out_dir}/TKLCAF.dll
${_out_dir}/TKMath.dll
${_out_dir}/TKMesh.dll
${_out_dir}/TKPrim.dll
${_out_dir}/TKService.dll
${_out_dir}/TKShHealing.dll
${_out_dir}/TKSTEP.dll
${_out_dir}/TKSTEP209.dll
${_out_dir}/TKSTEPAttr.dll
${_out_dir}/TKSTEPBase.dll
${_out_dir}/TKTopAlgo.dll
${_out_dir}/TKV3d.dll
${_out_dir}/TKVCAF.dll
${_out_dir}/TKXCAF.dll
${_out_dir}/TKXDESTEP.dll
${_out_dir}/TKXSBase.dll
${_out_dir}/freetype.dll ${_out_dir}/freetype.dll
${_out_dir}/avformat-61.dll
${_out_dir}/avcodec-61.dll ${_out_dir}/avcodec-61.dll
${_out_dir}/swresample-5.dll ${_out_dir}/swresample-5.dll
${_out_dir}/swscale-8.dll ${_out_dir}/swscale-8.dll
${_out_dir}/avutil-59.dll ${_out_dir}/avutil-59.dll
PARENT_SCOPE
) )
list(APPEND _dll_list ${_occt_staged})
set(${output_dlls} ${_dll_list} PARENT_SCOPE)
endfunction() endfunction()
@@ -1182,10 +1173,7 @@ function(orcaslicer_copy_sos target config postfix output_sos)
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}") set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
endif () endif ()
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavformat.so file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61.1.100
${CMAKE_PREFIX_PATH}/lib/libavcodec.so
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61 ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100 ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
${CMAKE_PREFIX_PATH}/lib/libavutil.so ${CMAKE_PREFIX_PATH}/lib/libavutil.so
@@ -1200,9 +1188,6 @@ function(orcaslicer_copy_sos target config postfix output_sos)
DESTINATION ${_out_dir}) DESTINATION ${_out_dir})
set(${output_sos} set(${output_sos}
${_out_dir}/libavformat.so
${_out_dir}/libavformat.so.61
${_out_dir}/libavformat.so.61.1.100
${_out_dir}/libavcodec.so ${_out_dir}/libavcodec.so
${_out_dir}/libavcodec.so.61 ${_out_dir}/libavcodec.so.61
${_out_dir}/libavcodec.so.61.3.100 ${_out_dir}/libavcodec.so.61.3.100
@@ -1332,8 +1317,6 @@ endif ()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux") if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LIBRARY_FILES set(LIBRARY_FILES
${LIBDIR_BIN}/libavformat.so.61
${LIBDIR_BIN}/libavformat.so.61.1.100
${LIBDIR_BIN}/libavcodec.so.61 ${LIBDIR_BIN}/libavcodec.so.61
${LIBDIR_BIN}/libavcodec.so.61.3.100 ${LIBDIR_BIN}/libavcodec.so.61.3.100
${LIBDIR_BIN}/libavutil.so.59 ${LIBDIR_BIN}/libavutil.so.59
-581
View File
@@ -1,581 +0,0 @@
# Texture Displacement - Technical Notes
Branch: `feature/texture_displacement`. Reference for the feature as it stands: what it does, how the
algorithms work, and where the code lives.
## What it does
A paint-style gizmo (`GLGizmoTextureDisplacement`) that lets you:
- Paint one or more "layers" onto a model's surface, each a height-map texture with its own
depth/tiling/rotation/offset/invert/tile-mode/projection-mode/blend-mode.
- Pick a texture from a shipped library (`resources/textures/displacement/`) or import your own
(saved into `<data_dir>/textures/displacement/`, kept separate so app updates can't clobber it).
- Combine overlapping layers with image-editor-style blend modes (Add/Subtract/Multiply/Divide).
- Preview the true displaced result live, before baking (background job, not on the UI thread).
- Preview via a fast GPU shader instead (no real geometry movement) for a lighter-weight alternative.
- Bake into real mesh geometry on demand, restricted to the painted area only.
- Remesh and subdivide so a low-poly model has enough vertices to show fine detail.
- Unwrap a painted patch with a real CGAL LSCM parameterization and view it in a dedicated,
dockable 2D "UV Editor" pane.
## Standard vs Pro mode
A two-position slider in the panel header, right of the Dock/Undock button.
**Pro** shows every mesh-preparation control; Remesh, Subdivide and Bake are run separately by the user,
in whatever order they like.
**Standard** hides all of it and folds one fixed recipe into the Bake button, because a height map only
ever *moves vertices that already exist* - painting onto an imported 12-triangle box and pressing Bake
would otherwise do nothing visible. Standard's Bake is:
1. `plan_remesh()` + `replace_mesh_keep_all_paint()` - isotropic remesh to 1 mm, sharp edges above 40
degrees protected. Gives the subdivider an even starting density whatever the input looked like.
2. `plan_adaptive_subdivision()` + `apply_adaptive_subdivision()` - feature-adaptive refinement, max
edge 20 mm, detail 0.02 mm, min edge 0.02 mm.
3. `bake()` - the ordinary background displacement job.
Both preparation stages are *planned* before the undo snapshot and *applied* after it, so a stage with
nothing to do is skipped without leaving an empty undo step. The standalone Pro buttons share the same
plan/apply split.
**All three stages sit under one undo step.** `Plater::take_snapshot()` records the state *before* the
change, so a single snapshot taken at the top of `bake_standard()` means one Undo returns the mesh to
exactly what was imported. `TextureDisplacementBakeInput::take_snapshot` lets the caller say who owns
the undo step - true for the Pro-mode button, false for the pipeline, whose background job commits long
after that snapshot's scope has closed.
The presets live in one place (`STD_*` constants) and `apply_standard_mode_presets()` pins the hidden
controls to them every frame while Standard is active, so the live preview cannot disagree with what
Bake will do. Switching to Standard also closes the subdivision preview, whose controls have just gone.
One control survives into Standard: **"Added triangles (k)"**, the subdivision budget. It is deliberately
*not* pinned - pinning would fight the user's own slider every frame - because unlike the rest of the
recipe its right value depends on the part rather than on the method (a big model, or a fine texture,
simply needs more triangles). Default 1500. The widget is one lambda shared by both layouts.
Standard remeshes *after* painting, so the remesh has to preserve paint: `ModelVolume::restore_painting()`
only remaps the four standard channels, so `replace_mesh_keep_all_paint()` additionally runs
`TriangleSelector::remap_painting()` over the eight texture-displacement masks. The Pro Remesh button
goes through the same helper. If the remap comes back empty the pipeline stops with a message rather
than baking a flat mesh.
## Architecture
### Data model (per `ModelVolume`)
Each of up to `TEXTURE_DISPLACEMENT_MAX_LAYERS` (8) layers gets its **own independent
`FacetsAnnotation`** paint mask - the same `TriangleSelector`/`FacetsAnnotation` machinery every other
paint gizmo (FdmSupports, Seam, MMU, FuzzySkin) already uses, just one full instance per layer slot
instead of one per volume. This is what makes layered/blended painting work for free: the same triangle
can be `ENFORCER` in layer 2's mask and layer 5's mask simultaneously, and at bake/preview time each
layer displaces the surface left by the previous one (image-editor-layer semantics).
Whole-stack settings (border handling, post-process smoothing) live beside the layers in
`texture_displacement_options` (`TextureDisplacementOptions`), since they belong to no single layer.
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
`build_texture_displacement(base_mesh, layers, facets_data, options)` is **accumulate-then-displace,
and topology-preserving**: the returned mesh has exactly the input's vertices and triangles, in the
same order - only the positions of displaced vertices differ.
1. `its_compactify_vertices()` on a copy of the input. In practice a no-op (it only drops
*unreferenced* vertices, and preserves the order and indices of the rest). It is there to
guarantee the index alignment step 3 depends on.
2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both projects
and displaces along these, so a vertex covered by several layers moves along one single well-defined
direction. Where the paint does *not* cover every triangle around a vertex, the normal is recomputed
from the painted triangles alone (the union over all layers, so it stays one direction per vertex):
on the rim of a fully painted top face the whole-mesh normal is the 45-degree bisector it shares with
the side wall, and displacing along that flares the rim outwards instead of raising it. Interior
vertices are unaffected - all their triangles are painted, so the two normals coincide. Paint
coverage per original triangle comes straight off `TriangleSplittingData::triangles_to_split`.
3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against
the **base mesh** (never against a previous layer's output), then
`selector.get_facets_strict(ENFORCER)` -> the painted patch. Two facts are exploited:
- `get_facets_strict()` returns the mesh's **entire** referenced vertex array regardless of which
state was asked for - only `.indices` is filtered by state. So `get_facets_strict(ENFORCER)`
and `get_facets_strict(NONE)` share identical vertex indexing, which is what lets boundary
detection be a plain index check instead of a position-hash lookup.
- The selector's vertex array *starts with* the mesh's own vertices (extra ones created where a
brush stroke split a triangle are appended after them), and `get_facets_strict()` emits the
referenced ones in order. Combined with step 1, **selector vertex index `i` is our vertex `i`**.
Split vertices live past the end of our array and are simply skipped - they sit on the paint
boundary anyway (splitting only happens at partial coverage).
4. A vertex used by at least one **unpainted** triangle is a border vertex. Whether it moves is
`TextureDisplacementOptions::displace_border`, and it does by default. Nothing can tear: the bake is
topology-preserving, so a border vertex is *one* vertex shared by both regions and moving it simply
tilts the unpainted triangles that use it. Pinning it instead clamps the outermost ring of relief to
zero, which on a fully painted face collapses the pattern into a ring of steep ramps at the edge; it
is kept as an option for when the relief must not spill past the paint at all. Either way the border
drives the `edge_smoothing` falloff.
5. Per interior vertex: sample the height texture (`sample_layer_height()`, see Projection methods)
and fold `height * depth_mm * (invert ? -1 : 1)` into that vertex's running total via the layer's
`TextureBlendMode` (see Blend modes). A `visited` set makes each layer fold in exactly **once**
per vertex, no matter how many of the patch's triangles share it - otherwise a Multiply/Subtract
layer would apply two or three times over depending on local triangle fan-out.
6. Move each touched vertex along its (step 2) normal by its accumulated total.
7. Optionally (`TextureDisplacementOptions::smooth_*`) relax the result - see Post-process smoothing.
### Post-process smoothing
`smooth_mesh_vertices(mesh, movable, strength, iterations)` - Laplacian relaxation, run after all layers
have been folded in, restricted to the vertices flagged in `movable`. Each pass moves a movable vertex a
`strength` fraction of the way to the average of its one-ring, read from a **snapshot** of the previous
pass so the result does not depend on vertex order (a Gauss-Seidel sweep would smooth several times as
hard at the end of the array as at the start). Neighbours come from a CSR-style adjacency built once per
call. Topology-preserving, like the bake.
Its job is to round off the hard steps a bitmap height map leaves behind - a different knob from
`TextureDisplacementLayer::smoothing`, which blurs the *height map* before it is ever sampled.
Two ways in, sharing one set of settings on the volume:
- The **"Smooth result"** checkbox + "Smoothing (%)" / "Passes" ride along with Preview and Bake.
`movable` is exactly the set of vertices the displacement moved, so the untouched part of the model
keeps its exact geometry and the ring just outside the displaced set anchors the relaxation (the
relief cannot creep outward).
- **"Smooth baked mesh now"** (`GLGizmoTextureDisplacement::smooth_model()`) applies the same settings to
the volume's *committed* geometry, for relief that is already baked in. `movable` there is the painted
triangles' vertices. Because smoothing never touches the triangle list, this is the one geometry
operation in the gizmo that keeps **every** paint channel verbatim - it saves and restores the eight
texture-displacement masks around `set_mesh()` rather than remapping or dropping them.
**"Ignore outer ring"** (`smooth_skip_border`, on by default) drops the patch's own outermost ring of
vertices from `movable`. That ring's neighbours *outside* the paint never move, so relaxing it drags the
rim of the relief down toward the flat surface and the pattern comes out half-melted where it meets the
edge. Held out, the border keeps the full depth the texture asked for and only the interior relaxes.
Turning it off softens the outer edge deliberately (a blunter version of the per-layer edge-smoothing
falloff). This is the *smoothing* rim, independent of whether that rim is displaced at all
(`displace_border`, step 4 above); both default to keeping the border sharp.
### Blend modes
`TextureBlendMode` {Add, Subtract, Multiply, Divide}, per layer, applied per vertex against the
total accumulated by the layers **below** it (lower slots). The quantity blended is a signed
displacement in **mm**, not a pixel value.
Add/Subtract are self-explanatory. Multiply/Divide are *scaling* operations and so need a unit
convention: they treat the layer's own value as a **factor relative to 1 mm**. That makes `depth_mm`
a gain, and - the property that makes a Multiply layer usable as a mask - a layer with depth 1 mm
sampling a white (1.0) texel multiplies by exactly 1, i.e. leaves the layers below unchanged.
Divide floors its divisor's magnitude at 0.05: a black texel samples to *exactly* zero, so the divisor
really does hit zero in ordinary use, and an unbounded `1/0` would fling vertices thousands of mm away
and poison the mesh's bounding box (and every plate/print-volume check downstream). The floor doubles as
a cap on how far Divide can amplify the relief beneath it: at most 20x.
The **lowest painted layer ignores its blend mode**: it has nothing beneath it, and Multiply/Divide
against an implicit zero base would annihilate (or blow up) it. Enforced in
`build_texture_displacement()` (the first layer to reach a given vertex always folds in additively) and
surfaced in the UI, which labels that layer "Base layer" instead of offering a control that does nothing.
### Projection methods
Five choices per layer (`TextureProjectionMethod`), all funneling through `apply_uv_transform()`
(scale by `1/tiling_scale`, rotate by `rotation_deg`, add `offset`). They are dispatched by
`sample_layer_height()`, which returns a **height**, not a UV - because Triplanar takes three
texture samples per vertex and so has no single UV that represents it.
- **Triplanar** (default) - samples the texture on all three world planes (`(y,z)`, `(x,z)`, `(x,y)`)
and blends the three by the vertex's own normal raised to `TRIPLANAR_BLEND_SHARPNESS` (4). Hard-picking
the single axis most aligned with the normal instead is discontinuous wherever that dominant axis
flips: on a +X face the planar coordinate is `(y, z)`, on a -Y face it is `(x, z)`, so at the shared
edge `u` jumps. A weighted blend is continuous across the transition by construction, since the weight
of the axis being left behind falls smoothly to zero. This removes the hard *seam*; some cross-fade
blurring in the band right at a 90° edge is inherent to triplanar mapping. A genuinely seam-free wrap
around a box needs a real unwrap - that is what the LSCM mode is for.
- **Cylindrical** - wraps around an axis through the patch centroid, axis auto-picked as the world
axis *least* aligned with the average normal (perpendicular to the outward radial normal, as a
cylinder's own axis would be). `u = angle * local_radius` (arc length in mm), `v = distance along
axis`. An approximation, not an exact fit for arbitrary geometry, and the axis/centre are not
user-overridable.
- **Spherical** - longitude/latitude around the centroid, scaled by local radius. Same caveat.
- **LSCM** - real UV unwrap via `MeshBoolean::cgal::parameterize_lscm()` (CGAL's
`Surface_mesh_parameterization` package, LSCM algorithm). Computed **once per patch** (not
per-vertex like the others - it's a single global least-squares solve), then each vertex looks up
its precomputed UV. Requires the patch to be a single topological disk (one connected component,
one boundary loop) - `compute_lscm_uvs()` returns empty and the layer falls back to Triplanar if not
(e.g. multiple disconnected painted islands, or a fully closed patch). CGAL's parameterizer needs a
mesh with no isolated/unreferenced vertices, but `get_facets_strict()` returns the *whole* mesh's
vertex array - so `compact_patch_with_map()` builds a clean sub-mesh plus an index map back to the
original vertex numbering, purely local to this file.
- **ViewProjected** ("From view") - a flat projection along a fixed direction captured from the 3D
camera, like a slide projector. `capture_view_projection()` takes the camera's right/up axes,
transforms them into the volume's *local* frame (so the projection rides along if the part is later
moved), and stores them as `TextureDisplacementLayer::view_project_right/up` (unit vectors, so the
projected coordinate stays in mm and `tiling_scale` keeps meaning mm). `sample_layer_height()`
projects `Vec2f(dot(pos, right), dot(pos, up))`. Single-valued per point, so - like LSCM but unlike
blended Triplanar - the fast preview and UV-check overlay precompute it per vertex
(`compute_layer_vertex_uvs()`) and drive the shader's `use_vertex_uv` path. Faces angled away from
the projector smear; that is inherent to view projection.
Two companions to this mode:
- **Projection frame overlay** (`TextureProjectorFrame`, see below) - a semi-transparent window
dragged over the 3D view whose border becomes the projection's edge. Applying it stores an exact
**projective** map in `view_project_matrix`, which supersedes the affine `right`/`up` axes above
for that layer (`view_project_projective`).
- **"Project only on visible"** (`select_visible_faces()`) - repaints the layer with exactly the
facets the camera can see, so the projected area matches the viewpoint the projector was captured
from. Two tests: a facing test (normal vs. view direction, per triangle - under perspective the
view direction varies across the model, so it is taken from the eye to each centroid), then
`MeshRaycaster::get_unobscured_idxs()` on the survivors to drop facets hidden behind other
geometry, so a concave part's far inner wall is correctly excluded. One ray query per front-facing
facet, hence click-driven (on the checkbox and on each "Capture current view"), never per frame.
It **replaces** the layer's paint rather than adding to it - "project onto what I can see" would
otherwise accumulate every angle the user had ever looked from.
### Manual seams and island cutting
`TextureDisplacementLayer::lscm_seam_edges` - undirected mesh-vertex-index edge pairs the unwrap is
forced to cut along, on top of the dihedral-angle seams. `segment_into_charts()` takes a set of these
(translated from mesh -> compacted-patch numbering inside `compute_patch_unwrap()`) and refuses to
union two triangles across a marked edge whatever their angle. Both the unwrap cache key and the
gizmo's `UVEditorState` include the seam list, so marking a seam (which leaves the paint mask
untouched) still forces a re-solve. Like the paint masks, seams are mesh-index-space and so dropped on
any topology change.
Two ways to write to it:
- **Mark seam (manual)** - a "Mark seams" click mode (`m_seam_edit_mode`) that suppresses painting. A
click raycasts the volume (`m_c->raycaster()->raycasters()[idx]->unproject_on_mesh()`, `idx` = the
volume's slot among model-part volumes), finds the facet's edge nearest the hit point, and toggles it.
Marked edges render as a red overlay (`render_seam_overlay()`), pulled toward the camera so they read
on top. This is the Blender mark-seam workflow.
- **Cut island (auto)** - `cut_island()` takes the selected chart's triangles (back-mapped from the
unwrap via `source_vertex`), finds their 3D bounding box, and marks every edge that straddles the
mid-plane perpendicular to the longest axis. The re-unwrap then splits the chart across its narrow
waist. Exposed as the UV pane's **Cut** button.
### UV-check overlays (checker / distortion)
`resources/shaders/{110,140}/texture_displacement_uvcheck.{vs,fs}`, one shader with a `mode` uniform,
drawn over the painted patch (`rebuild_uvcheck_mesh()`/`render_uvcheck_mesh()`, P3N3T2: `normal.x` =
distortion, `tex_coord` = uv), pulled forward with a polygon offset. **Checker** samples a procedural
checkerboard at the layer's uv (per-vertex for LSCM/ViewProjected, in-shader triplanar otherwise) -
squares that stay square mean low distortion. **Distortion** colours each triangle blue->green->red by
`log2(uv_area / surface_area)` centred on the patch's *median* stretch (so a globally-scaled unwrap
reads as uniformly ideal and only relative stretch shows), averaged to vertices. A separate **Show mesh
wireframe** toggle draws the whole volume's triangle edges, rebuilt only when the vertex count changes
(not per stroke).
### Tiling
`DecodedHeightTexture::sample(uv, tile_enabled, tile_method)`. Two tile methods when enabled
(Repeat, MirroredRepeat). **When `tile_enabled` is false, sampling outside `[0,1)` returns `0`
directly** rather than clamping the *coordinate* into range, which would smear the border row/column of
pixels outward to infinity in every direction (streaky lines radiating out from the painted patch).
### Subdivision - two modes
**Uniform (`subdivide_mesh_uniform()`)** - whole-mesh, 1-to-4 split. Recursive edge-midpoint split with
a shared per-pass midpoint cache (keyed by sorted vertex-index pair) so triangles sharing an edge get
the *same* new vertex - capped at `max_iterations` (default 6). Whole-mesh so it never leaves a
T-junction, at the cost of densifying everywhere. Wired as a "Subdivide steps" slider (**0-5**, 0 =
no subdivision), Apply snaps back to 0. Drops texture-displacement paint (no remap) via the standard
`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped.
**Adaptive (`subdivide_mesh_adaptive()`)** - refine **only the painted area**, by **Rivara longest-edge
bisection**, which is *conformal by construction*. Only **terminal** edges are ever bisected - an edge
that is the longest edge of *every* triangle sharing it - which splits both those triangles along one
shared midpoint at once, so a hanging node is never created. The edge to split for a triangle that wants
refining is found by **longest-edge propagation (LEPP)**: walk to the longest edge of ever-longer-edged
neighbours until a terminal one is reached, and bisect that. Edge length strictly increases along the
path (ties broken by mesh-vertex key, which both sides of an edge compute identically), so the walk
cannot cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number
of bisections. The transition triangles it pulls in just outside the painted patch are the graded band
that makes the size change conformal.
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
**Run to completion, worst-first, against a triangle budget.** The refinement loop is not a fixed number
of sweeps: it holds every triangle that is over its criteria in a max-heap keyed by *how many times over*
it is, pops the worst, walks its LEPP, bisects, and re-scores. Edge adjacency (`nb[e]`, the triangle
across each edge) is built **once** and maintained incrementally through each bisection, so the cost
scales with the refined region rather than with the whole model. `max_triangles` is the only bound;
stopping on it leaves a perfectly valid, still-conformal mesh that spent its budget on the largest
errors. A fixed sweep count instead spends itself grading the *coarse surroundings* - whose edges are
the longest, so they win every terminal-edge contest - and never reaches the painted patch.
**It carries the paint forward**, which is what makes it usable (uniform subdivide drops paint). Because
the refinement is *driven by* the paint, the remap is trivial: `subdivide_mesh_adaptive()` fills an
`out_source[new_tri] = input_tri` map (children inherit their parent), and the gizmo rebuilds each
layer's mask on the new mesh - a new triangle is painted iff its source was fully painted in that
layer. `collect_paint_region()` derives both:
- the union refine-region: **exactly** the original triangles the brush touched, read straight off
`TriangleSplittingData::triangles_to_split` (`serialize()` records an entry per original triangle that
is either split - i.e. partially painted, the patch boundary - or carries a non-default state). No
dilation: marking every triangle that shares a *vertex* with the patch drags in a whole fan of huge
unpainted neighbours and refines *those* down to the resolution floor, since the height field the
detail test samples is not restricted to the painted area. The conformal closure already grades the
size change outward on its own.
- the per-layer fully-painted-triangle sets (a `get_facets_strict(ENFORCER)` sub-triangle with all three
*original* vertex indices == a whole, fully-painted original triangle; a partial stroke's sub-triangles
always carry a split vertex).
The other four channels ride the normal `restore_painting()` remap.
Both modes share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox
picks the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the
wireframe while the subdivide preview is open in adaptive mode). The panel shows the previewed triangle
count.
**Feature-adaptive (follow texture detail).** A sub-mode of adaptive (the **"Follow texture detail"**
checkbox) that puts triangles where the *displaced surface actually bends*, not evenly. A flat region or
a linear **ramp** needs no extra vertices (linear interpolation is exact for a ramp); what needs them is
**curvature** - the *second* derivative, not the gradient. So the extra predicate is a **chord-error**
test: sample the combined displacement at the triangle's three edge midpoints *and its centroid*
(sampling the interior is what catches a hill sitting inside a triangle, the blind spot of an edge-only
test) and take the largest departure from the flat triangle's barycentric interpolation. Refine while
that exceeds `chord_tolerance_mm` ("Detail (mm)"). Zero chord error on a ramp => untouched; high on a
hill/ridge/noise => refined until captured. Same conformal machinery, so still crack-free. The
per-triangle error is cached and recomputed only for the children of a split.
Four knobs bracket it, and all four matter:
- **"Max edge (mm)"** (`target_edge_length_mm`) is a **baseline that applies in feature mode too**.
Without it the chord test aliases: a big triangle over a fine pattern can sample four points that all
land at similar heights, report no error, and stall before refinement ever starts. The baseline
guarantees a sampling density fine enough for the curvature test to see the texture at all.
- **"Detail (mm)"** is the chord tolerance above.
- **"Min edge (mm)"** is a hard floor under both, and is what guarantees termination across a sharp
texture *step*, where the error never falls however fine the mesh gets.
- **"Added triangles (k)"** is the budget, passed as `max_triangles` (the model's own triangle count plus
the slider, so the control still means something on an already-dense model).
The height field is `make_combined_displacement_sampler()` - it mirrors `build_texture_displacement()`'s
per-layer setup (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively")
but evaluated per point. Two deliberate simplifications, both erring toward *more* detail (safe -
over-refinement is never a crack): every sampleable layer is sampled at every point (no per-point paint
test), and edge-smoothing falloff is ignored. The first is *why* the refine region must not be dilated -
outside the paint the sampler still reports full relief. **LSCM layers are skipped** (no per-point UV); a
purely LSCM stack yields a null sampler and the code falls back to the length baseline alone. Per-vertex
heights are sampled lazily, so a small patch on a huge model never pays for the rest of it.
### Fast preview (GPU-only, no CPU meshing)
`resources/shaders/{110,140}/texture_displacement_shaded.{vs,fs}`, registered as
`"texture_displacement_shaded"`. Shades the *displaced* surface without moving geometry - active-layer
only, selected from the View row, and the default when the gizmo opens (`m_use_shaded_preview = true`).
Vertex format is `GLModel::Geometry::EVertexLayout::P3N3T2`: `normal.x` carries the per-vertex paint
weight (0/1), `normal.y` flags the UV island currently being dragged, and `tex_coord` carries a
precomputed texture UV, so it can use `GLModel` normally instead of a hand-rolled VBO/VAO manager.
The mesh is **flat** (vertices not shared between triangles): every corner of a painted triangle gets
weight 1, every corner of an unpainted one weight 0. A coarse mesh needs that - one painted face of a raw
cube has no strictly-interior vertex, so per-vertex weighting would either bleed onto the neighbours or
vanish outright. Duplicating vertices costs no shading quality here because the shader takes its surface
normal from screen-space derivatives of position, not from a per-vertex normal.
**Both preview meshes work in the patch's vertex space, not the mesh's.** Those agree only until a
*brush* stroke splits a triangle: `get_facets_strict()` then appends the split vertices, so the patch
array is longer. `rebuild_shaded_preview_mesh()` and `rebuild_uvcheck_mesh()` therefore index
`patch.vertices` throughout. The weight buffer is rebuilt at the same cadence as the true-displacement
preview (stroke-end/slider-release) but from the **live** `TriangleSelector` state, not the flushed model
facets, so it does not lag by a full model round-trip.
The perturbed normal is the analytic one for a height field `H = +/-depth_mm - h(uv)` displaced along
`N` over any orthonormal surface tangent pair `T`/`B`:
N' = normalize(N - (dH/da)-T - (dH/db)-B), a = dot(p,T), b = dot(p,B)
The two slopes have to be genuine **mm-per-mm** derivatives for the preview's apparent depth to match
the bake's.
**Two projection paths (`use_vertex_uv` uniform):**
- **Triplanar (`use_vertex_uv = 0`)** - `uv` and the `T`/`B` axes are both derived in-shader from
the dominant normal component, mirroring `project_planar()`/`apply_uv_transform()`, and the slope is
formed analytically. `T`/`B` are the projection's axis-aligned pair, exact only when the face is
axis-aligned; the shader drops the along-normal component to keep the gradient in the surface. Here
one `uv` unit is exactly `tiling_scale` mm, so the `1/tiling_scale` gradient factor is right.
- **Precomputed UV (`use_vertex_uv = 1`, used for LSCM and ViewProjected)** - `uv` comes per-vertex from
the CPU (`compute_layer_vertex_uvs()`, so island placement + tiling/rotation/offset are already folded
in), and the perturbed normal is built with **Mikkelsen's method** ("Bump Mapping Unparametrized
Surfaces on the GPU"): the surface gradient taken directly from the screen-space derivatives of the
*sampled height* and position. **This makes no uv->mm scale assumption**, which is essential, because an
LSCM map is **conformal, not isometric**: it is globally area-scaled but the *local* mm-per-uv varies
across the chart, so a single global `1/tiling_scale` factor gets the apparent depth wrong. `dFdx(h)`
captures the true on-screen rate of change however the chart is stretched. This path is also what makes
the fast preview follow the UV editor: move an island and its uv - hence its shading - moves with it
(the mesh rebuilds on drag-end, `on_island_edited(finished)` -> `rebuild_preview()` ->
`rebuild_shaded_preview_mesh()`). The branch is uniform and the paint weight gates by multiply, so the
texture derivatives stay well defined. A triangle straddling a seam has a discontinuous uv -> the
`det~0` guard skips it (a localised preview-only artifact, never in the bake).
**Parallax (triplanar path).** Perturbing the shading normal alone welds the pattern to the base surface:
it does not slide as the camera orbits, and does not get deeper as `depth_mm` grows. The triplanar path
therefore shades at the point the *displaced* surface would show at this pixel, found by **ray marching**
(parallax occlusion mapping). A point at ray parameter `s`, i.e. `P + V-s` (`P` the base point, `V` the
unit direction to the eye), sits at height `s-dot(V,n)` above the undisplaced surface. The displaced
surface lives in a shell between the extreme values of `amp-(h - midlevel)` - taken from both ends of
`h in [0,1]`, so it holds for an inverted layer and a raised midlevel too, where the surface sits *below*
the undisplaced one. The march starts at the top of that shell, where the ray is outside the surface by
construction, and steps inward until the ray height drops below the sampled height. That crossing *is*
the visible point.
Solving `Q = P + V-(H(Q)/dot(V,n))` by fixed-point iteration instead is geometrically exact but the
divisor goes to zero edge-on; the sample then lands a large fraction of a tile away and the iteration
oscillates, which reads as a second, flat copy of the pattern ghosted over the real one. Clamping the
step to one tile does not help - a tile-sized shift lands on the neighbouring tile, the same pattern
again. Offset limiting (stepping along the tangential part of `V`) is stable but understates parallax
enough that the relief still flattens as soon as the camera tilts. Marching has neither problem.
The hit is interpolated between the last two samples, which keeps `PARALLAX_STEPS` (24) affordable, and
the whole march is skipped when sweeping the shell would move the sample point less than half a texel -
the head-on case, so the common view pays almost nothing. The 140 variant samples with
`textureLod(..., 0.0)` inside the loop, since implicit derivatives are undefined in non-uniform control
flow. Two uniforms exist for this: `midlevel` (parallax needs the real height, not just its derivative)
and `eye_model_pos` (the camera in the volume's local frame).
Parallax cannot change the model's silhouette or cast shadows; the View row's Normal mode is one click
away for that. The LSCM path stays plain Mikkelsen normal perturbation - it has no closed-form uv, so there is no cheap
way to re-project a marched position. One further approximation: the GPU sampler's wrap mode stands in
for `tile_enabled`/`tile_method`, so with tiling *off* the GPU repeats where the CPU returns 0 outside
`[0,1)`.
### On-canvas "Adjust Texture" gizmo
A per-active-layer toggle ("Adjust placement") that disables painting and shows a flat pan panel (free
2D drag on both axes) plus two arrows along the patch's own U/V axes (constrained single-axis drag).
Anchored to the painted patch's centroid/average-normal (`compute_layer_paint_anchor()`). Hit-testing is
screen-space distance/point-to-segment, not real 3D ray intersection against the handle geometry - simple
and good enough at this handle size.
### Projection frame overlay (ViewProjected)
`src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - a semi-transparent, resizable `wxFrame` the user
drags **over the 3D view**, like a slide projector's gate. Whatever the model shows through it is what
the texture is projected onto, and the window's border becomes the hard edge of the displacement.
Press **Apply projection frame** and the gizmo reads the window's rectangle and commits it.
The window is deliberately **dumb**: it owns no placement state and reports nothing continuously. Its
position and size *are* the placement, read on demand at Apply - which is also when the expensive
visible-facet raycast runs. So dragging it is free and nothing recomputes until asked.
Plain 2D (`wxPaintDC`), not a `wxGLCanvas`: a second GL canvas would have to share the app's one real
`wxGLContext`. It only ever draws a bitmap and a border.
**The projective mapping (`apply_projection_frame()`)**. The frame defines a **screen-space** rectangle,
but the bake samples from a **local-space** position, so the two have to be reconciled.
`view_project_right/up` can only express an *affine* projection - exact under an orthographic camera, but
wrong under perspective, where the near end of a part projects larger than the far end and no pair of
axes reproduces that. So the layer instead stores a full projective map (`view_project_matrix`, row-major
3x4, `uv = (row0-p/row2-p, row1-p/row2-p)`), built like this:
- `K = projection - view - (instance - volume)`, i.e. local -> clip, the same product the renderer uses.
Note `Camera::get_projection_matrix()` is typed `Transform3d` (nominally affine) but its perspective
form explicitly writes a `(0, 0, -1, 0)` bottom row into the underlying 4x4, so `clip.w = -z_eye` is
genuinely carried. The build therefore multiplies **`.matrix()` products** (plain `Matrix4d`), never
`Transform3d` products, which would not compose that row correctly.
- Window coordinates follow `igl::project`'s convention (as `CameraUtils::project` does), with y
measured downward. Writing `uv = (win - rect_origin) / rect_size` makes u and v affine in
`ndc = clip.xyz / clip.w`; multiplying through by `clip.w` leaves a plain linear combination of `K`'s
rows, which is exactly the 3x4 matrix - the perspective divide survives intact.
- `w > 0` is checked rather than divided blindly. A point behind the projector has `w < 0` and divides
to a plausible-looking but **mirrored** uv - the classic way a projected decal reappears on the back
of a model. `project_uv_projective()` returns false there and the caller treats it as no height.
The map already includes placement, so `apply_uv_transform()` is **not** applied on top of it - the
window's own position and size are the placement, and the tiling/rotation/offset sliders would shove
the result off the frame the user just aligned. A "Clear" button drops back to the affine path where
those controls mean something again.
Apply also sets `tile_enabled = false`, so `DecodedHeightTexture::sample()` returns 0 outside `[0,1)`
and the border is a hard edge rather than the first seam of an endless repeat, and repaints the layer
via `select_visible_faces(&matrix)` - the frame's uv square clips the selection, which both matches the
paint to the border and keeps the ray queries proportional to the framed area instead of the model.
Owned by the gizmo and **destroyed** (not just hidden) in `on_shutdown()`. Closing it only hides it, so
reopening keeps it where it was left.
### UV Editor pane
`UVEditorCanvas` (`src/slic3r/GUI/UVEditorCanvas.hpp/.cpp`) - a standalone `wxGLCanvas` rendering the
flattened LSCM islands (per-island wireframe + outline + fill) over the height texture (background
quad tiled across the whole unwrap), with mouse pan/zoom. It is wrapped in a **`UVEditorPanel`**
(same file) that adds a button row (Frame / Snap / Avg scale / Cut / Join / Unjoin) and a status line
along the bottom naming the current gesture and the shortcuts in play. The *panel* is what is
registered as a `wxAuiPaneInfo` pane on `Plater`'s `m_aui_mgr`; `Plater::show_uv_editor(bool)`
shows/hides it (deferred via `CallAfter`, since the gizmo calls it mid-3D-frame), and
`get_uv_editor_canvas()` returns the inner canvas the gizmo talks to.
Deliberately **shares the app's one real `wxGLContext`** (`wxGetApp().init_glcontext(*this)`, the
same call `View3D`/`Preview`/`AssembleView` make) rather than creating an independent context like
`SkipPartCanvas` does elsewhere in this codebase - this is what lets it reuse the already-registered
`"flat"`/`"flat_texture"` shaders and `GLModel` as-is, instead of needing its own shader
compilation/VBO management.
**Geometry is uploaded once, in the unwrap's own (raw, mm) coordinates**, one `GLModel` set per island;
each island is then drawn through its own 2x3 affine (`island_transform_matrix()` composed with the
layer's tiling/rotation/offset) passed as the `flat` shader's `view_model_matrix`. A drag updates one
matrix per island and touches no vertex buffer - `on_island_edited(!finished)` calls only
`set_island_transforms()`, and the full `set_islands()` rebuild happens solely when the unwrap itself
changes (`unwrap_changed` in `update_uv_editor()`).
**Gestures** (canvas-owned, reported to the gizmo as incremental deltas via `IslandEditFn`): left-drag
= move, right-drag or **R** = rotate (hold **Shift** to snap to 15° steps - quantised on the
*cumulative* rotation, not each delta, so it doesn't judder, and accumulated incrementally so it
survives crossing +/-180°), **S** = scale (R/S modal, click/Enter to confirm, Esc to cancel), wheel =
zoom about the cursor, middle-drag = pan, **Home**/**F** = frame all. Scale writes
`TextureIsland::scale`; "Avg scale" (`average_island_scales()`) sets every island to the mean, so
one island scaled by hand can be matched back to its neighbours' texel density. **Snap** (canvas-owned
`m_snap_enabled`, toggled from the toolbar) sticks a dragged island's nearest boundary vertex onto a
neighbouring island's at drag-*end* only - a magnet that re-applies mid-drag is very hard to pull out
of. Toolbar commands the canvas can't service itself (Avg scale, Cut, Join, Unjoin) are forwarded to the
gizmo via `CommandFn`; view-only ones (Frame, Snap) it handles directly.
## File map
**libslic3r (core, no GUI dependency):**
- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods,
tiling, subdivision (uniform + adaptive longest-edge bisection), post-process smoothing
(`smooth_mesh_vertices()`), and `TextureDisplacementOptions` (the whole-stack settings). See doc
comments throughout, they're kept accurate and up to date.
- `src/libslic3r/MeshBoolean.hpp/.cpp` - `parameterize_lscm()` and `remesh_isotropic()` in the `cgal`
sub-namespace, reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already
there for mesh boolean ops. CGAL includes: `Polygon_mesh_processing/border.h`,
`Polygon_mesh_processing/connected_components.h`, `Surface_mesh_parameterization/{Error_code,
LSCM_parameterizer_3, parameterize}.h`. No new dependency - CGAL 5.6.3 is already vendored and the
`Surface_mesh_parameterization` package headers were already present.
- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor,
`texture_displacement_layers`, `texture_displacement_options`, and all the mirrored touch points
(see Data model above).
**GUI:**
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo and its whole panel.
- `src/slic3r/GUI/TextureLibrary.hpp/.cpp` - scans the shipped + user texture folders, imports an
arbitrary image into the user folder (converting it to the 8-bit grayscale PNG libslic3r decodes),
and loads a library file's bytes for a layer. The image->grayscale-PNG conversion lives here, on the
GUI side, because libslic3r has no image toolkit; both the import path and the "pick a shipped
texture" path go through the same one function.
- `resources/textures/displacement/*.png` - the 10 shipped height maps (Bricks, Grid, Hexagons,
Knurl, Noise, Quilt, Studs, Waves, Weave, Wood Grain). All 512x512 8-bit grayscale and **seamless**
(each is periodic over the full image in both axes, so tiling shows no seam). Generated
procedurally; the whole `resources/` tree is installed recursively by CMake, so a new folder under
it ships with no build-system change.
- `src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp/.cpp` - background bake commit.
- `src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp/.cpp` - background preview compute
(mirrors the bake job's shape but commits nothing to the Model).
- `src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - the semi-transparent projection-frame overlay for
ViewProjected layers (plain 2D `wxPaintDC`, no GL context - see its section above).
- `src/slic3r/GUI/UVEditorCanvas.hpp/.cpp` - the 2D UV unwrap viewer widget.
- `src/slic3r/GUI/Plater.hpp/.cpp` - `uv_editor_canvas` member, AUI pane registration,
`get_uv_editor_canvas()`/`show_uv_editor()`.
- `src/slic3r/GUI/GLShadersManager.cpp` - registers `"texture_displacement_shaded"`.
- `resources/shaders/{110,140}/texture_displacement_shaded.{vs,fs}` - the fast-preview shader.
- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` - `PainterGizmoType::TEXTURE_DISPLACEMENT`.
- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` - `EType::TextureDisplacement` registration.
## Tests
`tests/libslic3r/test_texture_displacement.cpp`. Covers `decode_height_texture` round-trip, empty-layer
no-op, full-cube uniform displacement, a second layer over the same area contributing, all four blend
modes (table-driven), the lowest layer ignoring its blend mode, border displace/pin, post-process
smoothing and its mask guarantees, and adaptive subdivision: conformality (`every_edge_used_twice` on a
partially-refined cube - an exact crack detector for a closed mesh), the target edge length actually
being reached, the triangle budget capping the result without opening a crack, curvature-driven
refinement (a Gaussian hill refines at its centre, a linear ramp adds nothing), and the max-edge
baseline.
`BUILD_TESTS` is `OFF` in the checked-in build cache; flip it on to run them:
cmake -S . -B build -DBUILD_TESTS=ON
cmake --build build --config Release --target libslic3r_tests -- -m
./build/tests/libslic3r/Release/libslic3r_tests.exe "[TextureDisplacement]" --order rand
-326
View File
@@ -1,326 +0,0 @@
# Texture Displacement - Feature & Controls Guide
Texture Displacement is a paint-style gizmo that stamps height-map textures onto a model's surface
and turns them into real relief - engraved or embossed detail - either as a live preview or baked
into actual mesh geometry. You paint where the texture applies, stack multiple textures as blended
layers, choose how each is projected onto the surface, and (for the unwrap projection) lay the result
out by hand in a dedicated 2D **UV Editor** pane.
This document describes every feature and control. For the internal architecture and algorithms, see
`TEXTURE_DISPLACEMENT.md`.
---
## Table of contents
1. [Quick start](#quick-start)
2. [Entering the tool](#entering-the-tool)
3. [Selection modes](#selection-modes)
4. [View modes](#view-modes)
5. [Auto update](#auto-update)
6. [Texture layers](#texture-layers)
7. [Per-layer settings](#per-layer-settings)
8. [Projection methods](#projection-methods)
9. [The UV Editor](#the-uv-editor)
10. [Seams](#seams)
11. [Adjust placement (on-model)](#adjust-placement-on-model)
12. [Preparing the mesh: Subdivide & Remesh](#preparing-the-mesh-subdivide--remesh)
13. [Baking & resetting](#baking--resetting)
14. [Controls reference](#controls-reference)
15. [Tips & limitations](#tips--limitations)
---
## Quick start
1. Select an object and open the **Texture displacement** gizmo from the left toolbar.
2. A texture layer is added automatically. Pick a texture from the layer's picker, or import your own.
3. **Paint** the area you want the texture to affect (or press **Select whole model**).
4. The relief appears live on the model. Tune **Depth**, **Tile size**, **Rotation**, etc.
5. If the model is low-poly, use **Subdivide** or **Remesh** so there are enough vertices for detail.
6. Press **Bake** to convert the preview into real geometry, or leave it as a live preview.
> The tool only ever affects the **painted** area. Everything you don't paint keeps its original
> surface, and bake blends the relief seamlessly into it.
---
## Entering the tool
The gizmo lives on the left gizmo toolbar (icon: `toolbar_texture_displacement.svg`). Its settings
panel opens beside the toolbar. You can **Dock panel / Undock panel** (top of the panel) to pin it or
float it freely over the 3D view, and **Close** at the bottom exits the gizmo.
When you first open the tool on a never-textured object it starts with **one texture layer already
added**, so you can paint straight away.
---
## Selection modes
Choose *how* you paint. All three write into the **active layer's** mask.
| Mode | What it does |
|------|--------------|
| **Brush** | Free-hand painting with a round brush. Shows a **Brush size** slider and a **Circle / Sphere** choice (circle = surface disc, sphere = 3D ball that also paints around curves). |
| **Face** | Click a single triangle to paint it. |
| **Connected area** | Click to flood-fill a region; the **Angle threshold** slider limits how far the fill spreads across changes in surface angle. |
- **Select whole model** - marks the entire model as painted for the active layer, instead of
brushing it by hand.
---
## View modes
A row of icon buttons labelled **View** controls how the painted area is shown. The first four are a
radio group; **Wireframe** is an independent toggle. Hover any icon for its tooltip.
| View | Meaning |
|------|---------|
| **Normal** | The true displaced geometry - exactly what **Bake** produces. Rebuilt in the background. |
| **Fast** | A GPU shaded approximation of the *active layer only*. No real geometry movement - quick to update, not exact. Best while tuning or dragging islands. |
| **Checker** | A test grid painted over the unwrap so you can see stretching (squares stay square where the map isn't distorted). |
| **Distortion** | A blue->green->red heatmap of how much each area is compressed or stretched in UV space. Needs the **Unwrap (LSCM)** projection. |
| **Wireframe** | Overlays the mesh edges (white). Independent of the view above; in **Normal** view it sits on the displaced surface. |
---
## Auto update
**Auto update** (on by default) rebuilds the true displaced geometry as soon as *anything* changes -
painting, swapping textures, moving sliders. Turn it off on very heavy models to only rebuild when you
release a slider (painting still updates on stroke end).
---
## Texture layers
You can stack up to **8** texture layers. Each has its own independent paint mask, its own texture,
and its own parameters, and they combine in slot order like layers in an image editor.
- **Add a layer** - the **+ icon** to the right of the *Texture layers* heading (reuses the tool icon
for now).
- **Remove** - the button on each layer's header row.
- **Active layer** - click a layer's header (or anywhere in its block) to make it active. The active
layer is the one you paint into and the one whose block is tinted. Only one layer is active at a time.
- **Erase all** - clears the active layer's paint.
Each layer shows a texture **picker** (large preview + name). Open it to choose from the shipped
library or import your own image (any png/jpg/bmp; it's converted to an 8-bit grayscale height map and
copied into your user texture folder so app updates can't overwrite it).
---
## Per-layer settings
| Control | Range / options | What it does |
|---------|-----------------|--------------|
| **Depth (mm)** | 0.01-10 (log) | Maximum displacement along the surface normal. |
| **Tile size (mm)** | 0.2-200 (log) | Physical size of one texture tile on the surface. |
| **Rotation** | 0-360° | Rotates the texture on the surface. |
| **Midlevel** | 0-10 | The grey level that means "don't move". At 0 the texture only pushes outward; raise it and darker texels cut *inward* (one map both embosses and engraves). 0.5 makes mid-grey neutral. |
| **Smoothing** | 0-1 | Blurs the height texture before it displaces - rounds hard edges and removes speckle without needing a softer source image. |
| **Edge smoothing** | checkbox + **Edge amount** 0-1 | Fades the relief to flat toward the *edge of the painted area*, so it blends into the surrounding surface. A small amount softens only a thin band at the very edge; the maximum flattens the whole painted face. |
| **Invert** | checkbox | Flips the height map (peaks become valleys). |
| **Blend** | Add / Subtract / Multiply / Divide | How this layer combines with the layers **below** it where they overlap. Add/Subtract pile relief on or carve it away; Multiply/Divide scale the relief underneath (a mask). The lowest painted layer is the **Base** and always behaves additively. |
| **Tile** | checkbox + **Repeat / Mirrored repeat** | When off, the texture is placed once (a decal) instead of repeating. Mirrored repeat flips every other tile to hide seams. |
| **Projection** | see below | How the texture is mapped onto the painted surface. |
> **Midlevel warning:** cutting inward can fold the surface through itself in sharp concave corners or
> thin walls. Keep Depth small relative to the feature you're cutting into; the panel warns when a deep
> inward setting is risky.
---
## Projection methods
How the 2D texture is wrapped onto the 3D painted area.
| Method | Best for | Notes |
|--------|----------|-------|
| **Triplanar (blended)** | Patches wrapping around edges | Projects from all three axes at once and blends, so there's no seam across a sharp edge. |
| **Cylindrical** | Round, tube-like selections | Wraps the texture around the patch's own centre/axis. |
| **Spherical** | Ball-like selections | Longitude/latitude wrap around the patch centre. |
| **Unwrap (LSCM)** | Flat, controlled layout | A real conformal unwrap. Cuts the area into pieces at sharp edges (see **Seam angle**), flattens each, and lets you lay them out by hand in the **UV Editor**. Unlocks Checker/Distortion, seams, and island editing. |
| **From view** | Decals / slide-projector look | Projects straight onto the surface from the current camera direction. Use **Capture current view** to re-lay it from wherever you're looking. |
### LSCM-only controls
These appear when a layer uses **Unwrap (LSCM)**:
- **Seam angle** (5-90°) - edges sharper than this are cut so each piece lies flat. Lower cuts more
(less stretching, more seams); raise to keep more in one piece. A box's 90° corners are cut by
default. *Ignored once you've marked any seam by hand* (your seams then define the pieces).
- **Connect islands** (on by default) - lays the unwrap out as a **connected net**: pieces that share
an edge are unfolded next to each other (a cube becomes a joined net instead of six loose squares).
They stay separate islands, so you can still move any of them by hand. Turn off for the classic
packed-grid layout.
- **Open UV editor** - shows the flattened unwrap in a side pane (see below). Opens *only* when you
turn this on - it never pops up on its own.
- **Mark seams** / **Path** / **Clear seams** - see [Seams](#seams).
- An **Unwrap: N islands, F faces, V verts** read-out tells you what the unwrap actually produced.
---
## The UV Editor
A dockable 2D pane (enable **Open UV editor** on an LSCM layer) showing the flattened unwrap over the
height texture. Islands are the flattened pieces; you can rearrange them freely - nothing re-packs them
behind your back. Moving an island updates the model **live** (in Fast view it tracks the cursor
smoothly, via a shader uniform - no rebuild until you release).
### Navigation
| Action | Control |
|--------|---------|
| Pan | Middle-drag |
| Zoom | Mouse wheel (zooms about the cursor) |
| Frame everything | **Home** or **F**, or the **Frame** toolbar button |
### Editing an island
| Action | Control |
|--------|---------|
| Select | Left-click an island |
| Move | Left-drag |
| Rotate | Right-drag, or press **R** then move the mouse (click/Enter to confirm, Esc to cancel) |
| Rotate snapped | Hold **Shift** while rotating - snaps to **global** 15° marks (0/15/30...). A protractor dial with tick marks and the current angle is shown. |
| Scale | Press **S** then move the mouse (click/Enter to confirm, Esc to cancel) |
| Undo / Redo | **Ctrl+Z** / **Ctrl+Shift+Z** or **Ctrl+Y** |
The **selected** island gets a bold light-green outline and a brighter wireframe; unselected islands
are a translucent light-green wash. The texture underneath repeats exactly as it will when baked.
A **status line** along the bottom always names the current gesture and the shortcuts in play.
### Toolbar
| Button | Action |
|--------|--------|
| **Frame** | Frame all islands (same as Home). |
| **Snap** | Toggle magnetic snapping - a dragged island sticks its boundary to a neighbour's when they come close. |
| **Avg scale** | Give every island the same texel density (Blender's "Average Islands Scale"). |
| **Cut** | Split the selected island across its long axis (useful for very long islands). |
| **Join** | Unfold the selected island onto its nearest neighbour along their shared edge - keeps both as separate islands with their own borders. |
| **Unjoin** | Send the selected island back to its own packed position. |
> **Checker / Distortion in the UV editor:** selecting those View modes also colours the UV pane - a
> checker background, or a per-island distortion heatmap - so you can judge stretch in 2D as well as
> on the model.
---
## Seams
Seams are edges the unwrap is forced to cut along, on top of whatever the Seam angle cuts - the
Blender "mark seam" workflow. They let you control exactly where the unwrap splits.
Enable **Mark seams** on an active LSCM layer, then:
- **Click an edge** on the model to mark it (it turns **red**); click a red edge again to unmark it.
The edge under the cursor is highlighted **yellow** so you can see what a click will toggle.
- **Path mode** (the **Path** checkbox) - for dense meshes where clicking each edge is tedious: click a
start point, then an end point, and the whole **shortest path** between them is seamed at once. It
chains (each click extends from the last point); the start vertex is shown in **green**.
- **Ctrl+drag** rotates/pans the camera while in seam mode.
- **Clear seams** removes them all.
Once any seam is marked, the automatic Seam-angle cutting is disabled so *your* seams define the
islands - pieces you leave un-seamed merge together.
---
## Adjust placement (on-model)
**Adjust placement** (on an active layer) lets you position the texture by dragging a handle on the
model instead of nudging the Rotation/offset numbers. The handle is a flat panel in the patch's
tangent plane (drag anywhere on it to move freely) plus U/V arrows for single-axis nudges. It's
anchored to the painted patch, so paint something first.
---
## Preparing the mesh: Subdivide & Remesh
Displacement can only move vertices that exist, so a coarse model needs more of them first.
### Subdivide
Splits every triangle into four, **1-5 times** (each step roughly quadruples the triangle count).
- **Subdivide steps** (1-5) - how many times to split.
- **Preview subdivision** - shows the result as a **cyan wireframe** without changing the model.
- **Apply** - commits the subdivision to the geometry.
- **Done** - ends the preview and leaves the model as it is.
### Remesh
Rebuilds the whole model with triangles close to a target edge length - evens out a mesh with wildly
varying triangle sizes (CGAL isotropic remeshing).
- **Target edge (mm)** - desired triangle edge length (seeded to the model's current average).
- **Remesh** - splits the big triangles and merges the small ones to that size.
> Both Subdivide-Apply and Remesh **replace the geometry** and clear any *not-yet-baked* paint on it
> (already-baked relief is kept). If you had the mesh **Wireframe** on before, it stays on afterward.
---
## Baking & resetting
- **Bake** - converts the current preview into real, permanent mesh geometry, restricted to the
painted area. Runs in the background; the button shows *Baking...* while it works.
- **Erase all** - clears the active layer's paint.
Baking is the exact same algorithm as the **Normal** preview, so what you see is what you get.
---
## Controls reference
### Mouse - 3D view (while painting)
| Input | Action |
|-------|--------|
| Left-drag | Paint the active layer |
| Ctrl + drag | Rotate / pan the camera (works in seam mode too) |
| Wheel | Zoom |
### Mouse & keys - UV Editor
| Input | Action |
|-------|--------|
| Left-click | Select island |
| Left-drag | Move island |
| Right-drag | Rotate island |
| **R** / **S** | Modal rotate / scale (mouse drives it, click or Enter confirms, Esc cancels) |
| **Shift** (while rotating) | Snap to global 15° marks |
| Middle-drag | Pan |
| Wheel | Zoom about cursor |
| **Home** / **F** | Frame all islands |
| **Ctrl+Z** / **Ctrl+Shift+Z** / **Ctrl+Y** | Undo / redo |
### Seam mode
| Input | Action |
|-------|--------|
| Click edge | Mark / unmark a seam (yellow = hover, red = marked) |
| Click (Path mode) | Set start, then seam the shortest path to the next click |
| Ctrl + drag | Rotate / pan camera |
---
## Tips & limitations
- **Paint first, then bake.** The preview is free to explore; only Bake changes the real mesh.
- **Not enough detail?** Subdivide or Remesh before painting fine textures.
- **Inward cuts** (high Midlevel + big Depth) can self-intersect on thin walls or sharp concave
corners - keep Depth modest there.
- **Fast vs Normal:** Fast preview only shades the relief and shows only the active layer; use it for quick
tuning and smooth UV dragging, but trust **Normal**/**Bake** for the exact result.
- **Topology changes drop unbaked paint.** Subdivide-Apply, Remesh, and Simplify replace the mesh, and
texture-displacement paint isn't remapped across that change (already-baked relief is unaffected).
- **Island placements** are tied to the current unwrap. Re-painting or changing the Seam angle can
re-segment the charts and renumber them, so a re-unwrap re-lays the connected net and discards
hand placements made before it.
- **Connect islands** is on by default; turn it off (per layer) for the classic packed-grid layout, or
if an unfold looks wrong on an unusual mesh.
+52
View File
@@ -0,0 +1,52 @@
set WP=%CD%
set debug=OFF
set debuginfo=OFF
if "%1"=="debug" set debug=ON
if "%2"=="debug" set debug=ON
if "%1"=="debuginfo" set debuginfo=ON
if "%2"=="debuginfo" set debuginfo=ON
if "%debug%"=="ON" (
set build_type=Debug
set build_dir=build-dbg
) else (
if "%debuginfo%"=="ON" (
set build_type=RelWithDebInfo
set build_dir=build-dbginfo
) else (
set build_type=Release
set build_dir=build
)
)
echo build type set to %build_type%
cd deps
mkdir %build_dir%
cd %build_dir%
set DEPS=%CD%/OrcaSlicer_dep
set "SIG_FLAG="
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
if "%1"=="slicer" (
GOTO :slicer
)
echo "building deps.."
echo cmake ../ -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
cmake ../ -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target deps -- -m
if "%1"=="deps" exit /b 0
:slicer
echo "building Orca Slicer..."
cd %WP%
mkdir %build_dir%
cd %build_dir%
echo cmake .. -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type%
cmake .. -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=%build_type% %SIG_FLAG%
cmake --build . --config %build_type% --target ALL_BUILD -- -m
cd ..
call scripts/run_gettext.bat
cd %build_dir%
cmake --build . --target install --config %build_type%
+2 -2
View File
@@ -53,7 +53,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
echo " -s: Build slicer only" echo " -s: Build slicer only"
echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)" echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)"
echo " -n: Nightly build" echo " -n: Nightly build"
echo " -t: Specify minimum version of the target platform, default is 12.0" echo " -t: Specify minimum version of the target platform, default is 11.3"
echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode" echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode"
echo " -b: Build without reconfiguring CMake" echo " -b: Build without reconfiguring CMake"
echo " -c: Set CMake build configuration, default is Release" echo " -c: Set CMake build configuration, default is Release"
@@ -95,7 +95,7 @@ if [ -z "$DEPS_CMAKE_GENERATOR" ]; then
fi fi
if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then
export OSX_DEPLOYMENT_TARGET="12.0" export OSX_DEPLOYMENT_TARGET="11.3"
fi fi
if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then
+190
View File
@@ -0,0 +1,190 @@
@REM OrcaSlicer build script for Windows with VS auto-detect
@echo off
set WP=%CD%
set _START_TIME=%TIME%
@REM Default target architecture to the host CPU arch; override by passing
@REM "x64" or "arm64" as an argument. PROCESSOR_ARCHITEW6432 covers a 32-bit
@REM shell running on a 64-bit OS, where PROCESSOR_ARCHITECTURE reads "x86".
set arch=x64
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set arch=ARM64
if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" set arch=ARM64
if /I "%1"=="arm64" set arch=ARM64
if /I "%2"=="arm64" set arch=ARM64
if /I "%1"=="x64" set arch=x64
if /I "%2"=="x64" set arch=x64
@REM Check for Ninja Multi-Config option (-x)
set USE_NINJA=0
for %%a in (%*) do (
if "%%a"=="-x" set USE_NINJA=1
)
@REM Check for clang-cl option (-l). Combined with -x it also builds the deps with
@REM clang-cl; on the Visual Studio generator it applies to the slicer only, because
@REM the dependency sub-builds have no toolset to inherit and stay on MSVC.
set CLANG_ARG=
set TOOLSET_ARG=
for %%a in (%*) do (
if "%%a"=="-l" (
set CLANG_ARG=-DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl
set TOOLSET_ARG=-T ClangCL
)
)
@REM Check for unit-tests option ("tests")
set BUILD_TESTS=OFF
for %%a in (%*) do (
if /I "%%a"=="tests" set BUILD_TESTS=ON
)
if "%USE_NINJA%"=="1" (
echo Using Ninja Multi-Config generator
set CMAKE_GENERATOR="Ninja Multi-Config"
set VS_VERSION=Ninja
goto :generator_ready
)
@REM Detect Visual Studio version using msbuild
echo Detecting Visual Studio version using msbuild...
@REM Try to get MSBuild version - the output format varies by VS version
set VS_MAJOR=
for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do (
for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a
set MSBUILD_OUTPUT=%%i
goto :version_found
)
@REM Alternative method for newer MSBuild versions
if "%VS_MAJOR%"=="" (
for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do (
for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a
set MSBUILD_OUTPUT=%%i
goto :version_found
)
)
:version_found
echo MSBuild version detected: %MSBUILD_OUTPUT%
echo Major version: %VS_MAJOR%
if "%VS_MAJOR%"=="" (
echo Error: Could not determine Visual Studio version from msbuild
echo Please ensure Visual Studio and MSBuild are properly installed
exit /b 1
)
if "%VS_MAJOR%"=="16" (
set VS_VERSION=2019
set CMAKE_GENERATOR="Visual Studio 16 2019"
) else if "%VS_MAJOR%"=="17" (
set VS_VERSION=2022
set CMAKE_GENERATOR="Visual Studio 17 2022"
) else if "%VS_MAJOR%"=="18" (
set VS_VERSION=2026
set CMAKE_GENERATOR="Visual Studio 18 2026"
) else (
echo Error: Unsupported Visual Studio version: %VS_MAJOR%
echo Supported versions: VS2019 (16.8+^), VS2022 (17.x^), VS2026 (18.x^)
exit /b 1
)
echo Detected Visual Studio %VS_VERSION% (version %VS_MAJOR%)
echo Using CMake generator: %CMAKE_GENERATOR%
:generator_ready
@REM Pack deps
if "%1"=="pack" (
setlocal ENABLEDELAYEDEXPANSION
cd %WP%/deps/build
if "%arch%"=="ARM64" cd %WP%/deps/build-arm64
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do set build_date=%%c%%b%%a
echo packing deps: OrcaSlicer_dep_win-!arch!_!build_date!_vs!VS_VERSION!.zip
%WP%/tools/7z.exe a OrcaSlicer_dep_win-!arch!_!build_date!_vs!VS_VERSION!.zip OrcaSlicer_dep
goto :done
)
set debug=OFF
set debuginfo=OFF
if "%1"=="debug" set debug=ON
if "%2"=="debug" set debug=ON
if "%1"=="debuginfo" set debuginfo=ON
if "%2"=="debuginfo" set debuginfo=ON
if "%debug%"=="ON" (
set build_type=Debug
set build_dir=build-dbg
) else (
if "%debuginfo%"=="ON" (
set build_type=RelWithDebInfo
set build_dir=build-dbginfo
) else (
set build_type=Release
set build_dir=build
)
)
if "%arch%"=="ARM64" set build_dir=%build_dir%-arm64
echo build type set to %build_type%, arch=%arch%
setlocal DISABLEDELAYEDEXPANSION
cd deps
mkdir %build_dir%
cd %build_dir%
set "SIG_FLAG="
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
if "%1"=="slicer" (
GOTO :slicer
)
echo "building deps.."
if defined CLANG_ARG if "%USE_NINJA%"=="0" echo Note: -l needs -x for the dependencies; building them with MSVC.
echo on
REM Set minimum CMake policy to avoid <3.5 errors
set CMAKE_POLICY_VERSION_MINIMUM=3.5
if "%USE_NINJA%"=="1" (
cmake ../ -G %CMAKE_GENERATOR% %CLANG_ARG% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target deps
) else (
cmake ../ -G %CMAKE_GENERATOR% -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target deps -- -m
)
@echo off
if "%1"=="deps" goto :done
:slicer
echo "building Orca Slicer..."
cd %WP%
mkdir %build_dir%
cd %build_dir%
echo on
set CMAKE_POLICY_VERSION_MINIMUM=3.5
if "%USE_NINJA%"=="1" (
cmake .. -G %CMAKE_GENERATOR% %CLANG_ARG% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target all
) else (
cmake .. -G %CMAKE_GENERATOR% -A %arch% %TOOLSET_ARG% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target ALL_BUILD -- -m
)
@echo off
cd ..
call scripts/run_gettext.bat
cd %build_dir%
cmake --build . --target install --config %build_type%
:done
@echo off
for /f "tokens=1-3 delims=:.," %%a in ("%_START_TIME: =0%") do set /a "_start_s=%%a*3600+%%b*60+%%c"
for /f "tokens=1-3 delims=:.," %%a in ("%TIME: =0%") do set /a "_end_s=%%a*3600+%%b*60+%%c"
set /a "_elapsed=_end_s - _start_s"
if %_elapsed% lss 0 set /a "_elapsed+=86400"
set /a "_hours=_elapsed / 3600"
set /a "_remainder=_elapsed - _hours * 3600"
set /a "_mins=_remainder / 60"
set /a "_secs=_remainder - _mins * 60"
echo.
echo Build completed in %_hours%h %_mins%m %_secs%s
+80
View File
@@ -0,0 +1,80 @@
@REM OrcaSlicer build script for Windows
@echo off
set WP=%CD%
@REM Pack deps
if "%1"=="pack" (
setlocal ENABLEDELAYEDEXPANSION
cd %WP%/deps/build
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do set build_date=%%c%%b%%a
echo packing deps: OrcaSlicer_dep_win64_!build_date!_vs2022.zip
%WP%/tools/7z.exe a OrcaSlicer_dep_win64_!build_date!_vs2022.zip OrcaSlicer_dep
exit /b 0
)
set debug=OFF
set debuginfo=OFF
@REM Default target architecture to the host CPU arch; override with x64/arm64 arg.
set arch=x64
if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set arch=ARM64
if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" set arch=ARM64
if "%1"=="debug" set debug=ON
if "%2"=="debug" set debug=ON
if "%1"=="debuginfo" set debuginfo=ON
if "%2"=="debuginfo" set debuginfo=ON
if /I "%1"=="arm64" set arch=ARM64
if /I "%2"=="arm64" set arch=ARM64
if /I "%1"=="x64" set arch=x64
if /I "%2"=="x64" set arch=x64
if "%debug%"=="ON" (
set build_type=Debug
set build_dir=build-dbg
) else (
if "%debuginfo%"=="ON" (
set build_type=RelWithDebInfo
set build_dir=build-dbginfo
) else (
set build_type=Release
set build_dir=build
)
)
if "%arch%"=="ARM64" set build_dir=%build_dir%-arm64
echo build type set to %build_type%, arch=%arch%
setlocal DISABLEDELAYEDEXPANSION
cd deps
mkdir %build_dir%
cd %build_dir%
set "SIG_FLAG="
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
if "%1"=="slicer" (
GOTO :slicer
)
echo "building deps.."
echo on
REM Set minimum CMake policy to avoid <3.5 errors
set CMAKE_POLICY_VERSION_MINIMUM=3.5
cmake ../ -G "Visual Studio 17 2022" -A %arch% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target deps -- -m
@echo off
if "%1"=="deps" exit /b 0
:slicer
echo "building Orca Slicer..."
cd %WP%
mkdir %build_dir%
cd %build_dir%
echo on
set CMAKE_POLICY_VERSION_MINIMUM=3.5
cmake .. -G "Visual Studio 17 2022" -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target ALL_BUILD -- -m
@echo off
cd ..
call scripts/run_gettext.bat
cd %build_dir%
cmake --build . --target install --config %build_type%
-10
View File
@@ -1,10 +0,0 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
# no headers for that object. ccache reproduces /showIncludes output on a hit.
foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
endif ()
endforeach ()
-8
View File
@@ -27,15 +27,8 @@ endif ()
# Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API # Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API
# takes volatile long*; cl compiles that with a warning, clang errors out. # takes volatile long*; cl compiles that with a warning, clang errors out.
set(_boost_c_flags_line "") set(_boost_c_flags_line "")
set(_boost_cxx_flags_line "")
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang") if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types") set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types")
# The Visual Studio generator applies only the link language's flags to a
# project, and boost_container links as C++, so its C file never sees
# CMAKE_C_FLAGS. The C++ flags reach every file; keep CMake's defaults.
if (CMAKE_GENERATOR MATCHES "Visual Studio")
set(_boost_cxx_flags_line "-DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS} -Wno-incompatible-pointer-types")
endif ()
endif () endif ()
orcaslicer_add_cmake_project(Boost orcaslicer_add_cmake_project(Boost
@@ -53,7 +46,6 @@ orcaslicer_add_cmake_project(Boost
"${_context_arch_line}" "${_context_arch_line}"
"${_context_impl_line}" "${_context_impl_line}"
"${_boost_c_flags_line}" "${_boost_c_flags_line}"
"${_boost_cxx_flags_line}"
) )
set(DEP_Boost_DEPENDS ZLIB) set(DEP_Boost_DEPENDS ZLIB)
+13 -47
View File
@@ -26,9 +26,9 @@ endif()
cmake_minimum_required(VERSION 3.2) cmake_minimum_required(VERSION 3.2)
if (APPLE) if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts) # if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET) if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE) set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE)
endif () endif ()
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}") message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
@@ -38,14 +38,6 @@ if(POLICY CMP0135) # DOWNLOAD_EXTRACT_TIMESTAMP
cmake_policy(SET CMP0135 NEW) cmake_policy(SET CMP0135 NEW)
endif() endif()
# project() reads this, so set it first. scripts/flatpak/make_deps_tar.sh packs deps/
# without cmake/, so the file is missing in a Flatpak build.
set(_rules_override "${CMAKE_CURRENT_LIST_DIR}/../cmake/modules/ClangClShowIncludes.cmake")
if (EXISTS "${_rules_override}")
set(CMAKE_USER_MAKE_RULES_OVERRIDE "${_rules_override}")
endif ()
unset(_rules_override)
project(OrcaSlicer-deps) project(OrcaSlicer-deps)
# Backward compatibility for old CMake versions # Backward compatibility for old CMake versions
@@ -63,7 +55,6 @@ endif ()
set(DEP_DOWNLOAD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/DL_CACHE CACHE PATH "Path for downloaded source packages.") set(DEP_DOWNLOAD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/DL_CACHE CACHE PATH "Path for downloaded source packages.")
set(FLATPAK FALSE CACHE BOOL "Toggles various build settings for flatpak, like /usr/local in DESTDIR or not building wxwidgets") set(FLATPAK FALSE CACHE BOOL "Toggles various build settings for flatpak, like /usr/local in DESTDIR or not building wxwidgets")
option(SLIC3R_CAD "Build the SolveSpace solver and OCCT ModelingAlgorithms module the parametric Design/CAD tab needs. Must match the main project's SLIC3R_CAD." ON)
if ("${DESTDIR}" STREQUAL "" OR "${DESTDIR}" STREQUAL "${AUTOGENERATED_DESTDIR}") if ("${DESTDIR}" STREQUAL "" OR "${DESTDIR}" STREQUAL "${AUTOGENERATED_DESTDIR}")
if (LINUX AND (NOT DEFINED USE_OLD_DESTDIR_PREV OR USE_OLD_DESTDIR_PREV) AND EXISTS "${CMAKE_BINARY_DIR}/destdir/usr/local" AND NOT EXISTS "${CMAKE_BINARY_DIR}/OrcaSlicer_dep/usr/local") if (LINUX AND (NOT DEFINED USE_OLD_DESTDIR_PREV OR USE_OLD_DESTDIR_PREV) AND EXISTS "${CMAKE_BINARY_DIR}/destdir/usr/local" AND NOT EXISTS "${CMAKE_BINARY_DIR}/OrcaSlicer_dep/usr/local")
@@ -164,7 +155,7 @@ if (NOT _is_multi AND NOT CMAKE_BUILD_TYPE)
endif () endif ()
function(orcaslicer_add_cmake_project projectname) function(orcaslicer_add_cmake_project projectname)
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND;SOURCE_DIR" "CMAKE_ARGS" ${ARGN}) cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN})
# MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the # MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the
# generator. A non-Visual-Studio superbuild passes its own generator down, and with # generator. A non-Visual-Studio superbuild passes its own generator down, and with
@@ -193,11 +184,6 @@ function(orcaslicer_add_cmake_project projectname)
if (_dep_msvc_gen) if (_dep_msvc_gen)
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}") set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}")
# The toolset picks the compiler here, not the CMAKE_<LANG>_COMPILER
# forwarded below, so without it a clang-cl superbuild builds with cl.
if (CMAKE_GENERATOR_TOOLSET)
list(APPEND _gen CMAKE_GENERATOR_TOOLSET "${CMAKE_GENERATOR_TOOLSET}")
endif ()
else() else()
set(_gen "") set(_gen "")
endif() endif()
@@ -210,18 +196,12 @@ function(orcaslicer_add_cmake_project projectname)
set(_build_j "-j${NPROC}") set(_build_j "-j${NPROC}")
endif () endif ()
set(_source_dir_arg "")
if (P_ARGS_SOURCE_DIR)
set(_source_dir_arg SOURCE_DIR ${P_ARGS_SOURCE_DIR})
endif ()
if (NOT IS_CROSS_COMPILE OR NOT APPLE) if (NOT IS_CROSS_COMPILE OR NOT APPLE)
ExternalProject_Add( ExternalProject_Add(
dep_${projectname} dep_${projectname}
EXCLUDE_FROM_ALL ON EXCLUDE_FROM_ALL ON
INSTALL_DIR ${DESTDIR} INSTALL_DIR ${DESTDIR}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname} DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
${_source_dir_arg}
${_gen} ${_gen}
CMAKE_ARGS CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_POLICY_VERSION_MINIMUM=3.5
@@ -234,7 +214,6 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
-DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER} -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER}
-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER} -DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER}
-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER} -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER}
-DCMAKE_USER_MAKE_RULES_OVERRIDE:STRING=${CMAKE_USER_MAKE_RULES_OVERRIDE}
-DCMAKE_TOOLCHAIN_FILE:STRING=${CMAKE_TOOLCHAIN_FILE} -DCMAKE_TOOLCHAIN_FILE:STRING=${CMAKE_TOOLCHAIN_FILE}
-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS} -DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS}
-DCMAKE_SHARED_LINKER_FLAGS:STRING=${CMAKE_SHARED_LINKER_FLAGS} -DCMAKE_SHARED_LINKER_FLAGS:STRING=${CMAKE_SHARED_LINKER_FLAGS}
@@ -255,14 +234,12 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
# note for future devs: shared libs may actually create a size reduction # note for future devs: shared libs may actually create a size reduction
# but orcaslicer_deps tends to get really funny regarding linking after that (notably boost) # but orcaslicer_deps tends to get really funny regarding linking after that (notably boost)
# so, as much as I would like to use that, it's not happening # so, as much as I would like to use that, it's not happening
if (NOT P_ARGS_SOURCE_DIR) ExternalProject_Add_Step(dep_${projectname} free_download_space
ExternalProject_Add_Step(dep_${projectname} free_download_space DEPENDEES download # do after download
DEPENDEES download # do after download COMMENT "Freeing Space: Removing source archive"
COMMENT "Freeing Space: Removing source archive" WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR}
WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR} COMMAND ${CMAKE_COMMAND} -E rm -r ${projectname}
COMMAND ${CMAKE_COMMAND} -E rm -rf ${projectname} )
)
endif ()
ExternalProject_Add_Step(dep_${projectname} free_build_space ExternalProject_Add_Step(dep_${projectname} free_build_space
DEPENDEES install # do after install DEPENDEES install # do after install
COMMENT "Freeing Space: Removing source and build files" COMMENT "Freeing Space: Removing source and build files"
@@ -276,7 +253,6 @@ else()
EXCLUDE_FROM_ALL ON EXCLUDE_FROM_ALL ON
INSTALL_DIR ${DESTDIR} INSTALL_DIR ${DESTDIR}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname} DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
${_source_dir_arg}
${_gen} ${_gen}
CMAKE_ARGS CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_POLICY_VERSION_MINIMUM=3.5
@@ -285,7 +261,6 @@ else()
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH} -DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER} -DCMAKE_C_COMPILER_LAUNCHER:STRING=${CMAKE_C_COMPILER_LAUNCHER}
-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER} -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CMAKE_CXX_COMPILER_LAUNCHER}
-DCMAKE_USER_MAKE_RULES_OVERRIDE:STRING=${CMAKE_USER_MAKE_RULES_OVERRIDE}
-DBUILD_SHARED_LIBS:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF
${_cmake_osx_arch} ${_cmake_osx_arch}
"${_configs_line}" "${_configs_line}"
@@ -383,11 +358,6 @@ include(GLEW/GLEW.cmake)
include(GLFW/GLFW.cmake) include(GLFW/GLFW.cmake)
include(OpenCSG/OpenCSG.cmake) include(OpenCSG/OpenCSG.cmake)
set(SLVS_PKG "")
if (SLIC3R_CAD)
include(SLVS/SLVS.cmake)
set(SLVS_PKG dep_SLVS)
endif ()
include(TBB/TBB.cmake) include(TBB/TBB.cmake)
@@ -405,6 +375,10 @@ include(libnoise/libnoise.cmake)
include(Draco/Draco.cmake) include(Draco/Draco.cmake)
include(FFMPEG/FFMPEG.cmake)
include(Assimp/Assimp.cmake)
# I *think* 1.1 is used for *just* md5 hashing? # I *think* 1.1 is used for *just* md5 hashing?
# 3.1 has everything in the right place, but the md5 funcs used are deprecated # 3.1 has everything in the right place, but the md5 funcs used are deprecated
# a grep across the repo shows it is used for other things # a grep across the repo shows it is used for other things
@@ -415,12 +389,6 @@ if(NOT OPENSSL_FOUND)
set(OPENSSL_PKG dep_OpenSSL) set(OPENSSL_PKG dep_OpenSSL)
endif() endif()
include(FFMPEG/FFMPEG.cmake)
include(Assimp/Assimp.cmake)
include(DataChannel/DataChannel.cmake)
set(DATACHANNEL_PKG dep_DataChannel)
# we don't want to load a "wrong" openssl when loading curl # we don't want to load a "wrong" openssl when loading curl
# so, just don't even bother # so, just don't even bother
# ...i think this is how it works? change if wrong # ...i think this is how it works? change if wrong
@@ -479,7 +447,6 @@ set(_dep_list
dep_NLopt dep_NLopt
dep_OpenVDB dep_OpenVDB
dep_OpenCSG dep_OpenCSG
${SLVS_PKG}
dep_OpenCV dep_OpenCV
dep_Eigen dep_Eigen
dep_CGAL dep_CGAL
@@ -494,7 +461,6 @@ set(_dep_list
dep_wxInspector dep_wxInspector
dep_FFMPEG dep_FFMPEG
dep_Assimp dep_Assimp
${DATACHANNEL_PKG}
) )
if (MSVC) if (MSVC)
-37
View File
@@ -1,37 +0,0 @@
# libdatachannel is the native ICE/DTLS/SCTP implementation used by the
# GUI WebRTC camera controller. Keep the source revision fixed: the signaling
# protocol is evolving independently of this transport dependency.
#
# It vendors plog, usrsctp and libjuice as git submodules, which a plain
# GitHub tag tarball does not include. The flatpak sandbox has no network
# access during the build, so there the manifest itself clones the repo
# (submodules and all) into the dependency download directory before the
# sandbox closes. ExternalProject_Add is pointed at that existing checkout
# instead of being given its own network-dependent download method.
if (FLATPAK)
set(_datachannel_source
SOURCE_DIR ${DEP_DOWNLOAD_DIR}/DataChannel
)
else()
set(_datachannel_source
GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git
GIT_TAG v0.24.5
GIT_SHALLOW ON
GIT_SUBMODULES_RECURSE ON
)
endif()
orcaslicer_add_cmake_project(DataChannel
DEPENDS ${OPENSSL_PKG}
CMAKE_ARGS
-DNO_EXAMPLES=ON
-DNO_TESTS=ON
-DNO_WEBSOCKET=ON
-DNO_MEDIA=ON
-DUSE_NICE=OFF
-DUSE_SYSTEM_JUICE=OFF
-DUSE_SYSTEM_USRSCTP=OFF
-DOPENSSL_ROOT_DIR:PATH=${DESTDIR}
-DOPENSSL_USE_STATIC_LIBS=ON
${_datachannel_source}
)
-3
View File
@@ -7,7 +7,4 @@ orcaslicer_add_cmake_project(Draco
${_options} ${_options}
URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip
URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77 URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
CMAKE_ARGS
# The encoder and decoder tools duplicate draco.lib; see deps-windows.cmake.
"${DEP_LLD_FORCE_MULTIPLE}"
) )
+7 -23
View File
@@ -1,26 +1,14 @@
set(_conf_cmd ./configure) set(_conf_cmd ./configure)
set(_ffmpeg_depends)
set(_ffmpeg_configure_command ${_conf_cmd})
if (TARGET dep_OpenSSL)
set(_ffmpeg_depends DEPENDS dep_OpenSSL)
set(_ffmpeg_configure_command
${CMAKE_COMMAND} -E env
"PKG_CONFIG_PATH=${DESTDIR}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}"
${_conf_cmd}
)
endif()
if (MSVC) if (MSVC)
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG") set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-winarm64-orca-shared-7.0.zip") set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-winarm64-orca-shared-7.0.zip")
set(PREBUILD_HASH_arm64 "da480cbb39680056de824c57ec4dc3bd577b479ebbc310ff1f9dc55cf014b4c1") set(PREBUILD_HASH_arm64 "12f4140279f2f8469885e1b5b2e8be9d788882914c21523cacd56989f3548054")
set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-win64-orca-shared-7.0.zip") set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-win64-orca-shared-7.0.zip")
set(PREBUILD_HASH_x64 "85da19daf198f5548259d8aabb349db84997a3f6e6886d8d7764114add9c6dae") set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c")
ExternalProject_Add(dep_FFMPEG ExternalProject_Add(dep_FFMPEG
${_ffmpeg_depends}
URL ${PREBUILD_URL_${DEPS_ARCH}} URL ${PREBUILD_URL_${DEPS_ARCH}}
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}} URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
@@ -33,8 +21,6 @@ if (MSVC)
) )
else () else ()
set(_openssl_cmd --enable-openssl)
if (APPLE) if (APPLE)
set(_minos_cmd set(_minos_cmd
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}" "--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
@@ -66,11 +52,10 @@ else ()
endif() endif()
ExternalProject_Add(dep_FFMPEG ExternalProject_Add(dep_FFMPEG
${_ffmpeg_depends}
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
CONFIGURE_COMMAND ${_ffmpeg_configure_command} CONFIGURE_COMMAND ${_conf_cmd}
${_cross_cmd} ${_cross_cmd}
${_pic_cmd} ${_pic_cmd}
${_arch_cmd} ${_arch_cmd}
@@ -78,21 +63,20 @@ else ()
"--prefix=${DESTDIR}" "--prefix=${DESTDIR}"
${_link_cmd} ${_link_cmd}
${_minos_cmd} ${_minos_cmd}
${_openssl_cmd}
--disable-doc --disable-doc
--enable-small --enable-small
--disable-outdevs --disable-outdevs
--disable-filters --disable-filters
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test* --enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
--disable-protocols --disable-protocols
--enable-protocol=file,fd,pipe,http,https,rtp,tcp,udp --enable-protocol=file,fd,pipe,rtp,udp
--disable-muxers --disable-muxers
--enable-muxer=rtp --enable-muxer=rtp
--disable-encoders --disable-encoders
--disable-decoders --disable-decoders
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv* --enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
--disable-demuxers --disable-demuxers
--enable-demuxer=h264,mp3,mov,mpjpeg,rtsp,sdp --enable-demuxer=h264,mp3,mov
--disable-zlib --disable-zlib
--disable-avdevice --disable-avdevice
BUILD_IN_SOURCE ON BUILD_IN_SOURCE ON
+3 -3
View File
@@ -5,7 +5,7 @@
#if defined (__GNUC__) && ! defined (__cplusplus) #if defined (__GNUC__) && ! defined (__cplusplus)
typedef unsigned long long t1;typedef t1*t2; typedef unsigned long long t1;typedef t1*t2;
-void g(){} -void g(){}
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){} +void g(int,t1 const*,t1,t2,t1 const*,int){}
void h(){} void h(){}
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0) static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;} {t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
@@ -17,7 +17,7 @@
#if defined (__GNUC__) && ! defined (__cplusplus) #if defined (__GNUC__) && ! defined (__cplusplus)
typedef unsigned long long t1;typedef t1*t2; typedef unsigned long long t1;typedef t1*t2;
-void g(){} -void g(){}
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){} +void g(int,t1 const*,t1,t2,t1 const*,int){}
void h(){} void h(){}
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0) static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;} {t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
@@ -26,7 +26,7 @@
#if defined (__GNUC__) && ! defined (__cplusplus) #if defined (__GNUC__) && ! defined (__cplusplus)
typedef unsigned long long t1;typedef t1*t2; typedef unsigned long long t1;typedef t1*t2;
-void g(){} -void g(){}
+void g(int a,t1 const*b,t1 c,t2 dd,t1 const*e,int ff){} +void g(int,t1 const*,t1,t2,t1 const*,int){}
void h(){} void h(){}
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0) static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;} {t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
-2
View File
@@ -8,8 +8,6 @@ orcaslicer_add_cmake_project(NLopt
-DNLOPT_GUILE:BOOL=OFF -DNLOPT_GUILE:BOOL=OFF
-DNLOPT_SWIG:BOOL=OFF -DNLOPT_SWIG:BOOL=OFF
-DNLOPT_TESTS:BOOL=OFF -DNLOPT_TESTS:BOOL=OFF
# testopt is built regardless of NLOPT_TESTS; see deps-windows.cmake.
"${DEP_LLD_FORCE_MULTIPLE}"
) )
if (MSVC) if (MSVC)
+1 -16
View File
@@ -11,21 +11,6 @@ else()
set(library_build_type "Static") set(library_build_type "Static")
endif() endif()
# SLIC3R_CAD (declared in deps/CMakeLists.txt) builds OCCT's ModelingAlgorithms module
# (fillet/offset/loft), whose only consumer is the parametric Design/CAD tab. With it OFF
# the deps prefix matches upstream exactly.
#
# With it ON the delta is THREE toolkits, not two: TKFillet (7.40 MiB archive, used via
# BRepFilletAPI), TKOffset (5.38 MiB, used via BRepOffsetAPI) and TKFeat (4.42 MiB), which
# nothing here references but which the module flag builds anyway -- it is all-or-nothing
# per module. The module's other nine toolkits are built either way, because DataExchange
# (the STEP path upstream already ships) depends on them.
#
# On macOS/Linux OCCT links statically, so an unreferenced toolkit costs build time and no
# shipped bytes. The Windows figure is a real DLL cost and has NOT been measured -- an
# earlier "3.77 MiB, Windows only" note here covered only two of the three toolkits and is
# not a number to quote. See docs/HLSD/design-tab.md.
if (IN_GIT_REPO) if (IN_GIT_REPO)
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT) set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
endif () endif ()
@@ -50,7 +35,7 @@ orcaslicer_add_cmake_project(OCCT
#-DBUILD_MODULE_DataExchange=OFF #-DBUILD_MODULE_DataExchange=OFF
-DBUILD_MODULE_Draw=OFF -DBUILD_MODULE_Draw=OFF
-DBUILD_MODULE_FoundationClasses=OFF -DBUILD_MODULE_FoundationClasses=OFF
-DBUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD} -DBUILD_MODULE_ModelingAlgorithms=OFF
-DBUILD_MODULE_ModelingData=OFF -DBUILD_MODULE_ModelingData=OFF
-DBUILD_MODULE_Visualization=OFF -DBUILD_MODULE_Visualization=OFF
${_occt_compiler_args} ${_occt_compiler_args}
-6
View File
@@ -80,12 +80,6 @@ ExternalProject_Add(dep_OpenSSL
INSTALL_COMMAND ${_install_cmd} INSTALL_COMMAND ${_install_cmd}
) )
if (CMAKE_GENERATOR MATCHES "Visual Studio")
# OpenSSL builds with cl, but MSBuild runs nmake in this project's toolset
# environment, and ClangCL's puts clang's headers first. Use the default.
set_target_properties(dep_OpenSSL PROPERTIES VS_PLATFORM_TOOLSET "$(DefaultPlatformToolset)")
endif ()
ExternalProject_Add_Step(dep_OpenSSL install_cmake_files ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
DEPENDEES install DEPENDEES install
-65
View File
@@ -1,65 +0,0 @@
# Replaces the upstream SolveSpaceLib CMakeLists, which builds a demo executable and
# has no install rules. The sources themselves are used verbatim.
cmake_minimum_required(VERSION 3.13)
project(SLVS VERSION 3.0)
add_library(slvs
libslvs/constrainteq.cpp
libslvs/entity.cpp
libslvs/expr.cpp
libslvs/system.cpp
libslvs/util.cpp
libslvs/platform/unixutil.cpp
libslvs/lib.cpp
libslvs/SolveSpaceSystem.cpp)
target_compile_features(slvs PUBLIC cxx_std_11)
# LIBRARY strips the solver core out of the SolveSpace application it was extracted from.
target_compile_definitions(slvs PRIVATE -DLIBRARY)
if (MSVC)
target_compile_definitions(slvs PRIVATE -D_CRT_SECURE_NO_WARNINGS -D_SCL_SECURE_NO_WARNINGS)
endif ()
target_include_directories(slvs
PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/libslvs/include>
PRIVATE ${PROJECT_SOURCE_DIR}/libslvs)
# libslic3r is linked into shared targets, so this has to be position independent.
set_target_properties(slvs PROPERTIES POSITION_INDEPENDENT_CODE ON)
# 2018 code, predating the project's warning settings; it is not ours to clean up.
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(slvs PRIVATE -w -fno-strict-aliasing)
endif ()
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY AnyNewerVersion)
install(TARGETS slvs
EXPORT ${PROJECT_NAME}Targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
set(ConfigPackageLocation ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(EXPORT ${PROJECT_NAME}Targets
FILE "${PROJECT_NAME}Config.cmake"
NAMESPACE ${PROJECT_NAME}::
DESTINATION ${ConfigPackageLocation})
install(FILES
${PROJECT_SOURCE_DIR}/libslvs/include/slvs.h
${PROJECT_SOURCE_DIR}/libslvs/include/SolveSpaceSystem.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
DESTINATION ${ConfigPackageLocation})
-13
View File
@@ -1,13 +0,0 @@
# libslvs — the geometric constraint solver behind the Design tab's sketch constraints.
# Extraction of solvespace.com's libslvs, taken verbatim from JacobStoren/SolveSpaceLib;
# only the CMakeLists is ours, because upstream's builds a demo and installs nothing.
# GPLv3, compatible with this fork's licence. Self-contained: no external dependencies.
orcaslicer_add_cmake_project(SLVS
URL https://github.com/JacobStoren/SolveSpaceLib/archive/4d8704523e4bf212fadf5189f92484244f670fea.zip
URL_HASH SHA256=1c4bdde9c3c6ef20ea4b50b73601de56769f2eb131b36927d7c6489f102e6c30
PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in ./CMakeLists.txt
)
if (MSVC)
add_debug_dep(dep_SLVS)
endif ()
-1
View File
@@ -15,7 +15,6 @@ orcaslicer_add_cmake_project(
-DTBB_BUILD_SHARED=OFF -DTBB_BUILD_SHARED=OFF
-DTBB_BUILD_TESTS=OFF -DTBB_BUILD_TESTS=OFF
-DTBB_TEST=OFF -DTBB_TEST=OFF
-DTBB_DISABLE_HWLOC_AUTOMATIC_SEARCH=ON
-DTBB_ENABLE_IPO=OFF -DTBB_ENABLE_IPO=OFF
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF
-DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON
-9
View File
@@ -42,15 +42,6 @@ else ()
message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}") message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}")
endif () endif ()
# Draco's tools and NLopt's testopt compile sources that are also in their
# static library. MSBuild passes the library before the objects and lld-link
# resolves as it goes, so the library's copy wins and the object then reads as
# a duplicate. Nothing uses those executables, so let lld keep the first one.
set(DEP_LLD_FORCE_MULTIPLE "")
if (CMAKE_GENERATOR MATCHES "Visual Studio" AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(DEP_LLD_FORCE_MULTIPLE "-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
endif ()
if (${DEP_DEBUG}) if (${DEP_DEBUG})
set(DEP_BOOST_DEBUG "debug") set(DEP_BOOST_DEBUG "debug")
else () else ()
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Compiles Objects/unicodectype.c without optimisation. VS 2026's ARM64 code
generator needs about 27 GB for _PyUnicode_ToNumeric, a switch with 1951
cases. CPython has the same workaround (python/cpython#153668). -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Update="..\Objects\unicodectype.c">
<Optimization>Disabled</Optimization>
<WholeProgramOptimization>false</WholeProgramOptimization>
</ClCompile>
</ItemGroup>
</Project>
+1 -11
View File
@@ -88,18 +88,8 @@ if(WIN32)
list(APPEND _python_env_args "PreferredToolArchitecture=${_python_tool_arch}") list(APPEND _python_env_args "PreferredToolArchitecture=${_python_tool_arch}")
endif() endif()
# MSBuild reads extra switches from PCbuild/msbuild.rsp.
set(_python_rsp "/p:PlatformToolset=${_python_platform_toolset}\n")
# VS 2026's ARM64 code generator needs about 27 GB for one function in
# Objects/unicodectype.c (python/cpython#153668); the property sheet compiles
# that file without optimisation.
if(_python_pcbuild_platform STREQUAL "ARM64")
file(TO_NATIVE_PATH "${CMAKE_CURRENT_LIST_DIR}/arm64-unicodectype.props" _python_arm64_props)
string(APPEND _python_rsp "/p:ForceImportAfterCppTargets=\"${_python_arm64_props}\"\n")
endif()
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/python3-msbuild.rsp" "${_python_rsp}")
set(_conf_cmd set(_conf_cmd
${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_BINARY_DIR}/python3-msbuild.rsp" <SOURCE_DIR>/PCbuild/msbuild.rsp cmd /c "echo /p:PlatformToolset=${_python_platform_toolset}>PCbuild\\msbuild.rsp"
) )
set(_build_cmd set(_build_cmd
${CMAKE_COMMAND} -E env ${_python_env_args} ${CMAKE_COMMAND} -E env ${_python_env_args}
-137
View File
@@ -1,137 +0,0 @@
# Deferred Page Construction: High Level Design
## Why it exists
The main window is a notebook of tabs, and only one of them is on screen when the first
frame appears. Others are opened later in the session, some never, and some only exist
for certain printers. Building every tab before the first frame makes each startup pay for
tabs the user may never open.
This subsystem builds a tab the first time it is shown, and builds the rest in small units
while the user is idle after startup. Startup pays only for what the first frame shows,
the other tabs are usually ready before anyone opens them, and a click that lands in the
middle of the idle build waits for one unit at most. Work that is not a tab, such as a
dialog or the 3D view's GL resources, uses the same machinery.
## The parts
The parts are independent. A holder can hold anything a factory makes, a placeholder page
is a holder with a widget, a staged object can live outside a holder, and the scheduler
knows none of them; it runs tasks, which the main window makes from the holders.
### The holder: `Lazy<T>`
A holder keeps one object and the factory that makes it. The rest of the app reads the
object if it exists, makes sure it exists now when about to show or navigate to it, or runs
something once it exists. A type with one instance in the app gets these as statics
through `LazyInstance<T>`, so callers need no reference to the main window, and all of them
are harmless while no holder exists.
The holder guarantees that callers see the object only once it is completely built. A
build cannot re-enter itself, so a nested request finds nothing yet, and a factory that
returns null or a unit that throws leaves the holder and the scheduler able to carry on.
The holder does not own the object; its wx parent does, as for any window. The holder has
no wx dependency and is unit-tested.
### The placeholder page: `LazyPage<Panel>`
The notebook needs a page object for a tab to exist and for tabs to be inserted and
removed by pointer, and the placeholder is that object. It builds the real panel inside
itself the first time it is shown and forwards showing and hiding afterwards, so a panel's
own show handling stays its activation hook. Nothing builds while the main window is
hidden; the window's first show builds the start page. A page that is out of the book is
not prebuilt. A panel built while its page is hidden stays hidden, and gets the theming
the window applied before the panel existed.
### Staged construction: `StagedBuild`
A constructor too big to be one unit builds a skeleton and queues the rest as steps, which
run one per unit. A child panel's steps can be forwarded to its parent, and the parent is
complete only once the child is. Nothing may use what a step builds before the last step
has run, so staged panels follow these constraints:
- members created in steps start out null, so a partly built panel can be destroyed;
- timers, event handlers and destructors that touch step content check that the panel is
complete first;
- nothing takes focus while off screen, since a unit may run while the user is typing
elsewhere;
- a widget added by a step keeps its place in the sizer through an empty slot the skeleton
creates.
### The scheduler: `IdleScheduler` and `PrebuildQueue`
The queue holds tasks in order, and a slice runs units of the first pending task until
the task finishes, the time budget is spent, or input arrives. It has no wx dependency and
is unit-tested with a fake clock. A task whose work goes away, such as a tab removed from
the book, is skipped, and becomes pending again if the work comes back.
A slice runs only once the user has been idle for a short quiet time, and each slice is its
own timer message, so paint, timers and input queued in between are handled before the
next slice. Posting slices as pending events would not do that, because wx drains every
pending event before the next native message. On GTK a timer that is always due starves
the lower-priority sources that repaint and deliver posted events, so slices are a few
milliseconds apart. On Windows a slice also waits while the native queue holds input,
not counting mouse moves, which Windows synthesizes whenever a window appears under the
cursor. A slice never runs inside a `wxYield()`, where it would build pages in the middle of
the code that yielded. A unit cannot be interrupted, so the largest unit bounds how long a
click can wait.
When nothing is pending the timer stops and the subsystem costs nothing.
The main window owns the scheduler because it owns what the tasks build, and clearing the
queue with the window keeps a task from outliving its object. Each owner provides its own
tasks, such as a tab, a dialog, the Prepare tab's settings page one option group at a
time, the Prepare page's layout at the size the book gives its pages, or the 3D view's GL
resources.
### The 3D view's GL resources
OpenGL is loaded on the Prepare tab's canvas. When the start page is not Prepare, loading
it is an idle task that makes the context current on the hidden canvas, so the start page
paints first and Prepare never appears. Loading it on a shown canvas under `Freeze()` holds
back the start page's paint, and on GTK `Freeze()` cannot hide the canvas, which is a
native child window or a Wayland subsurface drawn outside GTK. A hidden Windows child
window keeps its device context, macOS attaches the context to a hidden view, and GTK
creates the canvas's surface when the widget is realized, so on GTK the task realizes the
canvas first. If the context cannot be made current, the canvas's first render loads the
resources.
## Rules
**Before the first frame.** Only the start page and the Prepare tab's plater are built
before the first frame. Everything else goes through a holder.
**Reaching a lazy object.** Callers use the type's statics. Reading it if built is for
things the object can live without, such as a rescale, a color change or a status refresh.
Making sure it exists is for navigating to it or showing it. Running something once it is
built is for state it would not fetch for itself on construction. A panel that pulls its
state when constructed only ever needs to be read if built.
**Unit size.** A unit should fit in one slice on a fast machine. A constructor over that is
staged, and a single widget over it is accepted unless the widget itself can be split.
**Order.** Tasks run cheapest and most likely to be opened first. Each holder's order is
given where it is created, with gaps so a new task fits between its neighbors. A negative
order is never prebuilt, for something few sessions open that costs more to build unasked
than it saves.
## Adopting it
A lazy tab needs a panel type deriving from `LazyInstance`, a placeholder page the main
window creates once with its order and registers for the idle build, and every use of the
panel outside the main window going through the statics. Its constructor has to cope with
the main window already existing and the user being busy elsewhere, so it takes no focus
while off screen, and it does all of its own setup, since the main window does nothing to a
panel after creating it.
To stage a heavy constructor, keep the skeleton in the constructor, move the rest into
steps in its original order, and follow the staged-construction constraints. Measure the
units; a step that is still one big widget is split inside the widget or accepted.
## Verifying
The log lists the queue when it is registered, reports each completed task at info level
and each slice and unit at debug level, and reports every on-demand build with its units
and time. A task's completion line counts only the slice it finished in. A run from the
configured start page should show every registered task complete in order, with no unit
longer than intended. A click on a tab during the idle build should show an on-demand
build for what was left, with the slices resuming once the user is idle again.
-213
View File
@@ -1,213 +0,0 @@
# Design tab — High Level Design
## Purpose and scope
The Design tab is a parametric CAD environment inside the slicer: sketch, constrain, build
solid features, commit the result to the plate. It exists because the alternative is a round
trip through an external CAD application, and that round trip discards design intent at both
ends — a part edited after slicing comes back as a mesh rather than as the feature history
that produced it. Keeping the model in the project means a dimension can be changed after the
part has been sliced, with the nozzle diameter, the build volume and the material already
known.
Its coupling to the rest of the application is deliberately narrow. It adds no stage to the
slicing pipeline and touches neither the preset system nor `Tab`. It reaches the rest of Orca
in two places: **Commit to Plate**, which hands finished solids to Prepare as ordinary model
objects, and one optional 3MF archive entry that carries the recipe. Everything else is
contained in `src/libslic3r/CAD/` and `src/slic3r/GUI/CAD/`.
The user-facing manual lives in the wiki
([Design Tab](https://www.orcaslicer.com/wiki/design_tab)), not here. This document covers the
parts of the design that the code does not make evident.
## The model is a recipe
`CadDocument` holds an ordered list of `CadFeature` and nothing else that matters. Bodies,
meshes and display geometry are **derived**: `recompute()` replays the feature list from the
start and rebuilds them. Editing a dimension set twenty features ago is therefore an ordinary
edit — everything downstream is rebuilt by the same replay that built it the first time.
Two consequences follow from deriving rather than storing:
- **Undo is a snapshot of `features` alone.** The caller calls `checkpoint()` before the
mutations that make up one user action; undo restores that snapshot and recomputes. Because
everything else is derived, one checkpoint is exactly one `Ctrl+Z` step and the restored
state is exact rather than approximately reconstructed. The tab keeps this stack itself; it
is not Orca's project snapshot system, which operates on `Model` objects the Design tab does
not own until Commit.
- **Face and edge ids are session-scoped.** They are indices into `TopExp::MapShapes`, so a
rebuild invalidates every one of them. `CadDocument::topo_generation` is bumped on every
rebuild so a holder of an id can discover that it is stale instead of silently addressing a
different edge. The counter is deliberately not serialized: an id means nothing outside the
run that produced it.
## Geometry kernel and dependency surface
The kernel is OCCT, which OrcaSlicer **already** links — `Format/STEP.cpp`, `Format/svg.cpp`
and `Shape/TextShape.cpp` use it upstream. The Design tab adds no third-party dependency; it
widens the existing OCCT build by one module flag in `deps/OCCT/OCCT.cmake`:
```cmake
-DBUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD}
```
Most of that module's twelve toolkits were already being built, because `DataExchange` — the
STEP path upstream ships — depends on them. The delta is `TKFillet` (used through
`BRepFilletAPI`), `TKOffset` (`BRepOffsetAPI`) and `TKFeat`, which nothing here references but
which the module flag builds anyway, because OCCT's module flags are all-or-nothing. On macOS
and Linux OCCT links statically, so an unreferenced toolkit costs build time and no shipped
bytes; on Windows OCCT builds shared, so the cost there is real DLL bytes. That Windows figure
has not been measured, and `OCCT.cmake` says so rather than carrying a number that was derived
from an incomplete toolkit list.
On Windows the packaging step asserts that every linked OCCT toolkit has a shipped DLL and
fails the configure with the name of any that is missing, because the alternative failure — a
deps prefix built with a different `SLIC3R_CAD` setting than the app — otherwise surfaces as a
missing DLL at first launch.
## The sketch constraint solver
`src/libslic3r/slvs/` is a vendored subset of SolveSpace's `libslvs`: self-contained, no
external dependencies, **GPL-3.0**, with its `LICENSE` preserved verbatim in the directory.
`SketchSolver.cpp` is its only consumer and drives every sketch constraint in the tab.
OrcaSlicer is AGPL-3.0. GPLv3 §13 permits combining a GPLv3 work with an AGPLv3 work and
AGPLv3 §13 grants the converse, so the combined work is distributable under AGPL-3.0 with the
solver's GPLv3 terms preserved. The solver is vendored rather than fetched as a dependency
because it is a pinned subset with no build system of its own; the cost of that choice is
upstream-sync burden, paid deliberately to keep `deps/` unchanged.
## The SLIC3R_CAD gate
`SLIC3R_CAD` (default ON) compiles the tab and selects the OCCT module flag above. With it OFF
the tab is not built and the deps prefix matches upstream exactly. The gate is cheap because
the hooks the Design tab adds to shared GUI code — chiefly the `m_design_sketch_tool` member
and the render, mouse and key hooks in `GLCanvas3D` — are null-guarded on the path they extend,
so removing the tab removes behaviour rather than requiring the host code to be rewritten.
The flag has to agree between the dependencies and the application; that is what the DLL
assertion above is checking.
## Project persistence
A project stores the recipe as one optional archive entry, `Metadata/orca_cad.bin`, backed by
a single `std::string cad_recipe` on `Model`. The entry is written only when the string is
non-empty, and readers that do not know it ignore it, so projects that contain no CAD model are
byte-identical to what upstream would have written and older readers are unaffected.
The blob is a cereal binary archive whose layout is the field order of `CadFeature`'s
save/load. That makes the format the one irreversible decision in the subsystem, and the rules
that keep it survivable are:
- **Append only, never reorder.** Enums serialize positionally as their underlying integer, so
inserting a value in the middle of `SketchConstraintType` or `CadFeatureType` reinterprets
every constraint in every saved project. New fields go at the end.
- **Features are length-framed.** Since v5 each feature is a length-prefixed, self-contained
cereal stream, so a reader can skip a feature written by a newer build and stop cleanly on an
older one. This is what makes appending a field a non-breaking change from here on. v4 and
earlier still open through the pre-framing flat path; v1 is deliberately not loadable and has
no migration path.
- **A newer stamp is refused, not guessed at.** `deserialize_recipe` rejects a blob whose
version exceeds `ORCA_CAD_RECIPE_VERSION` with a message naming both versions.
- **The rules are held by fixtures, not by discipline.** `tests/data/cad_recipe_v{3,4,5}.bin`
are checked-in blobs from the builds that wrote them, and the tests that load them fail if a
field is reordered — which the in-memory round-trip test cannot detect. A regeneration test
(`[.regen]`, not run by default) produces a fresh fixture when a new version is stamped.
`Import` features embed the imported solid as an OCCT BRep string inside the recipe rather than
referencing the source file, so a project opens without the STEP or mesh it was built from.
The cost is that saved projects are coupled to an OCCT BRep revision.
## The interaction contract
Three inputs carry the whole modelling loop — left click, right click and `Esc` — and the
contract between them is stated in code rather than spread across handlers.
`DesignInteraction.hpp` defines a four-level LIFO stack whose enum value *is* the depth, so
"which level does this press belong to" is a comparison:
| Level | Holds | One `Esc` press |
| --- | --- | --- |
| `Transient` | a value field or a popup menu | closes it; the tool stays armed |
| `Gesture` | an uncommitted delta — an entity being drawn, a body being dragged | reverts it; committed work is untouched |
| `Tool` | a feature card, an armed sketch tool, a constrain session | exits it; drawn entities survive |
| `Idle` | nothing transient | clears the selection; leaves a sketch session only if it is empty |
`cad_escape_level()` is a `constexpr` free function over a POD of four booleans rather than a
method on the panel, so the ordering that is the entire contract is checkable without a window,
a GL context or an event loop — five `static_assert`s in the header do exactly that at compile
time.
**The strict invariant: no level of `Esc` deletes a feature, discards a sketch that holds
geometry, or rolls history back.** Destroying work needs a gesture that says so — `Del` on an
explicit selection, the sketch ribbon's Cancel, which asks first, or `Ctrl+Z`. A sketch
*session* is deliberately not a `Tool` level; it is the environment the `Idle` level lives in,
which makes the destructive path unrepresentable rather than merely unlikely.
Right-click is read at button-up against two independent budgets — 3 px of drift and 200 ms —
because drift alone still popped a menu at the end of a slow, careful orbit. The raycast uses
the press position, not the release. An armed sketch tool that already consumed the right
button (to terminate a chain, say) declines to also open a menu, through a read-and-clear flag.
Past either budget the event is navigation, and navigation does not transition the state
machine.
Entering a sketch changes three things at once so the mode is legible: a banner above the
canvas (a sibling of the canvas, not a child over it — on GTK a child window over a
`wxGLCanvas` is a native window and does not reliably stack over GL), the printer bed muted so
a plate grid is never read as a sketch grid, and `N` to look normal to the plane. Code that
changes any of the three belongs with a change to this section.
## The offer is generated, not hand-written
Right-clicking geometry opens the *offer*: eight families in a fixed order, each verb at a
permanent row index, verbs that do not apply shown disabled **in place with their reason**
rather than removed. The invariant is that a verb's row index is identical in every selection
where it appears and that adding a verb never moves an existing one — the hand learns the
position, so the menu is never re-sorted, compacted or adaptively ordered.
An invariant across 92 verbs and 20 selection kinds does not survive by review, so the map
exists once, as data: `scripts/CAD/tool_atlas.json` carries every verb with its row, key, icon,
accepted selections, preconditions and refusal string, and `scripts/CAD/gen_offer_table.py`
emits `src/slic3r/GUI/CAD/DesignOffer.hpp` from it. The header is checked in and never
hand-edited; `scripts/CAD/run-all-checks.sh` runs the generator with `--check` as its first
rung, which is what makes "GENERATED — DO NOT EDIT" a fact rather than a request. The generator
also refuses an atlas with a duplicate verb id, since `mcp_run_verb` resolves a verb by id and
would make the second one unreachable.
The atlas and its generator sit in `scripts/CAD/` rather than in `docs/`: they are build inputs
for a checked-in header, not documentation.
## Automation surface
`McpControl` exposes the document over JSON-RPC when `ORCA_CAD_MCP` is set in the environment,
with `tools/orca_cad_mcp_bridge.py` as the client side. It describes the scene, queries
topology, measures, and runs the same verbs the offer does — it re-implements nothing, so a
scripted action and a clicked one cannot diverge. It is off unless the variable is set.
## Where the code lives
| Path | Role |
| --- | --- |
| `src/libslic3r/CAD/CadDocument.*` | the feature recipe, its replay, undo and serialization |
| `src/libslic3r/CAD/GeometryEngine.*` | OCCT wrapper — faces, edges, booleans, healing |
| `src/libslic3r/CAD/SketchEngine.*` | profile → wire → solid |
| `src/libslic3r/CAD/SketchSolver.*` | constraint solving, over the vendored solver |
| `src/libslic3r/slvs/` | vendored 2D constraint solver (GPLv3) |
| `src/slic3r/GUI/CAD/DesignPanel.*` | the tab: toolbar, feature cards, tree, key maps |
| `src/slic3r/GUI/CAD/DesignCanvas.*` | viewport integration |
| `src/slic3r/GUI/CAD/DesignSketchTool.*` | in-canvas sketching |
| `src/slic3r/GUI/CAD/DesignInteraction.hpp` | the Esc level contract |
| `src/slic3r/GUI/CAD/DesignOffer.hpp` | generated offer table |
| `scripts/CAD/tool_atlas.json` | source of truth for the offer |
## Verification
The kernel is covered by Catch2 suites in `tests/libslic3r/` (`test_caddocument`,
`test_sketchconstraints`, `test_sketchedit`, `test_sketchimport`, `test_sketchinference`,
`test_sketchprofile`, `test_slvs_constraints`), which need no display;
`scripts/CAD/run-kernel-tests.sh` builds only `libslic3r_tests` and runs them headless.
The GUI half is not covered by CI, which has no OpenGL canvas or synthetic input: the ladders
in `scripts/CAD/` drive a running application in a local rig instead, and
`scripts/CAD/run-all-checks.sh` is the gate that runs all of them. A green kernel run says
nothing about the viewport, so the two are reported separately rather than as one number.
+105 -84
View File
@@ -9,7 +9,7 @@ OrcaFilamentLibrary (OFL), Qidi, or Snapmaker bundle. The granularity is the nam
not the brand behind it: `AAA PLA Lite` and `AAA PLA Pro` are two filaments with two ids, not not the brand behind it: `AAA PLA Lite` and `AAA PLA Pro` are two filaments with two ids, not
variants of one. variants of one.
**How it is generated:** an id is computed, never invented. `scripts/orca_profile_tool.py` **How it is generated:** an id is computed, never invented. `scripts/orca_id_tool.py`
mints it as a deterministic hash of the product's identity — the triple mints it as a deterministic hash of the product's identity — the triple
`(filament_vendor, filament_type, filament name)`, where the filament name is the preset name `(filament_vendor, filament_type, filament name)`, where the filament name is the preset name
with its `@...` variant suffix stripped — producing an 8-character `OF*` code that is the with its `@...` variant suffix stripped — producing an 8-character `OF*` code that is the
@@ -34,19 +34,23 @@ This page is the rule for authoring `filament_id` in system profiles
> [!IMPORTANT] > [!IMPORTANT]
> **Never write a `filament_id` value by hand.** A new filament gets its id from > **Never write a `filament_id` value by hand.** A new filament gets its id from
> `python scripts/orca_profile_tool.py generate-id`; one already in the tree has one — inherit it. > `python scripts/orca_id_tool.py --generate`; one already in the tree has one — inherit it.
## The design ## The design, in two pieces
Because several consumers match **globally by id alone, first hit wins** (see the next Because several consumers match **globally by id alone, first hit wins** (see the next
section), any two materials sharing one id feed wrong data somewhere — a wrong tray name, a section), any two materials sharing one id feed wrong data somewhere — a wrong tray name, a
wrong support-material flag, a wrong nozzle grouping — and inside one printer a duplicated id wrong support-material flag, a wrong nozzle grouping — and inside one printer a duplicated id
makes AMS spool matching a coin toss. Hand-written ids produce such collisions constantly, so makes AMS spool matching a coin toss. Hand-written ids produce such collisions constantly, so
the system is built to make them impossible: an id is a pure hash of the product's identity — the system is built to make them impossible:
no registry to maintain, no next-free-number ceremony, no way for two concurrent PRs to race
for the same number, and no way to get it wrong by hand, because you never write it by hand. 1. **Deterministic minting.** An id is a pure hash of the product's identity — no registry to
CI holds every id in the tree to that rule, so the profiles themselves are the whole record of maintain, no next-free-number ceremony, no way for two concurrent PRs to race for the same
which products exist and which bundles ship them. number, and no way to get it wrong by hand, because you never write it by hand.
2. **A sanctioned snapshot.** The complete id landscape derived from the tree must equal
`scripts/filament_id_snapshot.json` exactly, so every change to ids, claims (which bundles
ship which id, and for which filament), or product identity surfaces as a reviewable diff to
one file — the maintainer gate.
## Who consumes the id ## Who consumes the id
@@ -156,8 +160,8 @@ key needed). Tuning a generic material → **join the OrcaFilamentLibrary filame
different product by rule 5, so it then needs its own id. different product by rule 5, so it then needs its own id.
5. **Ids follow the product identity.** The id is a pure function of the product triple 5. **Ids follow the product identity.** The id is a pure function of the product triple
`(filament_vendor, filament_type, filament name)`, so correcting any of them re-mints the id `(filament_vendor, filament_type, filament name)`, so correcting any of them re-mints the id
**by design**, applied by `generate-id` (preview with `--dry-run`, confine with `--vendor`); **by design**, applied by `--generate` (preview with `--dry-run`, confine with `--vendor`) and
the exact sequence is in the FAQ. Nothing forwards gated by the `--update-snapshot` diff; the exact sequence is in the FAQ. Nothing forwards
the old value, so anything outside the tree that stored it — a device tray, a calibration the old value, so anything outside the tree that stored it — a device tray, a calibration
record, a saved project — falls back to matching by filament type until the user re-selects record, a saved project — falls back to matching by filament type until the user re-selects
the filament. Re-mint deliberately, and only to fix a genuinely wrong identity. the filament. Re-mint deliberately, and only to fix a genuinely wrong identity.
@@ -166,7 +170,7 @@ key needed). Tuning a generic material → **join the OrcaFilamentLibrary filame
## Minting — nobody invents ids ## Minting — nobody invents ids
New ids are deterministic, computed exactly like the `setting_id` precedent New ids are deterministic, computed exactly like the `setting_id` precedent
(the `setting_id` half of `scripts/orca_profile_tool.py generate-id`): (the `setting_id` half of `scripts/orca_id_tool.py`):
```text ```text
FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id") FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id")
@@ -192,13 +196,13 @@ Snapmaker bundles alike; the OFL generic `Generic/PLA/Generic PLA` mints `OFDSrz
by 35 bundles — most by independent declarations converging on the same mint, the rest by 35 bundles — most by independent declarations converging on the same mint, the rest
purely through inheritance from the OFL preset. purely through inheritance from the OFL preset.
Nothing but the triple feeds the mint — not the rest of the tree, not what another preset of Nothing but the triple feeds the mint — not the rest of the tree, not the snapshot, not what
the product happens to carry. Determined triple, determined id: one product another preset of the product happens to carry. Determined triple, determined id: one product
carries one id and there is no second acceptable value for it, so any other value on a preset carries one id and there is no second acceptable value for it, so any other value on a preset
is a mismatch `check` reports and `generate-id` pulls back. Two *different* products whose is a mismatch `--check` reports and `--generate` pulls back. Two *different* products whose
triples mint the same base62 value would be a collision (a roughly 36-bit id space against a triples mint the same base62 value would be a collision (a roughly 36-bit id space against a
few thousand products); nothing salts past it: `check` reports it naming both products, few thousand products); nothing salts past it: `--check` reports it naming both products,
`generate-id` refuses to write it, and the remedy is a rename so their triples differ. Where `--generate` refuses to write it, and the remedy is a rename so their triples differ. Where
two presets of one product would be AMS-ambiguous on a printer, the fix is likewise in the two presets of one product would be AMS-ambiguous on a printer, the fix is likewise in the
profiles — make their `compatible_printers` disjoint (structure rule 3), retire the redundant profiles — make their `compatible_printers` disjoint (structure rule 3), retire the redundant
preset, or, if they really are different products, give them different names so their triples preset, or, if they really are different products, give them different names so their triples
@@ -208,76 +212,80 @@ Workflow for a new filament:
```bash ```bash
# 1. Author the filament with NO filament_id key anywhere. # 1. Author the filament with NO filament_id key anywhere.
python scripts/orca_profile_tool.py generate-id --dry-run # 2. preview the ids — writes nothing python scripts/orca_id_tool.py --dry-run # 2. preview the ids — writes nothing
python scripts/orca_profile_tool.py generate-id # 3. apply them to the profile file(s) python scripts/orca_id_tool.py --generate # 3. apply them to the profile file(s)
python scripts/orca_profile_tool.py check # 4. validate — everything CI checks python scripts/orca_id_tool.py --update-snapshot # 4. record the new claims in the snapshot
python scripts/orca_id_tool.py --check # 5. validate the filament_id state
python scripts/orca_extra_profile_check.py # 6. ...and everything else CI checks
# 7. Commit the profile edits together with scripts/filament_id_snapshot.json, for review.
``` ```
`generate-id` makes every filament's id equal the mint of its own `--generate` makes every filament's id equal the mint of its own
`(filament_vendor, filament_type, filament name)` triple: it inserts one where an instantiated `(filament_vendor, filament_type, filament name)` triple: it inserts one where an instantiated
filament resolves none, and re-derives one that does not match. A preset that *inherits* a filament resolves none, and re-derives one that does not match. A preset that *inherits* a
mismatching id is the one case left to the author — check 2b names it, and the fix is to inherit mismatching id is the one case left to the author — check 3b names it, and the fix is to inherit
a preset of the same filament or to give the preset its own key. A declaration is left alone a preset of the same filament or to give the preset its own key. A declaration is left alone
exactly when it already equals the one id its triple mints, and a collision (check 2d) is exactly when it already equals the one id its triple mints, and a collision (check 3d) is
reported and left unwritten. The same run assigns reported and left unwritten. The same run assigns
`generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process `generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process
and machine preset of every vendor except BBL, which keeps its authoritative `G*` ids, strips and machine preset of every vendor except BBL, which keeps its authoritative `G*` ids, strips
`setting_id` from base profiles, and fixes the misspelled `settings_id` key — dropped, or, for `setting_id` from base profiles, and fixes the misspelled `settings_id` key — dropped, or, for
BBL, whose ids have no formula to fall back on, restored under the correct name. It is idempotent and BBL, whose ids have no formula to fall back on, restored under the correct name. It is idempotent and
byte-preserving (indentation, BOM, and line endings intact, every edited file re-parsed to fail byte-preserving (indentation, BOM, and line endings intact, every edited file re-parsed to fail
loudly), and a no-op on a tree that already passes `check`. loudly), and a no-op on a tree that already passes `scripts/orca_extra_profile_check.py` — the
check CI runs over both id kinds, of which `--check` is the `filament_id` half.
- `--filament-id` limits the run to `filament_id`. - `--filament-id` limits the run to `filament_id`.
- `--setting-id` limits the run to `setting_id`. The two exclude each other; pass neither to - `--setting-id` limits the run to `setting_id`. The two exclude each other; pass neither to
write both. write both.
- `--vendor VENDOR` confines the run to that bundle; repeatable. The id is a function of the - `--vendor VENDOR` confines the run to that bundle; repeatable. The id is a function of the
triple alone, so a narrowed run writes exactly what a full one would; `check` reports triple alone, so a narrowed run writes exactly what a full one would; `--check` reports
whatever it left outside. whatever it left outside.
- `--dry-run` reports what the run would do and writes nothing, so - `--dry-run` reports what `--generate` would do and writes nothing; with no mode of its own it
`generate-id --dry-run --vendor <Vendor>` previews just that bundle. implies `--generate`, so `--dry-run --vendor <Vendor>` previews just that bundle.
- `--profiles DIR` points the tooling at a different profile tree (default - `--profiles DIR` points the tooling at a different profile tree (default
`resources/profiles`). `resources/profiles`). `--check` and `--update-snapshot` read and write the sanctioned state of
the tree they are given, so pointing them elsewhere needs `--snapshot PATH` for that tree too —
`scripts/filament_id_snapshot.json` describes `resources/profiles` and no other tree.
The tool's other commands maintain the tree around the ids: `fix` normalises profile files, **Identity fixes need no separate mode.** `--generate` re-derives an id that no longer matches its
`trim` drops files no `<vendor>.json` list references, and `update-index` rebuilds those lists. triple exactly the way it fills in a missing one, so a rename or a `filament_vendor` /
They do not touch ids; `--help` documents them. `filament_type` correction is just: fix the config, run `--generate` (confine it with `--vendor`,
preview it with `--dry-run`), then `--update-snapshot` and review the diff.
**Identity fixes need no separate command.** `generate-id` re-derives an id that no longer matches
its triple exactly the way it fills in a missing one, so a rename or a `filament_vendor` /
`filament_type` correction is just: fix the config and run `generate-id` (confine it with
`--vendor`, preview it with `--dry-run`).
If you skip the tooling, CI fails and prints the remedy: the expected id for your filament and If you skip the tooling, CI fails and prints the remedy: the expected id for your filament and
the instruction to run `python scripts/orca_profile_tool.py generate-id`. the instruction to run `python scripts/orca_id_tool.py --generate`; once the id is minted, the
snapshot checks likewise point at `--update-snapshot` and tell you to commit the resulting
diff.
## Ids other systems compose ## Reserved namespaces — never mint or hand-write into
Every filament profile carries a minted id, with no exceptions and no spellings held back for A **reserved namespace** is an id space no system profile may declare, because an external
anyone. There is therefore no reserved namespace to respect and no bundle that owns one: an id catalog or a device protocol owns the values. None of them has an owning vendor: there is no
some other system composes for its own purposes is simply not the mint of a triple, so it bundle — not even the one whose printers use the catalog — that may write one into a profile.
cannot be a system profile's `filament_id`, and the format check rejects it for that reason
alone — same error, same remedy, whoever wrote it.
Three such spaces exist around us, and are worth recognising so nobody mistakes one for an id | Space | Status | Rule |
to copy into a profile: | --- | --- | --- |
| `GF*` | Bambu AMS/RFID catalog | declarable by **nobody**, BBL included: Bambu's own ids live in the generated catalog map, never in a profile |
| `QD_*` | Qidi device protocol | declarable by **nobody**, Qidi included: the box composes these ids at runtime and they are not preset ids |
| `P` + 7 hex chars (case-insensitive), `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp`) | never appears in system profiles |
- **Bambu's `GF*` catalog.** Bambu's device / RFID / cloud catalog is external and opaque. Every The two device namespaces, in detail:
BBL filament mints an `OF` id from its triple like every other vendor's, and the
correspondence to Bambu's catalog ids lives in one generated file the app applies at the - **Bambu (`GF*`).** Bambu's device/RFID/cloud catalog is external and opaque, which is a
printer boundary — the next section. Note that `GF` is a *prefix*, not a namespace the tree reason to keep it out of the profiles rather than to let one bundle own it. Every BBL filament
avoids: BBL's authoritative `setting_id` values include `GF`-prefixed ones, and mints an `OF` id from its triple like every other vendor's, and the correspondence to Bambu's
`resources/profiles/blacklist.json` and `BBL/filament/filaments_color_codes.json` both catalog ids lives in one generated file the app applies at the printer boundary — the next
reference Bambu catalog ids by design. The rule is about `filament_id` and nothing else. section. Nothing under `resources/profiles/**` carries a `GF*` id today and nothing can be
- **Qidi's `QD_*` protocol ids.** The Qidi box composes `QD_<series>_<vendor>_<typeidx>` at exempted, so a `GF*` id appearing anywhere in the tree is a mistake, whoever wrote it.
runtime (slot vendor and type indices reported by the device, the series digit inferred - **Qidi (`QD_*`).** `QD_*` is a device-*protocol* namespace, not a preset id space: the
client-side from the printer model/name). Qidi presets carry ordinary minted `OF*` ids Qidi box path composes `QD_<series>_<vendor>_<typeidx>` ids at runtime (slot vendor and
(generics share the OFL ids), so a composed id matches no preset and the slot falls back to type indices reported by the device, the series digit inferred client-side from the printer
filament type; translating it to the filament's id belongs in `QidiPrinterAgent`. Treating model/name). Qidi presets carry ordinary minted `OF*` ids (generics share the OFL ids), so
per-series protocol ids as preset ids would put one product under five ids a composed id matches no preset and the slot falls back to filament type; translating it to
(`QIDI PLA Rapido` would be `QD_0_1_1` through `QD_4_1_1`) — exactly the fragmentation the the filament's id belongs in `QidiPrinterAgent`. The alternative — treating per-series
mint rule removes. protocol ids as preset ids — would put one product under five ids (`QIDI PLA Rapido` would
- **`P` + 7 hex chars, and `"null"`.** What `CreatePresetsDialog.cpp` gives a filament a *user* be `QD_0_1_1` through `QD_4_1_1`), exactly the fragmentation the mint rule removes.
creates. Those are user presets, not system profiles, and the two never meet in the tree.
## The Bambu catalog map ## The Bambu catalog map
@@ -329,7 +337,7 @@ OrcaFilamentLibrary. **135 is the number to expect at every regeneration** — 1
one-off size of the transition and stopped being computable from the tree once the BBL bundle one-off size of the transition and stopped being computable from the tree once the BBL bundle
was re-minted, so do not "fix" the report to print it. was re-minted, so do not "fix" the report to print it.
**Check 4** lives in `check_filament_ids`, so profile CI runs it alongside the other three. It **Check 6** lives in `check_filament_ids`, so profile CI runs it alongside the other five. It
holds the file to its contract: it parses, carries `source` / `bambustudio_commit` / holds the file to its contract: it parses, carries `source` / `bambustudio_commit` /
`generated`, keys only `OF`-format ids, maps each Bambu id at most once, and — for every row `generated`, keys only `OF`-format ids, maps each Bambu id at most once, and — for every row
whose key the tree actually claims — agrees with the tree on that id's `(vendor, type, name)` whose key the tree actually claims — agrees with the tree on that id's `(vendor, type, name)`
@@ -418,14 +426,23 @@ map would silently reproduce the bug.
## How CI enforces this ## How CI enforces this
Profile CI (`check_profiles.yml`) runs `check_filament_ids()` tree-wide via Profile CI (`check_profiles.yml`) runs `check_filament_ids()` tree-wide via
`scripts/orca_profile_tool.py check`. Every check judges the tree against the rules on this `scripts/orca_extra_profile_check.py`. Its ground truth is
page and nothing else — there is no recorded id state to match and no grandfather list of any **`scripts/filament_id_snapshot.json` — the sanctioned state**: the id state derived from the
kind. tree must equal the snapshot exactly, in both directions. Any change to the id landscape
therefore surfaces as a diff to that file, and **that snapshot diff is what maintainers review
and gate in a PR**. Never edit the snapshot by hand — `--update-snapshot` regenerates it
deterministically (running it twice changes nothing). The snapshot holds one map, `ids`: each
entry is the product the id is minted from (`filament_vendor`, `filament_type`, `name`) and the
`filaments` claiming it (`Vendor/Filament`), and it sanctions *state*, never exceptions: no check
consults it to excuse a preset from a rule, and there is no grandfather list of any kind.
The checks, in brief: The checks, in brief:
- **Format** — every id occurring in the tree is `OF` + 6 base62 chars. No exceptions, not - **Format** — every id occurring in the tree is `OF` + 6 base62 chars. No exceptions: not a
even BBL. snapshot entry, not BBL.
- **Snapshot equality** — tree claims == snapshot claims **and** each id's declared triple ==
its snapshot entry, both directions: any `filament_vendor`/`filament_type`/name change
surfaces as a snapshot diff.
- **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the - **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the
one id its declarer's own triple mints, with no second acceptable value; the id an one id its declarer's own triple mints, with no second acceptable value; the id an
instantiated preset *inherits* must equal the mint of *its* own triple, however it inherits instantiated preset *inherits* must equal the mint of *its* own triple, however it inherits
@@ -433,6 +450,8 @@ The checks, in brief:
system filament must resolve an effective id at all (recall: an id-less one is a hard load system filament must resolve an effective id at all (recall: an id-less one is a hard load
error in C++ that discards the whole vendor bundle); and no two products mint one id (a error in C++ that discards the whole vendor bundle); and no two products mint one id (a
base62 collision, resolved by renaming one of them). The errors print the expected id. base62 collision, resolved by renaming one of them). The errors print the expected id.
- **Reserved namespaces** — `GF*`, `QD_*`, `P<7-hex>` or `"null"` claimed by any vendor,
BBL and Qidi included.
- **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and - **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and
`filament_type` (generics use `"Generic"`), and all declarers of one filament within a `filament_type` (generics use `"Generic"`), and all declarers of one filament within a
bundle must agree on the triple. bundle must agree on the triple.
@@ -442,14 +461,16 @@ The checks, in brief:
tree claims. See [The Bambu catalog map](#the-bambu-catalog-map); the remedy is always to tree claims. See [The Bambu catalog map](#the-bambu-catalog-map); the remedy is always to
regenerate, never to hand-edit. regenerate, never to hand-edit.
A profile that declares an id no triple mints — a Bambu catalog id, a composed Qidi one, a A profile that declares a **reserved-namespace** id — `GF*`, `QD_*` or `P<7-hex>`, whatever
hand-typed value, whatever its vendor — fails the format check. For a Bambu-cataloged product its vendor — cannot pass the format check, so `--update-snapshot` refuses to sanction it
the catalog map is where the correspondence belongs. Two products sharing one id are caught by rather than hide the mistake until CI. For a Bambu-cataloged product, the catalog map is where
the identity check whether the id is declared or inherited. the correspondence belongs. Any other new sharing via a *declared* id is caught by the identity
check; sharing through inheritance carries no declaration to check and surfaces only as a new
claim in the snapshot diff — which is exactly why that diff is the gate.
The same `check` run holds every declared id to the AMS 8-character limit, tree-wide and for `orca_extra_profile_check.py` separately holds every declared id to the AMS 8-character limit,
every vendor alike, scoped to the presets a vendor's index actually references (a file the index tree-wide and for every vendor alike, scoped to the presets a vendor's index actually
never loads cannot break AMS matching). references (a file the index never loads cannot break AMS matching).
Complementing the Python checks, CI also runs the C++ profile validator with `-f` Complementing the Python checks, CI also runs the C++ profile validator with `-f`
(`check_filament_subtypes`): it loads the bundle exactly as the app does and flags any printer (`check_filament_subtypes`): it loads the bundle exactly as the app does and flags any printer
@@ -467,21 +488,21 @@ ambiguity check behind structure rule 3.
`Generic PLA` base name, set `compatible_printers`; no id key needed. `Generic PLA` base name, set `compatible_printers`; no id key needed.
- **A branded filament that borrows a generic's settings?** Fine — inherit `Generic X @System` - **A branded filament that borrows a generic's settings?** Fine — inherit `Generic X @System`
(or any real filament) for the settings and declare the id of your own filament; run (or any real filament) for the settings and declare the id of your own filament; run
`python scripts/orca_profile_tool.py generate-id` to mint it. Inheritance never changes the id. `python scripts/orca_id_tool.py --generate` to mint it. Inheritance never changes the id.
- **I need to fix a filament's `filament_vendor` or `filament_type`.** Fix the config, run - **I need to fix a filament's `filament_vendor` or `filament_type`.** Fix the config, run
`generate-id --vendor <Vendor>` (preview with `--dry-run`), and commit the result. The id `--generate --vendor <Vendor>` (preview with `--dry-run`), then `--update-snapshot`, and commit
re-derives from the corrected identity, and the profile and snapshot diffs together. The id re-derives from the corrected identity, and
nothing forwards the old value, so a tray or record still holding it falls back to matching by nothing forwards the old value, so a tray or record still holding it falls back to matching by
filament type. filament type.
- **I need to rename a filament.** Rename the presets (adding `renamed_from`, which keeps the - **I need to rename a filament.** Rename the presets (adding `renamed_from`, which keeps the
preset *name* resolving), then `generate-id --vendor <Vendor>` (preview with `--dry-run`). The preset *name* resolving), then `--generate --vendor <Vendor>` (preview with `--dry-run`), then
id follows the new filament name; as with any identity fix, the old id `--update-snapshot`. The id follows the new filament name; as with any identity fix, the old id
is not forwarded. is not forwarded.
- **Can I reuse a `QD_*` id for a Qidi profile?** No — it is not a mint, so it is not a - **Can I reuse a `QD_*` id for a Qidi profile?** No — nobody can. It is the device protocol's
`filament_id`. Those values are composed by the box at runtime, and no preset carries one. own id space: the box composes those values at runtime and no preset carries one. Author
Author Qidi filaments like any other vendor's. Qidi filaments like any other vendor's.
- **CI says my filament needs an id.** Run `python scripts/orca_profile_tool.py generate-id` and - **CI says my filament needs an id.** Run `python scripts/orca_id_tool.py --generate`, then
commit the result. Do not type an id by hand. `--update-snapshot`, and commit both diffs. Do not type an id by hand.
For general profile authoring, see the profile development guide on the For general profile authoring, see the profile development guide on the
[OrcaSlicer wiki](https://www.orcaslicer.com/wiki). [OrcaSlicer wiki](https://www.orcaslicer.com/wiki).
-132
View File
@@ -1,132 +0,0 @@
# Keyboard Shortcuts
## Why it exists
Key events arrive in several windows (the main frame's char hook, the 3D canvases, the
gizmo manager and the object list), and the same keys are shown again in menu labels,
toolbar tooltips, gizmo names and the shortcuts dialog. The registry is the one table all
of them read. Each binding is defined once; dispatchers look key events up there, labels
are derived from it, and a change the user makes updates all of them.
## Data model
`KeyChord` (`src/slic3r/GUI/KeyChord.hpp`) is one key press: the key code as
`wxEVT_KEY_DOWN` reports it, plus the `wxMOD_*` modifiers held with it. It has two
text forms. The canonical one (`Ctrl+Shift+S`) is platform-neutral and doubles as the wx
accelerator string and the config format. The display one uses translated modifier
names and the command and option glyphs on macOS. `KeyChord::from_event()` turns any wx
key event into the same key code and modifiers, so a chord recorded in the dialog is
equal to the chord a dispatcher builds from the key press.
`Shortcut` is the enum of every user-facing binding. `shortcut_table` in
`src/slic3r/GUI/Shortcuts.cpp` gives each one a config key, a description, a context
mask, a default chord, a `repeatable` flag and a `modifier_variants` flag, in the order
the dialog lists them; a `static_assert` keeps the table and the enum in step.
`ShortcutRegistry` overlays the user's overrides on the defaults and keeps a
chord-to-shortcut index for lookups. It reads and writes the `shortcuts` section of
`AppConfig`. Only overrides are stored, so a default can change between releases
without touching anyone's config; `none` records a shortcut the user unbound.
## Contexts
A key press is looked up in the context of the window that received it.
| Context | Dispatcher | Examples |
|--------------|-----------------------------------------------------------|-------------------------------|
| `Global` | `MainFrame`'s `wxEVT_CHAR_HOOK`, before any child sees it | New project, camera views |
| `Plater` | `GLCanvas3D` of the 3D and assembly views | Arrange, gizmo activation |
| `Preview` | `GLCanvas3D` of the G-code preview | One-layer mode, jump to layer |
| `ObjectList` | the object list | Copy, delete, auto drop |
| `Painting` | `GLGizmosManager` while a painting gizmo is open | Circle, sphere, fill tools |
A shortcut can belong to several contexts, which is how copy and paste are a single
binding for the canvas and the object list. Two shortcuts can share a chord when their
contexts do not overlap; `C` is the cut gizmo in the 3D view, the G-code window in the
preview and the circle tool while painting. A Global chord is dispatched before every
other context, so the dialog treats it as conflicting with all of them.
A Global shortcut has to include Ctrl or Alt or use a key that types nothing, since a
bare printable key in the frame hook would swallow that character in every text field.
The dialog refuses such chords and `ShortcutRegistry::load()` drops them from the config.
Space counts as typing. The speed dial's default is the one bare Space, and
`MainFrame` leaves it to a focused control that uses Space itself (text fields, buttons,
combo boxes), so it opens the dial from the canvases and the tab strip only.
## Which event a chord matches
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
on the keyboard layout: the key labelled `Q` on an AZERTY keyboard and the key in the
same position under a Cyrillic layout both report `Q`. Numpad keys fold onto their main
keyboard equivalents, so `Ctrl+1` and `Ctrl+Numpad 1` are one binding.
Punctuation matches on `wxEVT_CHAR`, because only the char event knows which character
a key produced under the active layout. `+` is Shift and `=` on a US keyboard and a key
of its own on a German one, and the binding means the character in both cases. The
canvas looks a key up on key-down first and, when nothing matched, once more on the char
event, for punctuation chords only. The dialog records chords the same way: a printable
non-alphanumeric key pressed with nothing but Shift is taken from the char event that
follows.
wxGTK does not report key auto-repeat, so the canvases share one record of the keys
seen going down and swallow the repeats of every shortcut not marked `repeatable`. Zoom
and undo repeat, for example; a toggle such as Tab does not. The record is shared because
a shortcut can move the focus to another canvas while its key is still held; a key
released while no canvas had the focus is dropped on the next press.
A few shortcuts have `modifier_variants`: Shift or Ctrl added to their binding selects a
step of the same action (1 mm and camera-space moves of the selection, five-step slider
moves). Only a binding without Shift or Ctrl of its own has steps, so no two bindings
share one. `ShortcutRegistry::match()` looks the exact chord up first and only then, when
nothing is bound to it, looks for such a shortcut whose binding is the chord minus those
modifiers, reporting which were added; a binding on Ctrl+Shift+key therefore wins over
the combined step. The Shift and Ctrl steps themselves are reserved. `step_owner()` names
the shortcut they belong to, the capture dialog refuses to assign them, and
`conflicts()` reports exact chords only. A binding made before its key became a stepping
key keeps its chord and shadows that one step. A move or rotation of the selection
started from the keyboard runs until the key that started it is released, or the
canvas loses focus, so a held key is one undo step.
## Labels
Menu labels, toolbar tooltips, gizmo names, the context menu and the shortcuts dialog
read the registry, so a rebinding shows up in all of them. Each tracked menu item keeps
its base label; `MainFrame::update_shortcut_labels()` appends the current binding
again after an edit, which also installs the new wx accelerator.
A chord that is unsafe as a menu accelerator, meaning a bare printable key, is appended
to the label as plain text so the menu cannot take it away from text fields. The macOS
edit menu shows its clipboard and undo entries that way, because a system-menu key
equivalent for Cmd+C would run instead of the text field's own copy.
On macOS the object list receives no key events at all, so its bindings are installed as
a `wxAcceleratorTable`, regenerated from the registry after each edit.
## Editing
The shortcuts dialog has a page per context, each opening with a line that says when its
keys apply. A page lists the shortcuts under the headings of `section_table`, with the
fixed keys that cannot change (mouse buttons, the step modifiers, Esc, the digit keys
that pick a filament) sorted into the same sections. The mouse drag rows describe the
camera actions chosen in Preferences; their button opens Preferences > Control with that
option scrolled into view and focused, instead of editing a key.
Editing a row opens a capture dialog that records the next chord, names the shortcuts it
would take the chord from, and on confirmation unbinds those and binds this one.
Resetting a row asks the same question when its default is now held by another
shortcut, so a reset cannot leave two shortcuts on one chord. Each change is written to
the config at once and pushed to the menus, tooltips and accelerator tables through
`GUI_App::on_shortcuts_changed()`. The dialog opens from the Help menu and Preferences >
Control on the Global page, and from the `?` key on the page of the view that received it.
## Adding a shortcut
1. Add the enum value to `Shortcut` and its row to `shortcut_table`, in the position
the dialog should list it; the row's section heading is the `section_table` entry
above it, so a new section needs an entry there too. Pick a default that does not
collide inside its contexts; the `[Shortcuts]` tests check every default against the
others.
2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`,
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's
`on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor.
3. Where the UI shows the key, ask the registry (`display()` for tooltips,
`accelerator()` for menu labels); no label holds a literal key name.
-129
View File
@@ -1,129 +0,0 @@
# Multiline infill — High Level Design
## Purpose and scope
`fill_multiline` prints every sparse infill wall as N adjacent lines instead of
one, so a wall is `d1 = N * spacing` thick. Only internal sparse infill uses it.
Each pattern first builds its single-line centerlines at N times the usual line
spacing (so the density holds), and `multiline_fill()` then replaces each
centerline by the lines of that wall: the centerline itself when N is odd, and
closed outlines around it at every `spacing` out to `d1 / 2`. The outlines are
clipped to the fill region contracted by half a line width, then connected like
any other infill.
Outlines of centerlines that cross each other overlap at every crossing, which
over-extrudes the wall intersections. The line-crossing patterns Grid,
Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross
(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and
Support Cubic (`FillAdaptive`); the other patterns outline their usual
centerlines.
## Non-crossing centerlines
The crossing lines are resolved into x-monotone paths, the levels of the line
arrangement: walking along x, the k-th path is always the k-th line from the
bottom. At every crossing, the two paths bounce off each other instead of
passing through. Adjacent paths meet only at crossings, so their outlines touch
there and nowhere overlap.
Where two paths meet, each is cut short by a line perpendicular to the bisector
of its bend, `d1 / 2` from the crossing. The two cut segments are parallel and
`d1` apart, so the outermost lines of the two walls sit exactly `spacing` apart,
like the lines inside a wall. Where three lines meet at one point, the middle
path runs straight through and the outer two are cut `d1` from it.
Each pattern builds its rows along x in a rotated frame. Grid lines run at ±45°
there, and its rows are trapezoid waves that transpose on alternate layers. The three families of Triangles, Tri-hexagon and
Cubic run at 0°, 60° and 120°. Those rows rotate by 120° every layer about a
3-fold center of the arrangement, so each family takes every role in turn.
The pattern is phased on fixed positions, so it lines up across layers and
across the regions of one layer. Rounding the corners with
`sparse_infill_smooth_factor` happens before `multiline_fill()`.
Each region builds only the rows over its bounding box in that frame, and
outlines only the centerlines within `d1 / 2` of it, the ones whose outlines
reach it. Every row is monotone along its direction, so each outline is started
on the cap at the first end of its centerline, outside the region, and clipping
to the region cuts it only where it crosses the boundary.
## Cubic
Single-line Cubic draws the three families at the same spacing `h` and shifts
them with z: by `+dx`, `-dx` and `+dx`, `dx = z / sqrt(2)`. The multiline paths
follow the same lines. In the frame where one family is horizontal, the other two
cross in rows `h` apart, alternating by half a period, at height
`tau = -3 * dx (mod h)` above the horizontal line below them. The crossings split
every band between horizontal lines into up-pointing triangles of height `tau`,
down-pointing triangles of height `h - tau`, and hexagons. At `tau = 0` (and `h`)
all three families meet at common points, as in Triangles. At `tau = h / 2` the
triangles are equal, as in Tri-hexagon. The origin of that frame is always a
3-fold center, whatever z is, so the per-layer rotation keeps the lines in place.
Each band holds two paths that touch at its crossings: the upper one takes the
V below the crossing and runs along the top horizontal line, and the lower one
takes the inverted V above it and runs along the bottom line. Both are the same function
of `tau`, the lower one mirrored with `h - tau`. `cubic_upper_level()` builds one
period of the upper path as the lower envelope of five lines, clipped from below:
- the two slanted lines through the crossings,
- the horizontal line, lowered when the triangle above it is less than `1.5 * d1` high,
- the two chamfers where the path turns onto and off the horizontal line, `d1 / 2`
from those crossings,
- the flat cut into the V at the crossing.
The cut height `clamp(tau - d1 / 2, 0, h - d1) + d1` is what makes the pattern
continuous in z. While both triangles are at least `1.5 * d1` high, every
crossing is a pair of bends `d1 / 2` from it, as in Tri-hexagon. When a triangle
is thinner, its three paths stack like a triple crossing. The path through it
flattens toward its base line and lies on it once the triangle is under `d1 / 2`
high, and the paths beside it are pushed `d1` away. The layout thus reaches the
Triangles one where the families meet. Adjacent paths stay at least `d1` apart
at every `tau` and at every density up to 100%.
## Adaptive Cubic
Adaptive Cubic and Support Cubic take their lines from an octree of cubes
standing on a corner. On each layer every cube cuts its three mid-planes into
segments of the same three 60° families as Cubic, but the pattern is not
periodic. Smaller cubes near the surface add finer lines, and a finer line ends
where it meets the wall of its coarser cube, so the lines form crossings and
T-junctions. `FillAdaptive::multiline_paths()` builds the paths from these
segments directly, for each fill region and within `4 * d1` of it.
At a crossing the two paths bounce as in Cubic. At a T-junction the through line
runs straight on and the path of the ending line stops there. Every path still
runs left to right in the frame where one family is horizontal, and that family
rotates with the layer.
Every line of every cube size lies on one fine lattice, so crossings closer than
a few `d1` are the corners of one small triangle of that lattice, as in Cubic.
The cuts follow the Cubic rules without a closed formula:
- The two bends of a crossing are cut `d1` apart, `d1 / 2` each, perpendicular
to their bisector, so their walls touch. A cut goes no further than the path
end, and the other bend takes the rest of `d1`.
- At the tip of a small triangle, between the two slanted families, a cut also
goes no further than the neighbouring bend turning the other way, and the
path beyond that bend is kept a wall away from it. The bends onto the
horizontal family are not limited this way: pushing their paths apart would
open gaps between walls that should touch.
- A cut moves the path only where the cut line lies beyond it, near its bend.
The sharp bends between the two slanted families are cut after the bends onto
the horizontal family, so the tip of a small triangle wins, as in Cubic.
- A path stopping at a T-junction is trimmed until it is `d1` less half a line
spacing from every other path, so that its end overlaps the wall it stops on
by half a line and bonds to it. The paths are trimmed one at a time against
the others as already trimmed, so two ends facing each other meet instead of
both backing off. A path stopping on the line of another is trimmed before
that one, so it gives way and the other still reaches the line it stops on. A
second round trims every path again from its full length, so an end grows
back where the ends it gave way to were trimmed later, and a last round only
shortens them, keeping them that far apart. Paths shorter than `d1` are left
out.
- A line that ends on another less than `2 * d1` past a crossing stops at that
crossing instead, the shorter one where both do. The path along such a stub
would be trimmed away, leaving a hole between the walls that were cut to
touch it.
Short paths enclosed by coarser lines still print as closed outlines, but most
paths run on across several cells.
-239
View File
@@ -1,239 +0,0 @@
# Precise Seam — High Level Design
## Purpose and scope
Precise Seam places the seam where a helper volume intersects the external
wall. The user attaches a mesh to an object as a Precise Seam modifier, and on
every layer the seam placer reads the modifier's slice to decide where the seam
of each external perimeter may, must or must not go. The same mesh keeps
working after the model changes, so the seam does not have to be repainted
after every design revision, and a swept helper body can guide the seam along
any path.
The modifier is non-printing geometry. It does not take part in slicing, region
assignment, filament selection or brim adhesion. It affects only seam
placement, which runs during G-code export.
## Volume types and priority
Precise Seam adds six `ModelVolumeType` values after `SUPPORT_ENFORCER`. The
strong types come first and the weak types follow. `is_precise_seam()`,
`is_precise_seam_strong()` and `is_precise_seam_weak()` are range checks that
depend on this order.
| Type | Group | Effect on the perimeter |
| --- | --- | --- |
| `PRECISE_SEAM_CENTER` | strong | seam at the arc-length midpoint of the intersection |
| `PRECISE_SEAM_LEFT` | strong | seam at the first point of the intersection |
| `PRECISE_SEAM_RIGHT` | strong | seam at the last point of the intersection |
| `PRECISE_SEAM_ENFORCED` | weak | intersection marked as enforced |
| `PRECISE_SEAM_BLOCKED` | weak | intersection marked as blocked |
| `PRECISE_SEAM_NEUTRAL` | weak | intersection reset to neutral |
A strong modifier fixes one point. A weak modifier only changes the
enforced/blocked type of seam candidates, and the configured seam position then
chooses among them. First and last are taken along the perimeter made
counter-clockwise seen from above. On an outer wall seen from outside, Left is
the left end of the intersection. On the wall of a hole seen from inside the
hole, the two ends are swapped.
The order of volumes in the object is the priority order, highest first.
`ModelObject::sort_volumes()` keeps every strong modifier before every weak one
and preserves the user's order within each group. The object list lets the user
drag a modifier only within its own group. A type change that crosses a group
boundary moves the volume to the end of its new group, where it has the lowest
priority. Strong modifiers are tried in this order, and the first one that
yields a seam on a perimeter wins. Weak modifiers are applied from the lowest
priority to the highest, so the highest one overwrites any overlapping zone.
## Model storage and 3MF compatibility
Projects must stay readable by earlier releases, and the modifier must not
change a print there. Both 3MF writers therefore store a Precise Seam volume as
an ordinary parameter modifier: `modifier_part` in the Bambu-format part
subtype, and `ParameterModifier` together with the legacy `modifier` flag in
the Prusa-format volume metadata. The seam mode is written separately under
`precise_seam_type`, using the names from `ModelVolume::type_to_string()`
(`precise_seam_center` and so on).
On load, the mode applies after all other volume metadata, regardless of XML
key order, and only when the base type is a modifier. Missing or unknown modes
leave an ordinary modifier. Seam metadata on any other base type is ignored.
Files that stored the seam mode directly as the volume type still load.
A Precise Seam volume keeps any per-volume settings it had as a part or
modifier, but they are inactive and the object list shows no settings item for
it. The writers prefix these keys with `precise_seam_config:`, so an earlier
reader drops them as unknown options. The volume therefore loads there as a
modifier without settings and has no effect on the print. The current reader
restores the keys only when the volume ends up as a Precise Seam type, so the
settings return when the user changes the type back. Configuration values are
XML-escaped in both writers, for every volume type.
## Print invalidation
`Print::apply()` compares the Precise Seam volumes of each object by type, ID
and transformation. Adding, removing, moving, reordering or retyping one
cancels background processing and invalidates only `psGCodeExport`; the sliced
layers are kept. `model_volume_list_update_supports_and_seams()` then brings
the support and Precise Seam volumes of the print's model copy in line with the
new model in one pass. A volume may switch between the two families, since
neither affects slicing. A conversion to or from a part or ordinary modifier
changes the solid and modifier volume lists and reslices as before.
## Modifier slices
`SeamPlacer::init()` collects the Precise Seam volumes of each object once:
strong ones in priority order and weak ones reversed. It slices each volume
separately with `PrintObject::slice_single_volume()`, which shares
`slice_modifier_volumes()` with support blockers and enforcers but does not
merge volumes, so each keeps its own priority. The result is cached per volume
and indexed by object layer; `Layer::id()` includes raft layers, which are
subtracted. Seam candidates are then gathered in parallel over the layers and
read the cache without locking.
Objects without Precise Seam volumes follow the unchanged seam placement path.
For objects that have them, perimeter extraction also removes consecutive
duplicate points and the repeated closing point of each extrusion loop.
Zero-length edges at path junctions would otherwise prevent point insertion
there. Distinct visits to one point of a self-touching contour are kept.
## Finding the wall segment
The seam placer works on the external perimeter loops of each layer, both
outer contours and holes, each made counter-clockwise. For every modifier
polygon on the layer that overlaps the perimeter's bounding box, the region
enclosed by the perimeter is clipped against the modifier polygon. The boundary
of each intersection polygon alternates between runs that follow the perimeter
and runs that follow the modifier outline. The wall segment is the longest
continuous run of intersection vertices that lie on the perimeter, measured in
vertices.
The fast path first finds an intersection vertex that exactly matches a
perimeter vertex. It then walks forward and backward, expecting the adjacent
perimeter vertex and falling back to projection when Clipper has merged or
split collinear edges. A vertex counts as on the perimeter when its projection
is within about 1.6 nm, which covers Clipper's rounding. If no vertex matches
exactly, or every vertex lies on the perimeter, the general path projects all
vertices. When every vertex is on the perimeter, the edge midpoints are checked
instead: a modifier chord can join two perimeter vertices directly, and the
chords split the vertex ring into runs. If no edge leaves the perimeter, the
perimeter lies entirely inside the modifier.
`Polygon::point_projection()` optionally reports the edge that holds the
projection, and every point of the segment keeps the index of its perimeter
edge. New points are inserted on that edge. A point within 1 µm of an existing
vertex snaps to that vertex instead.
## Strong modifiers
For a strong modifier, the target is the first point, the last point or the
arc-length midpoint of the segment. The midpoint is projected back onto the
original perimeter, because Clipper may have merged several perimeter edges
into one segment edge. The target is inserted into the perimeter, and a helper
point is inserted 1 µm before and after it. Strong modifiers are tried in
priority order, the first valid intersection decides the seam, and weak
modifiers are not processed for that perimeter.
When candidates are built, the inserted point is the only enforced candidate
and becomes the central enforcer; every other candidate is blocked. The seam
position modes then pick that point: Aligned and Aligned Back prefer the central
enforcer, while Back, Random and Nearest rank enforced candidates above blocked
ones. Alignment and random placement can still move the final position along an
edge. After alignment, `restore_precise_seam_positions()` writes the exact point
and its index back into every perimeter that has a strong seam. Inner walls take
their seam from the external seam as usual, including staggering.
## Weak modifiers
Weak modifiers produce one segment per intersection polygon, so one modifier can
mark several zones on one perimeter. All segment boundaries are inserted into
the perimeter in order of decreasing arc length. Each insertion then leaves the
indices of the pending, shorter ones unchanged; a point on the closing edge is
appended rather than inserted at index zero. A helper point is added 1 µm
outside each boundary. Random placement picks a position along the edge that
follows a candidate. These helpers keep that edge 1 µm long at each boundary, so
a zone cannot extend or intrude further than that. Boundaries that coincide
share their helper points.
The zone types are then resolved in priority order, and the edges of enforced
zones are subdivided into steps of at most
`SeamPlacer::enforcer_oversampling_distance` (0.2 mm). The middle candidate of
the longest enforced patch is therefore close to the geometric middle of the
zone. That patch is measured in candidates, across the closing edge, regardless
of where the contour starts; the same rule applies to painted seams.
Candidates first receive their type from seam painting. The weak zones then
overwrite it, lowest priority first. Blocked and Enforced zones therefore take
precedence over painting, and Neutral clears painting inside its zone.
## Unsupported geometry and warnings
Some modifier shapes cannot be resolved to one seam or one zone per crossing.
They are detected cheaply and reported rather than guessed:
- A strong modifier that crosses a perimeter in more than one place uses only
its first valid segment. The other crossings are ignored.
- A modifier that crosses the whole region enclosed by the perimeter is
detected when the modifier outline minus that region leaves more than one
piece, none of them a hole. Its intersection holds two wall runs, and only
one of them is used.
- A modifier whose slice has a hole on a layer, found as a clockwise polygon in
the flattened slice, is skipped on that layer. The flattened slice no longer
records which hole belongs to which contour.
- A perimeter that lies entirely inside a modifier is ignored by that modifier.
The conditions are atomic flags shared by all layers and objects. After all
objects are processed, `SeamPlacer::init()` issues at most one non-critical
warning with the ID `SlicingPreciseSeamWarning`. The warning is a single line
that lists every cause found, because the export warnings dialog shows only the
first line of each warning. Repeated warning events replace this notification
instead of appending text to it.
## User interface
- *Add Precise Seam* in the object menu creates a Center modifier from a
primitive or a loaded mesh. Text and SVG volumes cannot become Precise Seam
modifiers: the menu does not offer them, and `ObjectList::set_volume_type()`
refuses the change.
- *Change Type* has a single *Precise Seam* entry. It converts other volumes to
Center and keeps the mode of volumes that are already Precise Seam. The
*Precise Seam Type* submenu appears only when every selected item is a
Precise Seam volume, including settings rows that resolve to one. It sets the
chosen mode on all selected volumes.
- Each mode has its own icon in the object list and its own color in the 3D
view, at 60% opacity: warm oranges for the strong modes, and green, red and
gray for Enforced, Blocked and Neutral.
- Object list drops map visible rows to volume indices while skipping hidden
cut connectors, and they refresh the row-to-volume map of the object.
- Precise Seam volumes have no filament, block pasting into SLA, and are exposed
to Python plugins as `ModelVolumeType` values plus the `is_precise_seam*()`
methods.
## Implementation and verification
- [PreciseSeam.cpp](../../src/libslic3r/GCode/PreciseSeam.cpp) implements segment
detection, point insertion, weak-zone resolution and position restoration.
[SeamPlacer.cpp](../../src/libslic3r/GCode/SeamPlacer.cpp) integrates it into
candidate gathering and issues the warning.
- [Model.hpp](../../src/libslic3r/Model.hpp) defines the types and their order,
[PrintApply.cpp](../../src/libslic3r/PrintApply.cpp) handles invalidation, and
[PrintObjectSlice.cpp](../../src/libslic3r/PrintObjectSlice.cpp) slices the
modifiers. [bbs_3mf.cpp](../../src/libslic3r/Format/bbs_3mf.cpp) and
[3mf.cpp](../../src/libslic3r/Format/3mf.cpp) store them.
- [GUI_Factories.cpp](../../src/slic3r/GUI/GUI_Factories.cpp) and
[GUI_ObjectList.cpp](../../src/slic3r/GUI/GUI_ObjectList.cpp) provide the menus,
type changes and ordering.
- [Precise Seam tests](../../tests/fff_print/test_precise_seam.cpp) cover the
strong positions, including a midpoint on an existing vertex or the closing
edge. They also cover shared and coincident weak boundaries, every warning,
and the priority order.
- [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) cover
enforced-patch selection independent of the contour start, fully painted
contours, duplicate removal, and `Print::apply()` synchronization through
type changes and restored model snapshots.
- [3MF tests](../../tests/libslic3r/test_precise_seam_3mf.cpp) cover the round
trip of every mode and of inactive settings, attribute escaping, and which
metadata combinations restore a seam mode.
[Plugin tests](../../tests/slic3rutils/test_precise_seam_plugin.cpp) cover the
Python bindings.
+24 -54
View File
@@ -28,8 +28,8 @@ Per-vendor granularity is what makes the system practical:
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd - A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
vendors keep theirs — even when the bumped vendor is the shared Orca filament vendors keep theirs — even when the bumped vendor is the shared Orca filament
library everyone else inherits from. library everyone else inherits from.
- The setup wizard loads its vendors through the same routine as startup, so it - The setup wizard, which loads vendors one at a time, gets the same speedup as
gets the same speedup without a second code path. startup without a second code path.
- A vendor with no cache, or a broken one, costs only that vendor a parse. - A vendor with no cache, or a broken one, costs only that vendor a parse.
A cache holds *system* presets only. User presets, project settings and modified A cache holds *system* presets only. User presets, project settings and modified
@@ -76,10 +76,9 @@ and the count of errors the original parse hit.
Each entry is one preset **in source form**: what its JSON sub-file states and nothing Each entry is one preset **in source form**: what its JSON sub-file states and nothing
that resolving it derives — the preset's own config diff, the name of the preset it that resolving it derives — the preset's own config diff, the name of the preset it
inherits, the names of the presets it includes, and the parse metadata (name, sub-path, inherits, and the parse metadata (name, sub-path, description, instantiation, setting
description, instantiation, setting and filament ids, renames). Non-instantiated base and filament ids, renames). Non-instantiated base presets are stored too; the children
presets are stored too; the children that inherit from or include them cannot resolve that inherit from them cannot resolve without them.
without them.
**The payload names its own keys.** The dictionary holds the distinct `opt_key`s the **The payload names its own keys.** The dictionary holds the distinct `opt_key`s the
file uses, the `ConfigOptionType` each was written as, and the distinct enum *value file uses, the `ConfigOptionType` each was written as, and the distinct enum *value
@@ -162,20 +161,15 @@ cache nothing can invalidate is worse than no cache.
Vendors load in a fixed order, because filament inheritance crosses exactly one Vendors load in a fixed order, because filament inheritance crosses exactly one
boundary: any vendor's filament may inherit from the shared Orca filament library, boundary: any vendor's filament may inherit from the shared Orca filament library,
and nothing else reaches across vendors — an `include` is always vendor-local. Only and nothing else reaches across vendors. The library therefore goes first, alone;
installing a vendor's presets crosses it; reading the vendor, from its cache or its every other vendor follows in parallel, resolving against it; and the results are
JSONs, needs nothing from the library. So every other vendor is read while the library
loads, each is installed against it as soon as both are done, and the results are
merged in a stable order: merged in a stable order:
```mermaid ```mermaid
flowchart LR flowchart LR
lib["1 · OrcaFilamentLibrary loaded;<br/>meanwhile every other vendor read<br/>from its cache or its JSONs"] --> par["2 · every other vendor installed<br/>in parallel, each into its own bundle,<br/>filaments resolving against the library"] --> merge["3 · bundles merged into one,<br/>in one pass per collection,<br/>in stable vendor order"] lib["1 · OrcaFilamentLibrary<br/>loaded first, synchronously"] --> par["2 · every other vendor in parallel,<br/>each into its own bundle, filaments<br/>resolving against the loaded library"] --> merge["3 · bundles merged into one,<br/>sequentially, in stable vendor order"]
``` ```
`PresetBundle::load_vendors` runs these steps for startup and for the setup wizard,
which hand it the vendors to load and the directory each is installed in.
Whether a vendor comes from its cache or from a parse changes nothing in that Whether a vendor comes from its cache or from a parse changes nothing in that
order — both produce the same bundle, so cached and parsed vendors mix freely in order — both produce the same bundle, so cached and parsed vendors mix freely in
one startup. one startup.
@@ -216,28 +210,14 @@ shipped cache answered first, so the profile in `<data_dir>/system/` was never p
and its cache was never written back. and its cache was never written back.
Serving from a cache is not a memory-image restore. The entries are deserialized and Serving from a cache is not a memory-image restore. The entries are deserialized and
then installed by `install_vendor`, the routine the JSON path hands the vendor's entries then installed one by one — inheritance resolved against the presets installed before
to once it has parsed the sub-files: inheritance resolved against the presets installed them and the currently loaded filament library, configs flattened onto the collection
before them and the currently loaded filament library, includes layered in, configs defaults, validated and registered — by the same function the JSON path calls straight
flattened onto the collection defaults, validated and registered. An `include` layers after parsing a sub-file. The two paths share everything below the parse, which is what
what the included base states, between the parent and the preset's own keys: the base's makes a cache-loaded bundle indistinguishable from a JSON-loaded one by construction
diff against the rather than by test coverage. Installation also rebuilds each preset's file path from
default, taken when the base itself was installed and before the per-variant padding the local data directory, so a shipped cache never carries the generating machine's
`inherits` sees, so only what a template sets reaches the presets including it. The two paths.
paths share everything below the parse, which is what makes a cache-loaded bundle
indistinguishable from a JSON-loaded one by construction rather than by test coverage.
Installation also rebuilds each preset's file path from the local data directory, so a
shipped cache never carries the generating machine's paths.
Installing an entry is split in two. `resolve_vendor_preset` flattens it, reading only
what is registered under the names it inherits and includes, and `commit_vendor_preset`
registers it, the only step that writes anything shared. Entries resolve across threads
in runs and commit in the order the vendor lists them. A run ends before an entry that
inherits or includes one already in it, since that one's commit registers what the
entry resolves against, so no entry in a run reads what another in it registers. An
entry's parse messages are held until it commits. The bundle, the log's parse and
install messages and the error count therefore come out as parsing and installing one
entry at a time would leave them, whatever the listing order.
App upgrades work because a cache normally survives one. Only a deliberate App upgrades work because a cache normally survives one. Only a deliberate
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at `CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
@@ -270,20 +250,17 @@ the wizard caches the *derived JSON*, not another form of the inputs:
open, the wizard computes the current stamps (one version peek per vendor) and, when open, the wizard computes the current stamps (one version peek per vendor) and, when
they match, serves the catalog from the file — no bundle built, no preset installed. they match, serves the catalog from the file — no bundle built, no preset installed.
Caching bundle inputs instead was tried and measured: rebuilding the bundle from Caching bundle inputs instead was tried and measured: rebuilding the bundle from
per-vendor caches costs over a second of preset installation whatever feeds it, so per-vendor caches costs ~2 s of preset installation whatever feeds it, so only
only skipping the rebuild entirely wins. skipping the rebuild entirely wins.
Any change to the set — a vendor added, removed or updated, or its cache-only Any change to the set — a vendor added, removed or updated, or its cache-only
`.opc` replaced by a newer one — changes the stamps and retires the whole file; `.opc` replaced by a newer one — changes the stamps and retires the whole file;
the wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where
startup uses (per-vendor caches serving where they cover), and writes the catalog they cover) and writes the catalog back. Selections, region and per-open decorations
back. When a vendor fails to load, the filament library included, that open falls are applied downstream of the cache either way, so a served catalog is
back to the wizard's own scan of the vendor JSONs, as when no bundle can be built, indistinguishable from a rebuilt one. Nothing ships this file and the updater never
and writes nothing. Selections, region and per-open decorations are applied touches it; it is a locally written artifact, re-derived whenever stale, written
downstream of the cache either way, so a served catalog is indistinguishable from a through a temp file and rename so half a cache is never readable.
rebuilt one. Nothing ships this file and the updater never touches it; it is a
locally written artifact, re-derived whenever stale, written through a temp file and
rename so half a cache is never readable.
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor
@@ -397,13 +374,6 @@ enumerates only `*.json` will find no vendors at all in a packaged build.
the `CachedPreset` field list — written and read by `visit_entry` in the `CachedPreset` field list — written and read by `visit_entry` in
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or `PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand. the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
- **Adding a kind of reference between presets**, as `inherits` and `include` are:
parse the names into `CachedPreset` (a field change, so `CACHE_VERSION` is bumped),
have `install_vendor_entries` end a run before an entry that names one already in it
and retain what the names point at, look them up only in `resolve_vendor_preset`, and
register what they point at only in `commit_vendor_preset`. The listing-order test in
`test_vendor_cache.cpp` fails for a kind the runs do not check once its fixture uses
it.
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most - **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
65535 options and one cache at most 65535 distinct enum value names. 65535 options and one cache at most 65535 distinct enum value names.
`CacheDictionary::save` throws past that, which surfaces when CI generates the `CacheDictionary::save` throws past that, which surfaces when CI generates the
-197
View File
@@ -1,197 +0,0 @@
# Prime tower sparse layers — High Level Design
## Purpose and scope
A prime tower exists to absorb filament changes, but it is planned on every
object layer below the topmost change, not only on the layers that purge. The
layers in between carry no filament change and print nothing but a block of the
tower's own footprint to keep its top level. They are called sparse layers, and
on a print with few changes they are most of the tower: they cost time, filament
and a travel to the tower on every layer.
Two settings trade that cost against something else. `wipe_tower_no_sparse_layers`
drops them, which sinks the tower below the model. `wipe_tower_sparse_layers_combination`
merges runs of them into fewer, thicker layers, which keeps the tower level with
the model. Both are off by default, and with both off the tower prints one layer
per object layer as it always has.
The decisions belong to tower planning and G-code emission. They do not change
sliced object geometry, but they do change the emitted G-code, the filament and
time estimates, and — for the compacted case — whether a plate is printable at
all. Changing either setting invalidates the tower step.
## What a sparse layer is
`ToolOrdering::fill_wipe_tower_partitions` counts the filament changes per layer
and propagates that count downwards, so every layer below the topmost change is
marked as carrying a tower. It then fills any gap between two tower layers, so
the tower is continuous from the bed to its last purge. `wipe_tower_layer_height`
is the distance from the previous tower layer, which is the object's layer height
whenever the tower prints on every layer.
`Print::_make_wipe_tower` plans one tower layer per such object layer. A layer
whose only call keeps the current filament leaves no toolchange in the plan, and
the layer it generates is a single result whose initial and new tool are equal.
That is what `wipe_tower_layer_is_sparse` recognises, and it is the unit both
settings work on.
The plan stays one entry per tower layer in every case. The G-code emitter walks
`WipeTowerData::tool_changes` by layer index, advancing once per object layer
that carries a tower, so a planner that removed entries would silently shift
every later layer onto the wrong tower geometry. Layers that print nothing are
therefore still planned and still generated; they are marked, and the emitter
drops them.
## Shared rules
Tower planning, G-code emission and the plate validation all have to agree about
which layers print and where. They ask one set of free functions, declared beside
the tower classes, rather than each re-deriving the answer from the raw options:
- `wipe_tower_sparse_layers_skipped` — whether sparse layers are really dropped.
Smooth timelapse and clumping detection park the nozzle on the tower every
layer, so with either of them on no layer is ever dropped and the option reads
as off everywhere.
- `wipe_tower_sparse_layers_combined` — whether runs are really merged. The same
two rule it out, and so does `wipe_tower_no_sparse_layers`: dropping the layers
outright is the stronger answer to the same problem, so the two settings are
exclusive and the GUI greys out the second while the first is on.
- `wipe_tower_layer_is_sparse`, `wipe_tower_layer_is_combined_away` — per-layer
questions the emitter asks about generated results.
- `compute_compacted_wipe_tower_z` — the tower's print z per planned layer when
it is compacted.
- `combine_sparse_wipe_tower_layers` and its `combine_sparse_wipe_tower_plan`
wrapper — the merge rule, applied to either generator's plan.
Both tower generators are driven through these. `WipeTower` (Type 1, the block
tower) and `WipeTower2` (Type 2, the default) keep separate plans with the same
per-layer shape — print z, layer height, toolchanges, and a `combined_away` flag
— so one template covers both.
## Dropping sparse layers
With `wipe_tower_no_sparse_layers`, the tower only grows on layers that carry a
real change. It therefore falls one layer height behind the object for every
sparse layer, and by the top of a tall print it can sit far below the model. The
nozzle has to reach down to it at each purge.
`compute_compacted_wipe_tower_z` derives that z once, from the generated results,
so the emitter and the validator cannot disagree. Emission descends to it, but
only once the nozzle is parked over the tower: descending while still over the
model would drive the nozzle into the print, so a descent that would do that is
deferred until after the travel to the tower. Extrusions emitted without an
explicit z — the nozzle-change wipe in particular — are pulled down to the
compacted z for the same reason.
Reaching down is only safe if nothing tall stands near the tower. `Print.hpp`
carries the clearance rule: a keep-out zone grown from the tower's footprint by
the spiral z-hop envelope, and a per-object limit on how high an object may rise
near it, tiered by the nozzle cone, the head body, the rod and the lid. The same
rule serves the precise check on real extrusions, the pre-slice estimate that
feeds the plater, and the outlines the plater draws while an object is dragged,
so that the ring the user sees touches the object's outline exactly when the
check trips.
## Merging sparse layers
With `wipe_tower_sparse_layers_combination`, no layer is dropped and nothing is
compacted: the tower keeps following the object, and the nozzle never descends.
Instead a run of consecutive sparse layers prints once, on the run's last layer,
at the accumulated height of everything it covers — the same way infill
combination merges sparse infill. The layers below it in the run print nothing.
`combine_sparse_wipe_tower_plan` runs before the tower's depths are planned,
because the heights it rewrites feed the extrusion flow of every later pass. It
raises `height` in place on the layer that prints a run and sets `combined_away`
on the rest; generation then proceeds unchanged, and the flag is copied onto the
results so the emitter can drop them.
Four constraints shape the rule:
- **Whole layers only.** A tower layer is entered at the object's z, so a merged
layer has to end on an object layer boundary. The merged height is therefore a
sum of whole layer heights, never a clamped value.
- **The nozzle's maximum layer height.** A run stops growing as soon as one more
layer would pass `max_layer_height` for the nozzle printing it — three quarters
of the nozzle diameter when that is left at 0, as elsewhere in slicing. The cap
is read through the filament-to-nozzle map, since `max_layer_height` is per
nozzle while the tower indexes filaments. This is what makes the setting inert
at common layer heights: two 0.2 mm layers are 0.4 mm and do not fit under a
0.3 mm maximum, so nothing merges until the layer height is 0.15 mm or below,
or the maximum is raised.
- **A filament change purges at its own z.** A layer with a real change can
neither be merged away nor absorb the run below it, so a run always ends on its
own last sparse layer and the change above it is untouched.
- **The first layer stays on the bed.** It carries the brim and is never merged.
A run holds one filament throughout — that is what makes it sparse — so the cap
is uniform across it, and the tower reserves depth only for the purges above a
layer, so a run has one footprint and the merged layer covers exactly the area
the layers it replaces would have.
## Emission and accounting
`WipeTowerIntegration` drops a layer whose results are marked, for both settings,
through the same `ignore_sparse` path in `tool_change` and
`is_empty_wipe_tower_gcode`. A dropped layer emits no travel to the tower and no
extrusion.
Filament used is accumulated by the generators while they write, so a layer that
will be dropped must not be charged. Type 1 asks `layer_is_printed` at each of
its accumulation points; Type 2 guards the equivalent block in `finish_layer`,
which also stops a merged-away layer from adding height of its own — the layer
that prints the run carries all of it.
A merged layer is the only case where the tower's layer height differs from the
object layer it sits on, and therefore the only case where the height the
exporter already emitted for that layer is wrong for the tower. Both generators
do declare a height, but each hardcodes a tag dialect — the block tower forces
the BBL tag, the other writes the compatible one — while the G-code processor
reads only the tag its printer uses. On a non-BBL printer with a Type 1 tower the
declaration is dropped, and the merged layer is drawn and costed as a thin one.
`WipeTowerIntegration::tower_height_tag` therefore declares it at export time,
where the printer is known, and only when the tower's own G-code does not already
carry the tag that will be read. The object's height returns on the next object
path, because emission forces the processor role to the tower on any layer that
carries one.
## Constraints
A layer that prints nothing prints nothing at all, including any interface work
the tower planner scheduled there. The Type 1 block planner marks a layer as a
contact layer when a filament category stops or starts being used relative to the
layer below, and a sparse layer immediately above a change qualifies. Merging a
run, like dropping its layers, replaces that interface with the run's single
layer. Both settings are off by default for this among other reasons.
Neither setting changes what the tower is for. A plate that needs a tower on
every layer — smooth timelapse, clumping detection — gets one, and the settings
read as off rather than compacting or merging in one place and not another.
## Implementation and verification
- [WipeTower.hpp](../../src/libslic3r/GCode/WipeTower.hpp) declares the shared
rules and the plan-merging template;
[WipeTower.cpp](../../src/libslic3r/GCode/WipeTower.cpp) implements them and
the Type 1 tower, [WipeTower2.cpp](../../src/libslic3r/GCode/WipeTower2.cpp)
the Type 2 tower.
- [ToolOrdering.cpp](../../src/libslic3r/GCode/ToolOrdering.cpp) decides which
layers carry a tower at all, and
[Print.cpp](../../src/libslic3r/Print.cpp) plans it and runs the clearance
check whose rule lives in [Print.hpp](../../src/libslic3r/Print.hpp).
- [GCode.cpp](../../src/libslic3r/GCode.cpp) emits the tower, drops the layers
that print nothing, and declares a merged layer's height;
[PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines the settings and
[ConfigManipulation.cpp](../../src/slic3r/GUI/ConfigManipulation.cpp) their
mutual exclusion.
- [GLCanvas3D.cpp](../../src/slic3r/GUI/GLCanvas3D.cpp) and
[PartPlate.cpp](../../src/slic3r/GUI/PartPlate.cpp) draw the compacted tower's
keep-out outlines live while the user drags.
- [Rule tests](../../tests/libslic3r/test_wipe_tower.cpp) cover the gating of
both settings, the per-layer predicates, the compacted z, the merge rule's run
flushing, height conservation, the nozzle cap and the first-layer exemption,
and the clearance geometry the plater draws.
- [Slicing tests](../../tests/fff_print/test_wipe_tower.cpp) slice a real print
and check that a run folds, that the tower still covers the object exactly
once, that a run too thin for the cap is left alone, and that a merged layer
declares its height in the tag the printer's processor reads.
-240
View File
@@ -1,240 +0,0 @@
# Printer agents
Printer agents isolate printer-specific communication from the rest of OrcaSlicer. The GUI and
`DeviceManager` operate on a shared set of printer operations and device state; a selected printer
agent implements those operations for a particular printer ecosystem. The agent boundary allows
Bambu, Moonraker-based printers, built-in integrations, and Python-provided integrations to use the
same application workflow without making the GUI understand every printer protocol.
The current boundary is an adapter boundary around the existing application contract. In particular,
some request fields and message payloads still use the Bambu-shaped representation that existing
`MachineObject` and `DeviceManager` code consumes. The printer agent is responsible for translating
that representation into the protocol spoken by its printer. This is an intentional compatibility
constraint of the current design; the interface is not yet a neutral printer protocol.
The v1 dialect migration path is deliberately narrow. `DeviceManager` currently speaks the Bambu JSON
dialect because that is the payload shape already used throughout the command and state workflow. The
v1 `OrcaPrinterAgent` also accepts that Bambu dialect. Its transport path places the small translation
needed for the target printer at `deliver_to_sink`, keeping the compatibility code at the edge rather
than spreading it through `DeviceManager` or the agent interface.
The eventual direction is for `DeviceManager` to produce an Orca JSON dialect. The Bambu agent will then
own the translation from Orca JSON to Bambu's protocol, while `OrcaPrinterAgent` can forward the Orca
payload directly to its sink. The v1 translation at `deliver_to_sink` can then be removed without
changing `DeviceManager`, the command callers, or the rest of the agent workflow.
## Components
The system has four relevant layers:
```text
GUI / DeviceManager / MachineObject
|
NetworkAgent
/ \
IPrinterAgent ICloudServiceAgent
| |
printer protocol authentication and cloud services
```
### `DeviceManager` and `MachineObject`
`DeviceManager` owns the application-facing printer workflow. It maintains `MachineObject` instances,
updates their state, filters devices for the active printer agent, and initiates operations such as
homing, temperature changes, printing, subscriptions, and camera playback.
`MachineObject` remains the shared state model used by the GUI. It does not contain the implementation
of a printer protocol. When a device is discovered or returned by a cloud query, the device is tagged
with the active `printer_agent_id`. Device lists and selected-machine operations use that tag to avoid
sending an operation through an agent that does not own the device.
### `NetworkAgent`
`NetworkAgent` is the façade used by the GUI and `DeviceManager`. It owns:
- the currently selected `IPrinterAgent`;
- the registered cloud-service instances, indexed by provider;
- callbacks shared by the active printer agent and the application;
- the forwarding methods for printer commands and cloud operations.
There is one active printer agent for the currently selected printer preset. Switching the preset
increments the machine-list generation, disconnects the old printer agent, removes its callbacks, and
installs the newly selected agent. The façade then forwards printer operations to that agent.
Cloud operations are selected separately using a provider key. `NetworkAgent` forwards a cloud request
to the matching `ICloudServiceAgent`, and forwards cloud camera operations with a device ID. The
printer agent receives a cloud-agent pointer through `set_cloud_agent()` when it is created, allowing
printer communication to obtain cloud tokens without depending on a concrete cloud implementation.
### `IPrinterAgent`
`IPrinterAgent` is the printer-facing contract. It covers:
- cloud-relay and direct-LAN message delivery;
- LAN connection, discovery, binding, and certificates;
- printer subscriptions and callbacks;
- print operations;
- filament synchronization;
- camera capability and local camera URL reporting;
- printer command methods.
Concrete built-in implementations include the Bambu wrapper, the native Orca/Moonraker path, and
other printer-agent implementations registered by the application. A printer agent may use either
the cloud agent, a direct LAN connection, or both.
### `ICloudServiceAgent`
`ICloudServiceAgent` owns authentication and services provided by a cloud backend. It covers login
state, tokens, user and printer lists, settings synchronization, model services, cloud messages, and
cloud camera operations.
Cloud camera operations are device-scoped:
- `get_camera_url(dev_id, callback)` obtains a stream URL for one device;
- `create_camera_signaling_channel(dev_id)` creates signaling for one device where the provider
supports it.
This is separate from the local camera URL exposed by `IPrinterAgent`, which is currently scoped to
the active printer agent because a normal LAN agent represents one physical printer connection.
## Agent registration and selection
`NetworkAgentFactory` maintains the printer-agent registry. Each registry entry contains an agent ID,
a display name, and a factory function. Built-in agents register during application initialization.
Python printer-agent capabilities register dynamically and contribute an agent ID and factory entry.
The selected printer preset contains the printer-agent choice. If no explicit choice is stored, the
application preserves the existing default behavior: Bambu presets select the Bambu agent and other
presets select the native Orca agent. When a preset is changed, `GUI_App` resolves the effective agent
ID, obtains the corresponding cloud agent, creates the printer agent through the registry, and installs
it in `NetworkAgent`.
The registry rejects conflicting agent IDs. This matters for Python plugins because an agent ID is the
stable identity used by presets and device ownership; two enabled plugin capabilities must not claim
the same ID.
## Message and command flow
There are two low-level message paths:
- `send_message()` publishes a command through the printer's cloud relay;
- `send_message_to_printer()` sends a command directly to the printer over the LAN path.
Both paths accept a JSON string, quality-of-service and flag values, and return the existing network
status code domain. The agent owns the conversion from that JSON contract to its native transport.
The typed `command_*` methods are the application-facing convenience layer. The five generic defaults
currently implemented by `IPrinterAgent` construct the existing JSON dialect and route through the
same message path:
| Method | Default operation |
| --- | --- |
| `command_xyz_abs()` | Send `G90` for absolute positioning |
| `command_auto_leveling()` | Send `G29` for bed leveling |
| `command_go_home()` | Use the supported homing operation or send `G28` |
| `command_set_bed()` | Use the supported bed control or send `M140` |
| `command_set_nozzle()` | Send `M104` for nozzle temperature |
These are compatibility defaults for common printer workflows, not a guarantee that every firmware
implements every command identically. An agent can override a method when its protocol needs another
operation. For example, a Klipper configuration may use `BED_MESH_CALIBRATE` instead of `G29`.
The remaining common command methods default to `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` because their
existing behavior is vendor-specific or has no portable implementation:
- AMS RFID refresh;
- AMS calibration;
- AMS tray selection;
- camera start;
- axis control.
The methods remain on the common interface so an agent that supports them can override them explicitly.
`sequence_id` remains part of the command contract because `DeviceManager` creates and tracks it as
the command ID.
## Device ownership and stale responses
Printer-agent ownership is represented by `printer_agent_id` on device records and `MachineObject`
instances. The active agent ID is attached when a device is discovered, returned by a cloud list, or
reused after a preset switch. Local-machine configuration also persists the agent ID so a saved LAN
device is not silently reused by an unrelated agent.
Cloud printer-list responses carry three pieces of request context added by `NetworkAgent`:
```text
provider cloud provider used for the request
agent_id active printer agent when the request was made
generation machine-list generation when the request was made
```
`DeviceManager` accepts the response only when those values still match the current provider, active
agent, and generation. This prevents a slow response from the previous preset or provider from
repopulating the current device list.
The provider mapping is currently selected by `GUI_App`: the Bambu agent maps to the Bambu cloud
provider and other agents map to the Orca cloud provider. The generation check protects that existing
selection from races; it does not make cloud-provider ownership intrinsic to an agent. Cloud-printer
ownership and the broader Orca cloud services are therefore still separate architectural concerns.
## Python printer agents
`PrinterAgentPluginCapability` implements `IPrinterAgent` directly. The live capability object is
registered with `NetworkAgentFactory` and handed out as the printer agent when its agent ID is selected.
The plugin receives the selected `ICloudServiceAgent` through `set_cloud_agent()` just like a built-in
printer agent.
Python plugins must implement the core communication and lifecycle methods required by the interface,
including agent metadata, printer connection, discovery callbacks, and the two message-send methods.
Methods that are meaningful only to a particular printer are optional overrides where the C++ base
class provides a default.
All ten `command_*` methods are available in the Python binding and in the trampoline. Their override
status is intentionally optional:
- the five generic commands use the C++ default when Python does not override them;
- the five vendor-specific commands return `NOT_SUPPORTED` unless Python supplies an implementation;
- a Python implementation can replace either behavior for its own protocol.
The Python camera binding exposes HTTP, HTTPS, RTSP, and HTTP-snapshot modes. WebRTC remains a
built-in C++ camera mode, but is not exposed as a Python mode because the current Python capability
does not provide the corresponding cloud signaling-channel contract.
## Camera playback boundary
The camera stream mode describes how a stream is obtained; it does not by itself define ownership of
the wxWidgets view that renders it. `MediaPlayCtrl` selects and tears down the active backend, while
the wx parent owns the child window or renderer. This is important because a web view, native media
control, and frame-based/WebRTC renderer have different wx window-lifetime requirements.
Cloud URL and signaling requests are routed through `NetworkAgent` to the cloud provider selected for
the device. Local URL requests are routed to the active printer agent. The distinction keeps cloud
account services device-scoped while preserving the current one-LAN-agent/one-printer model.
## Compatibility constraints
The printer-agent boundary intentionally preserves several existing application contracts:
- Bambu-shaped JSON is still the shared command representation;
- existing network status codes are reused, with Orca-specific unsupported/capability errors added
in the Orca-reserved range;
- `MachineObject` remains the shared device-state model;
- preset and local-machine data retain compatibility with the existing agent-selection behavior;
- Python plugins use the existing capability and pybind11 registration system.
The agent abstraction is therefore responsible for containing vendor differences, not for pretending
that all vendor protocols are identical. The planned Orca JSON dialect is the protocol-neutral command
model for the `DeviceManager`/agent boundary. Once it is introduced, Bambu-specific translation remains
inside the Bambu agent and the Orca agent's v1 sink adapter can be removed as a self-contained cleanup.
## Main implementation locations
- [`IPrinterAgent`](../../src/slic3r/Utils/IPrinterAgent.hpp) — printer-agent contract and generic command defaults
- [`ICloudServiceAgent`](../../src/slic3r/Utils/ICloudServiceAgent.hpp) — cloud service and per-device
cloud camera contract
- [`NetworkAgent`](../../src/slic3r/Utils/NetworkAgent.hpp) — façade and dispatch between active agents
- [`NetworkAgentFactory`](../../src/slic3r/Utils/NetworkAgentFactory.hpp) — built-in and Python agent registry
- [`DeviceManager`](../../src/slic3r/GUI/DeviceCore/DevManager.cpp) — device ownership, filtering, and
stale-response checks
- [`PrinterAgentPluginCapability`](../../src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp)
— Python bindings
- [`MediaPlayCtrl`](../../src/slic3r/GUI/MediaPlayCtrl.cpp) — camera backend selection and playback lifecycle
-170
View File
@@ -1,170 +0,0 @@
# Wipe inward — High Level Design
## Purpose and scope
Wipe inward reduces reheating of fresh plastic and visible seam artifacts by
moving the hot nozzle toward adjacent printed material during the external-wall
wipe. Wipe marks are especially visible at layer heights below 0.1 mm.
The option applies only to wipes after external walls, including walls around
holes. It does not offset wipes after inner walls, infill or supports. For an
outer contour the move is inward; for a hole it is away from the hole, toward
the surrounding material. The path must remain supported by material that is
already present when the wipe executes.
The operation belongs to G-code generation. It uses extrusion paths, their actual
widths and their print order. Changing its settings invalidates G-code export
while preserving the sliced geometry.
## Settings and eligibility
`wipe_inward` defaults to disabled and requires Wipe while retracting to be
enabled for the active filament. `wipe_inward_distance` defaults to 50% of the
actual external-wall extrusion width; it also accepts an absolute distance in
millimeters. Using the path width makes Auto width and Arachne's variable widths
meaningful. The effective offset is limited by that width and the spacing to the
adjacent wall. A zero distance disables the offset.
Only external perimeters with a suitable, previously printed inner perimeter
are eligible. A configured wall count alone cannot establish eligibility:
the local geometry may contain fewer walls, and walls scheduled later do not
provide support. Outer/Inner wall order therefore normally retains the regular
wipe path.
Retraction and pressure advance calibrations disable inward wiping so it cannot
mask the behavior being measured. The calibration settings turn it off, and
G-code generation enforces this even if a profile or object override enables it.
## Path selection and support
The planner identifies an adjacent inner perimeter on the material side of the
outgoing wall. Contour winding and the distinction between outer contours and
holes establish a preferred direction; local printed geometry resolves ambiguous
or self-touching contours.
Candidate paths offset or translate the portion needed for the configured wipe
distance. A wide seam gap can prevent a supported forward path; following the
incoming printed wall backwards is also a candidate. If translating that wall
cannot provide a complete wipe around a curve, the planner tries an offset of
the reversed wall. Direction checks allow coordinate-rounding error at a
perpendicular entry, while rejecting actual backtracking. The planner checks the
complete executable path, including its connector from the nozzle position,
against the current and earlier printed perimeters. Nearby endpoints alone do
not establish support across a gap.
Each region accumulates its printed perimeter prefix once, in extrusion order.
Every entity contributes its geometry only after it is printed, and the prefix
is discarded when the region ends. This collection is skipped when inward wiping
is disabled or its configured distance is zero. A mixed inner-wall loop remains
an eligible target even when its first path is an overhang: ordinary inner-wall
paths elsewhere in the loop identify it. Likewise, an external loop with an
overhanging start remains eligible when other segments identify the external
wall. It is available for support checks but is not an inner-wall target.
Candidate-specific support filtering and AABB trees are built only for eligible
external loops, then reused across their candidate paths.
Material-side validation applies with or without a seam gap. Along each
candidate, local wall normals point toward the adjacent printed inner wall;
samples on the opposite side are rejected even when they remain close enough
to the external wall to pass the support check. This uses the open wall geometry
without treating it as a closed polygon. Full paths at a zero-gap seam also
retain clearance from the external wall after their initial connector. At a
clipped corner, another branch can be closer than the requested offset, so
material-side and support checks apply without that additional clearance rule.
An accepted candidate replaces the stored wipe path as a whole. A short direct
inward move is also eligible when longer candidates fail validation. It may
waive full wall clearance, but must pass the material-side check. Its initial
direction is checked from the actual nozzle position after any loop pre-move;
the original wall endpoint is retained separately for intersection checks. It takes
priority over the alternate offset when the preferred and translated paths
are unusable. A longer reversed path may replace the selected candidate only
when its distance to the target inner wall is no worse within tolerance.
## Fallback to the regular wipe
The original wipe path is retained when:
- No suitable adjacent inner wall has already been printed near the seam. This
includes single-wall areas, locally missing inner walls and normally Outer/Inner
wall order. A distant wall or a wall on the air side does not qualify.
- The requested or available offset, or the configured wipe distance, is zero
or too small at the geometry's coordinate precision.
- Degenerate geometry prevents construction of a usable candidate, or all
candidates fail the checks for printed support, direction, wall clearance or
the connector from the actual nozzle position. This can occur at tight corners,
narrow features or seam gaps.
Corners and seam gaps do not automatically trigger fallback: an offset,
translated, reversed or short direct inward path may still be valid. The regular
wipe is retained only when no candidate is accepted.
Fallback uses the path and retraction rules for `wipe_inward` disabled.
Wipe while retracting must still be enabled for a wipe to occur; `wipe_on_loops`
remains controlled by its own setting.
## Interaction with Wipe on loop
`wipe_on_loops` is an independent option that makes a short move before leaving
an external loop. It can operate with `wipe_inward` disabled. When both options
are enabled, its destination is the starting position for the inward wipe.
The loop move samples the outgoing and incoming paths by distance across path
boundaries. The sampling distance is bounded by the nozzle diameter and one
quarter of the total path length. It samples the outgoing path at up to 20% of
the nozzle diameter and rotates that point around the seam through one third
of the material-side corner angle. For a closed square outer contour, this
produces a move of 20% of the nozzle diameter at 30 degrees into the corner.
Coincident samples or degenerate angles suppress the move.
The nozzle position stored by G-code generation must match the emitted loop
move. Both travel planning and wipe execution depend on this position, including
when Wipe inward is disabled.
With a seam gap, a loop move may advance past the inward offset's original entry.
If that alone makes the connector backtrack, the entry advances to the nozzle's
projection on the offset. The planner extends the source as needed to preserve
the configured wipe length and validates the new connector and complete path.
Joins that already backtrack across the seam gap are not adjusted this way.
## Execution and retraction
The stored wipe path uses a sentinel first point. Execution starts from the
actual nozzle position and proceeds to the second stored point. Path selection,
support validation and wipe-length calculation must all use this same executable
geometry, especially after a Wipe on loop move.
An accepted inward path executes at the end of the external loop, after any
Wipe on loop move, without retracting filament. It consumes the stored path and
updates the nozzle position before travel planning. A short travel to the next
wall cannot discard this wipe or force a retraction or Z-hop. Subsequent travel
uses the normal minimum-travel threshold and retraction/lift settings from the
new position. The regular wipe, including fallback, remains deferred until a
normal retraction uses it.
Retraction is divided into portions before, during and after wiping. The amount
that can be retracted during the wipe depends on its executable length, wipe
speed and the active filament's retraction speed. Fractional retraction speeds
are retained in this calculation. For a 2 mm wipe at 100 mm/s and a retraction
speed of 25.5 mm/s, the wipe can retract 0.51 mm. With a total retraction of 0.8 mm
and both before/after percentages set to zero, the remaining 0.29 mm is retracted
before wiping. This split applies to regular deferred wipes, including fallback;
an accepted inward wipe executes separately without retraction.
## Implementation and verification
- [GCode.cpp](../../src/libslic3r/GCode.cpp) integrates path selection, nozzle
position and retraction; [Print.cpp](../../src/libslic3r/Print.cpp) controls
invalidation, and [PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines
the settings.
- [WipePathHelpers](../../src/libslic3r/GCode/WipePathHelpers.hpp) implements path
sampling, offset selection and support checks.
- [Geometry tests](../../tests/libslic3r/test_wipe_path.cpp) cover support,
degenerate paths, contour and hole orientations, and exact loop-move geometry
across path subdivisions.
- [FFF tests](../../tests/fff_print/test_wipe.cpp) cover emitted trajectories,
fallback, minimum-travel retraction and Z-hop rules, and export invalidation.
With Wipe inward disabled, they check the loop move's direction and magnitude
for Classic and Arachne, the subsequent wipe's start and length, and fractional
retraction splitting in absolute and relative E modes.
Loop-move checks use reserved role/wipe markers and extrusion state, and run
with human-readable G-code comments both enabled and disabled.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-12
View File
@@ -125,7 +125,6 @@ src/slic3r/GUI/ThermalPreconditioningDialog.hpp
src/slic3r/GUI/Jobs/SLAImportJob.cpp src/slic3r/GUI/Jobs/SLAImportJob.cpp
src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp
src/slic3r/GUI/AboutDialog.cpp src/slic3r/GUI/AboutDialog.cpp
src/slic3r/GUI/ActionRegistry.cpp
src/slic3r/GUI/AMSMaterialsSetting.cpp src/slic3r/GUI/AMSMaterialsSetting.cpp
src/slic3r/GUI/ExtrusionCalibration.cpp src/slic3r/GUI/ExtrusionCalibration.cpp
src/slic3r/GUI/AmsMappingPopup.cpp src/slic3r/GUI/AmsMappingPopup.cpp
@@ -160,7 +159,6 @@ src/slic3r/GUI/SelectMachinePop.cpp
src/slic3r/GUI/StatusPanel.cpp src/slic3r/GUI/StatusPanel.cpp
src/slic3r/GUI/Monitor.cpp src/slic3r/GUI/Monitor.cpp
src/slic3r/GUI/MsgDialog.cpp src/slic3r/GUI/MsgDialog.cpp
src/slic3r/GUI/NativeCommands.cpp
src/slic3r/GUI/NotificationManager.hpp src/slic3r/GUI/NotificationManager.hpp
src/slic3r/GUI/NotificationManager.cpp src/slic3r/GUI/NotificationManager.cpp
src/slic3r/GUI/ObjectDataViewModel.cpp src/slic3r/GUI/ObjectDataViewModel.cpp
@@ -181,8 +179,6 @@ src/slic3r/GUI/PublishDialog.cpp
src/slic3r/GUI/PublishSettingsDialog.cpp src/slic3r/GUI/PublishSettingsDialog.cpp
src/slic3r/GUI/SavePresetDialog.cpp src/slic3r/GUI/SavePresetDialog.cpp
src/slic3r/GUI/Search.cpp src/slic3r/GUI/Search.cpp
src/slic3r/GUI/SettingsIndex.cpp
src/slic3r/GUI/SpeedDialDialog.cpp
src/slic3r/GUI/Selection.cpp src/slic3r/GUI/Selection.cpp
src/slic3r/GUI/SelectMachine.cpp src/slic3r/GUI/SelectMachine.cpp
src/slic3r/GUI/PrePrintChecker.cpp src/slic3r/GUI/PrePrintChecker.cpp
@@ -213,7 +209,6 @@ src/slic3r/Utils/Process.cpp
src/libslic3r/GCode.cpp src/libslic3r/GCode.cpp
src/libslic3r/GCodeWriter.cpp src/libslic3r/GCodeWriter.cpp
src/libslic3r/GCode/ToolOrdering.cpp src/libslic3r/GCode/ToolOrdering.cpp
src/libslic3r/GCode/SeamPlacer.cpp
src/libslic3r/ExtrusionEntity.cpp src/libslic3r/ExtrusionEntity.cpp
src/libslic3r/Flow.cpp src/libslic3r/Flow.cpp
src/libslic3r/Format/AMF.cpp src/libslic3r/Format/AMF.cpp
@@ -251,7 +246,6 @@ src/slic3r/GUI/TroubleshootDialog.cpp
src/slic3r/Utils/3DPrinterOS.cpp src/slic3r/Utils/3DPrinterOS.cpp
src/slic3r/Utils/AstroBox.cpp src/slic3r/Utils/AstroBox.cpp
src/slic3r/Utils/Duet.cpp src/slic3r/Utils/Duet.cpp
src/slic3r/Utils/UltiMaker.cpp
src/slic3r/Utils/FlashAir.cpp src/slic3r/Utils/FlashAir.cpp
src/slic3r/Utils/MKS.cpp src/slic3r/Utils/MKS.cpp
src/slic3r/Utils/Moonraker.cpp src/slic3r/Utils/Moonraker.cpp
@@ -297,9 +291,3 @@ src/slic3r/GUI/PrinterWebViewHandler.cpp
src/slic3r/GUI/AMSDryControl.cpp src/slic3r/GUI/AMSDryControl.cpp
src/slic3r/GUI/AMSDryControl.hpp src/slic3r/GUI/AMSDryControl.hpp
src/libslic3r/PresetBundle.cpp src/libslic3r/PresetBundle.cpp
src/slic3r/GUI/CAD/DesignPanel.cpp
src/slic3r/GUI/CAD/SketchInlineEditor.cpp
src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp
src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp
src/slic3r/GUI/KeyChord.cpp
src/slic3r/GUI/Shortcuts.cpp
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Belt temperature-tower asset generator (discrete-provini design).
A vertical temperature tower cannot be sliced on a belt printer, so lay a row of
DISCRETE provini (one per temperature) along the belt (designed Y) with a fixed
surface gap. Each provino is the chevron+arc unit (belt_temp_provino_unit.stl,
keel-first); its temperature is ENGRAVED upright into the 50 mm face — a raised
number would be an unsupported overhang on the belt. The C++ calib_temp belt branch
(Plater.cpp) injects one M104 per zone 70 layers INTO provino i:
print_z[i] = i * PITCH * cos(theta) + 70 * layer_height (theta = 45)
inside the body, not in the empty inter-provino gap (which has no sliced layers for
the event to attach to). PITCH below is the shared geometry contract with that code —
keep them in sync.
Generates one STL per filament temp range used by Temp_Calibration_Dlg.
"""
import numpy as np, trimesh, os
from matplotlib.textpath import TextPath
from matplotlib.font_manager import FontProperties
from shapely.geometry import Polygon as ShPoly
from shapely.ops import unary_union
HERE = os.path.dirname(os.path.abspath(__file__))
UNIT = os.path.join(HERE, 'belt_temp_provino_unit.stl') # single provino, keel-first
SURF_GAP = 25.0 # surface-to-surface gap between provini (mm) — user spec
TEXT_H = 9.0
TEXT_DEPTH = 0.8 # engraving depth (numbers are CUT into the face, not raised:
# a raised number is an unsupported Y-overhang on the belt)
TEXT_OVERSHOOT = 0.6 # extra height poking out of the face for a clean boolean cut
# Temperature ranges (start, end) per filament family, 5 C step. File name encodes them.
RANGES = [(230,190),(270,230),(250,230),(280,240),(240,210),(320,280)]
unit = trimesh.load(UNIT)
dY = unit.bounds[1,1] - unit.bounds[0,1]
PITCH = dY + SURF_GAP # designed-Y pitch == C++ contract constant
print(f"unit dY={dY:.2f} PITCH={PITCH:.3f} (C++ contract: print_z[i]=i*{PITCH:.3f}*cos45)")
# 50 mm face normal (0,-1,1)/sqrt2 ; UPRIGHT basis u=+X det(+1) (verified non-mirrored)
n = np.array([0,-1,1.])/np.sqrt(2)
u = np.array([1,0,0.]); v = np.array([0,1,1.])/np.sqrt(2)
R = np.column_stack([u,v,n])
fn = unit.face_normals; fc = unit.triangles_center; fa = unit.area_faces
sel = (fn@n) > 0.9
face_c = (fc[sel]*fa[sel,None]).sum(0)/fa[sel].sum()
def text_mesh(s):
tp = TextPath((0,0), s, size=TEXT_H, prop=FontProperties(family='DejaVu Sans'))
rings = [ShPoly(p) for p in tp.to_polygons() if len(p)>=3]
rings.sort(key=lambda r:r.area, reverse=True)
used=[False]*len(rings); parts=[]
for i,o in enumerate(rings):
if used[i]: continue
holes=[]
for j in range(i+1,len(rings)):
if not used[j] and o.contains(rings[j]): holes.append(rings[j].exterior.coords); used[j]=True
parts.append(ShPoly(o.exterior.coords,holes)); used[i]=True
poly = unary_union(parts)
geoms = list(poly.geoms) if poly.geom_type=='MultiPolygon' else [poly]
m = trimesh.util.concatenate([trimesh.creation.extrude_polygon(g,height=TEXT_DEPTH+TEXT_OVERSHOOT) for g in geoms])
c = m.bounds.mean(axis=0); m.apply_translation([-c[0],-c[1],0]); return m
for t_start, t_end in RANGES:
temps = list(range(t_start, t_end-1, -5))
parts=[]
for i,T in enumerate(temps):
c = unit.copy(); c.apply_translation([0, i*PITCH, 0])
t = text_mesh(str(T)); M=np.eye(4); M[:3,:3]=R; t.apply_transform(M)
# place the text spanning from TEXT_DEPTH inside the face to TEXT_OVERSHOOT outside,
# then CUT it out of the provino (engrave) — no raised material, no Y-overhang.
t.apply_translation(face_c - n*TEXT_DEPTH + np.array([0,i*PITCH,0]))
c = trimesh.boolean.difference([c, t], engine='manifold')
parts.append(c)
asset = trimesh.util.concatenate(parts)
out = os.path.join(HERE, f"belt_temp_tower_{t_start}_{t_end}.stl")
asset.export(out)
dims = np.round(asset.bounds[1]-asset.bounds[0],1)
wt = all(p.is_watertight for p in parts)
print(f" {t_start}->{t_end}: {len(temps)} zones bbox={dims} watertight={wt} -> {os.path.basename(out)}")
+1 -1
View File
@@ -75,7 +75,7 @@ documentation_link = https://www.orcaslicer.com/wiki/material_temperatures#print
[hint:Calibration] [hint:Calibration]
text = Calibration\nDid you know that calibrating your printer can do wonders? Check out our beloved calibration solution in OrcaSlicer. text = Calibration\nDid you know that calibrating your printer can do wonders? Check out our beloved calibration solution in OrcaSlicer.
documentation_link = https://www.orcaslicer.com/wiki/calibration_guide documentation_link = https://www.orcaslicer.com/wiki/calibration
[hint:Auxiliary fan] [hint:Auxiliary fan]
text = Auxiliary fan\nDid you know that OrcaSlicer supports Auxiliary part cooling fan? text = Auxiliary fan\nDid you know that OrcaSlicer supports Auxiliary part cooling fan?
-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M14.5,1.5v12a1,1,0,0,1-1,1H1.5a1,1,0,0,1-1-1V1.5a1,1,0,0,1,1-1h12a1,1,0,0,1,1,1Z" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polyline points="4,5 6.5,7.5 4,10" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><line x1="8" y1="10" x2="11" y2="10" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

Before

Width:  |  Height:  |  Size: 524 B

-14
View File
@@ -1,14 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_23434_47713)">
<circle cx="9.32187" cy="4.2125" r="0.9" fill="#009688"/>
<circle cx="3.65625" cy="8.75" r="0.75" fill="#009688"/>
<path d="M8 0.5C12.0195 0.5 15.2656 3.35278 15.4824 6.74512L15.4932 7.0752C15.4914 9.23822 13.7134 11.0155 11.5498 11.0156H9.95312C9.71275 11.0126 9.4739 11.0574 9.25098 11.1475C9.0257 11.2386 8.82126 11.3741 8.64941 11.5459C8.47744 11.7179 8.34113 11.9229 8.25 12.1484C8.1817 12.3176 8.13909 12.4957 8.12402 12.6768L8.11816 12.8496C8.11817 13.3362 8.27448 13.7495 8.59473 14.0801V14.0811C8.73012 14.2338 8.81831 14.4375 8.81836 14.6494C8.81836 15.1395 8.45218 15.5 8 15.5C3.87614 15.5 0.5 12.1239 0.5 8C0.5 3.87614 3.87614 0.5 8 0.5Z" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="5.17031" cy="5.11172" r="1.35" fill="#009688"/>
<circle cx="12.1578" cy="7.10313" r="1.15" fill="#009688"/>
</g>
<defs>
<clipPath id="clip0_23434_47713">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#b6b6b6" stroke-width="0.85" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17 A11 11 0 0 1 20 17"/><circle cx="4" cy="17" r="1.5" fill="#b6b6b6" stroke="none"/><circle cx="12" cy="7" r="1.5" fill="#b6b6b6" stroke="none"/><circle cx="20" cy="17" r="1.5" fill="#b6b6b6" stroke="none"/></svg>

Before

Width:  |  Height:  |  Size: 406 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#b6b6b6" stroke-width="0.85" stroke-linecap="round" stroke-linejoin="round"><path d="M5 18 A13 13 0 0 1 18 5"/><path d="M5 5 5 18M5 5 18 5" stroke-dasharray="2.5 2.5" opacity="0.5"/><circle cx="5" cy="5" r="2" fill="#b6b6b6" stroke="none"/><circle cx="5" cy="18" r="1.5" fill="#b6b6b6" stroke="none"/><circle cx="18" cy="5" r="1.5" fill="#b6b6b6" stroke="none"/></svg>

Before

Width:  |  Height:  |  Size: 472 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#b6b6b6" stroke-width="0.85" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="6" height="6"/><rect x="15" y="3" width="6" height="6"/><rect x="3" y="15" width="6" height="6"/><rect x="15" y="15" width="6" height="6"/></svg>

Before

Width:  |  Height:  |  Size: 350 B

Some files were not shown because too many files have changed in this diff Show More