Compare commits

..

107 Commits

Author SHA1 Message Date
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 Liebscher
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 Bianchi
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 Bianchi
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 Robertson
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
harrierpigeon
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
harrierpigeon
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
213 changed files with 9174 additions and 9051 deletions

View File

@@ -80,12 +80,8 @@ endif()
if (DEFINED BBL_RELEASE_TO_PUBLIC)
add_compile_definitions("BBL_RELEASE_TO_PUBLIC=${BBL_RELEASE_TO_PUBLIC}")
if (BBL_RELEASE_TO_PUBLIC)
add_compile_definitions(WXINSPECTOR_DISABLE)
endif ()
else ()
add_compile_definitions("BBL_RELEASE_TO_PUBLIC=$<CONFIG:Release>")
add_compile_definitions("$<$<CONFIG:Release>:WXINSPECTOR_DISABLE>")
endif ()
find_package(Git)

View File

@@ -52,14 +52,6 @@ ExternalProject_Add(dep_OpenSSL
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
"--openssldir=${DESTDIR}"
"--prefix=${DESTDIR}"
# OpenSSL's linux-x86_64 target sets multilib=64, so it installs to
# <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's
# --with-openssl only ever emits -L<dir>/lib, so it misses the bundled
# static libs and silently links the system OpenSSL instead -- which,
# against 1.1.1w headers, leaves _ssl.so with an undefined
# SSL_get_peer_certificate (removed in OpenSSL 3.x). Pin libdir so the
# prefix stays single-layout.
"--libdir=lib"
${_cross_comp_prefix_line}
no-shared
no-asm

View File

@@ -1,72 +0,0 @@
# Printer agents
Printer agents let OrcaSlicer communicate with printers through a
standardized protocol. They translate between a printer's
native API and the application interfaces that the app already
uses.
This documentation explains the compatibility boundary, runtime ownership,
connection and status flow, command and feature behavior, built-in and plugin
agent implementations, and the testing evidence required for compatibility
claims.
## What printer agents do
A printer agent has two jobs:
1. Accept the app's existing commands and translate the ones its
printer supports.
2. Convert native printer status into correctly-shaped state that
`MachineObject` understands.
Currently, agents work at a compatibility boundary, i.e., making other vendors compatible with Bambu-shaped code, not a vendor-neutral one.
Some Bambu concepts remain part of the payload and command vocabulary.
End goal is to make the whole command and payload interfaces vendor-neutral.
## Vocabulary
Every chapter reuses these terms. The "Is not" column is the part that
causes confusion when it is left implicit.
| Term | Is | Selected by | Is not |
| --- | --- | --- | --- |
| Agent ID | Which printer agent implementation to use | `printer_agent` on the printer preset; empty is the legacy `bbl`-or-`orca` sentinel | Which printer |
| Printer agent | The live `IPrinterAgent` instance for that ID, created and cached once per ID by `NetworkAgentFactory` | Factory lookup on the agent ID | A connection, and not one object per printer |
| Device ID | One printer inside that implementation | Bind with Access Code for the Moonraker family, where the entered address becomes the ID; Bambu uses its own discovery identity | Which protocol |
| `MachineObject` | The Device tab's view of one selected printer | `DeviceManager::selected_machine`, which stores only an ID | Proof that a printer is reachable |
| Freshness | `is_connected()`, a test over the last-update time | Any reset of the update time, including one no status has followed | Proof that status arrived |
| Status-confirmed readiness | A push-status message has actually been parsed | The first real status message | The same thing as a successful `connect_printer()` |
Earlier drafts used "transport" for the printer agent instance. That term
is retired: the code selects an implementation, not a wire protocol.
## How to use this guide
- [Architecture](architecture.md) describes objects, ownership, lifetimes,
error handling, the feature gate, and compatibility contracts for printer
agents.
- [Connection and status](connection-and-status.md) describes how presets,
machines, access codes, status messages, and commands fit together at
runtime. Unlike Architecture, it follows the sequence of selecting an
agent, connecting, receiving status, and sending commands.
- [Printing](printing.md), [filament synchronization](filament.md), and
[camera support](camera.md) are separate chapters because they contain
per-feature detail rather than because they are universally special:
Printing has its send, preflight, recovery, and start contracts; Filament
covers acquisition, mapping selection, and print-time delivery; Camera
covers the distinct Bambu, Moonraker, and Snapmaker ownership models.
- [Built-in agents](agents.md) describes the Moonraker family and the Qidi,
Snapmaker variants.
- [Python plugin agents](plugin-agents.md) describes the plugin bridge and
lifecycle.
- [Testing and troubleshooting](testing.md) explains automated checks, manual
hardware work, known defects, and the evidence required for compatibility
claims.
- The [capability matrix](reference/capability-matrix.md) is the compact
feature reference. The [manual checklist](reference/manual-checklist.html)
is for a live-printer verification pass.
Treat source code as authoritative when it differs from this guide. In
particular, preserve the compatibility rules called out in each chapter:
they protect stored presets, existing profiles, and the Device tab's
assumptions.

View File

@@ -1,205 +0,0 @@
# Built-in printer agents
*Owns the per-vendor behavior of the built-in agents: what each subclass
changes and what it inherits unchanged. Defers the interface every agent
implements to [Architecture](architecture.md) and
[Python plugin agents](plugin-agents.md).*
This chapter covers the built-in Moonraker family: the general
`MoonrakerPrinterAgent` and the Qidi and Snapmaker variants. Creality
(`CrealityPrintAgent`) is also a member of this family and inherits the base
behavior, but has no section here; see the capability matrix for its
per-feature coverage. They share the same connection and status machinery.
Change the base class only when the behavior is valid for all of them.
Each subclass is thin. `MoonrakerPrinterAgent` holds the HTTP connection,
the WebSocket status subscription, the REST command worker, thumbnail
lookup, the chamber-light heuristic, and the upload-and-start path.
`QidiPrinterAgent` overrides filament discovery and adds multi-color box
mapping; `SnapmakerPrinterAgent` overrides filament discovery and camera
setup; `CrealityPrintAgent` overrides filament refresh. Each derives from
`MoonrakerPrinterAgent` and is `final`, which is why the guard rule below
must be type-based.
## Moonraker family
### Connection and commands
Moonraker-family agents use plain HTTP for the LAN connection. The connection
path deliberately ignores a TLS request because the supported printer stacks
serve Moonraker or a reverse proxy over HTTP. Restoring the caller's TLS
default can send a connection to an unavailable HTTPS endpoint.
Status is a Moonraker WebSocket subscription. Commands use REST. Command
translation happens immediately, but the resulting HTTP work runs through one
agent-owned FIFO worker. Each queued operation captures the current base URL
and API key before it is queued, so a later printer switch does not redirect
an earlier command. Keep this separation: network work on the UI path makes
controls feel stalled, and allowing a queued command to reread connection
state can send it to the wrong printer.
Pause, resume, and cancel use the dedicated Moonraker print endpoints. Do not
replace them with queued `PAUSE`, `RESUME`, or `CANCEL_PRINT` G-code. The
endpoints interrupt the print directly; a G-code command can wait behind the
active print or macro.
The request router accepts the Bambu-shaped JSON used by the native device
tab. Supply object-shaped namespaces such as `print` and `system`. A malformed
but parseable payload with a scalar where the router expects an object can
still fail before the unsupported-command fallback. The supported generic fan
status is the standard `fan` object, which represents the part fan only.
Ordinary part-fan control also works through the legacy `gcode_line` path,
which sends `M106` while `is_enable_np` is false. Auxiliary and chamber fans
are neither reported nor controlled.
Do not add `cfg`, `fun`, `aux`, and `stat` to the Moonraker status payload just
to make it look more complete. Together those fields set `is_enable_np` and
make the UI choose its structured fan and extruder commands instead. The
Moonraker agent does not translate those commands, so working controls become
unsupported no-ops. This is a UI-routing constraint, not a reason to expose
structured fan support.
### Status shown by the native device tab
The agent translates Moonraker status into the Bambu-shaped status payload the
existing Device tab understands. Some fields are necessarily synthetic:
- The virtual SD-card readiness bit and a basic software-version row make the
native UI consider the printer ready. Each pull payload also ensures
`m_push_count` and `m_full_msg_count` are at least one and refreshes
`last_push_time`. Together with the normal-storage state and a placeholder
module version, this satisfies the native `is_info_ready()` and printing
gates. These are compatibility scaffolding, not reports of physical storage
or OTA support.
- Current and total layers are emitted only when `print_stats.info` contains
numeric values. Moonraker may send `null`, and many profiles do not emit the
`SET_PRINT_STATS_INFO` data needed to populate them. Do not turn that gap
into a JSON conversion exception.
- Remaining time is estimated from elapsed print time and virtual-SD progress.
It is omitted below two percent progress because the early estimate is too
unstable. Do not derive an ETA by subtracting Moonraker duration counters:
both are elapsed counters, so their difference is overhead, not remaining
time.
- Temperature readings are available, but nozzle diameter and nozzle type are
not supplied in the status payload. The UI can therefore show an unknown
nozzle. Do not make print submission depend on those missing fields.
### Camera thumbnails and lights
For a running job, the agent asks Moonraker for thumbnails and chooses the
widest usable entry, rather than assuming the first entry is useful. It accepts
both thumbnail path spellings used by Moonraker versions, encodes each path
segment, and caches the result by filename. A failed transient lookup is tried
again only a bounded number of times; a clean response without a thumbnail is
cached as a negative result. The response shape handling is source-derived,
not hardware-verified.
> **Do not perform this HTTP lookup while holding `payload_mutex`.** The
> WebSocket thread builds the status payload under that mutex and the UI
> path also needs it, so a thumbnail timeout taken under the lock would
> stall status delivery or the UI. The lookup still blocks the WebSocket
> thread briefly, so move it to a worker if that becomes measurable.
Chamber-light control searches Moonraker objects for names that look like a
light or a standalone LED, then writes the first matching pin, LED, or macro.
The filter exists to avoid treating unrelated objects, such as a beeper, as a
lamp. It remains a heuristic. The incoming `led_node` is validated, but only
`chamber_light` is acted on; `chamber_light2` is deliberately ignored. A
printer with more than one lamp therefore has no reliable node-to-object map.
### Common maintenance limits
The same cache is reused for a selected agent ID, not per physical printer.
Qidi and Snapmaker inherit this behavior. A stateful feature added
to the base class must be reset carefully when a preset switches hosts.
> **Keep guards for this family type-based** - check whether an agent
> derives from `MoonrakerPrinterAgent` rather than comparing its ID to
> `moonraker`. An ID-based guard silently excludes Qidi, Snapmaker, and
> Creality, even though they share the base behavior.
The family has no generic implementation for firmware-specific AMS write
commands. Keep unsupported commands unsupported until the printer-side macro
or API is known. Reporting success for an untranslated command makes the
native UI claim that an action happened when it did not.
## Qidi
Qidi inherits the Moonraker connection, status, camera, and local-print path.
Its differences are Qidi filament discovery and the pre-print multi-color-box
mapping.
### Filament discovery
Discovery first reads the printer's device information to infer a Qidi series
identifier, then falls back to the configured Orca model if needed. Series
inference intentionally recognizes only a narrow set of known names. An
unknown model still produces usable generic filament data, but not a
series-specific preset identifier.
The agent reads a Qidi filament dictionary and the `save_variables` plus
slot-runout data. Failing to fetch the dictionary is non-fatal: slot discovery
continues with fallback material and colour values. Failing to fetch or parse
slot data is fatal to the refresh. A missing runout value means the agent
cannot prove filament is loaded, so it reports that slot as empty. This is an
ambiguity in the firmware data, not proof that the box is empty.
`save_variables.variables` must be an object. Qidi firmware can return `null`
there, and generic JSON value access can throw on a present null. The parser
rejects that shape without throwing. Preserve the null-slot tests whenever the
response parser changes.
### Multi-color mapping before a print
Before every Qidi print-start wrapper, the agent writes `enable_box` and, for
mapped tools, persistent `value_t<tool>` variables. These writes survive the
job. Invalid mapping JSON is checked only after `enable_box` has been written.
When the mapping is enabled, that failure can therefore leave `enable_box=1`.
There is no rollback for this or for a later per-tool write failure, so a
partial mapping can remain on the printer. An empty mapping is accepted when
the box is enabled. Single-colour jobs disable the box but leave old per-tool
assignments in place.
`enable_box` currently follows `task_use_ams`. That meaning has not been
verified against all Qidi firmware: if firmware treats it as "a box exists"
rather than "use the box for this job", this gate is wrong and needs hardware
evidence before it changes.
Only `start_local_print` reaches Moonraker's real upload-and-start path. The
other Qidi mapping wrappers currently return success stubs after applying the
mapping. Do not describe those wrappers as confirmed print paths.
Because the agent cache is keyed by agent type, a Qidi mapping can also become
stale when switching between Qidi printers. This is a generic Moonraker-family
state risk, made more consequential by Qidi's persistent firmware variables.
The configured `printer_type` can also be stale, so treat it as a fallback
hint rather than device truth.
## Snapmaker
Snapmaker uses the Moonraker base and overrides filament discovery and camera
setup. Neither path is hardware-verified in the current documentation set.
Filament information comes from parallel arrays in `print_task_config`.
`filament_exist` defines the number of slots; shorter type, subtype, colour,
vendor, or NFC arrays use safe fallback values. The agent first tries a visible
vendor, type, and colour preset, then a visible type match, and finally a
generic identifier when no preset bundle is available. An empty reported type
is changed to `PLA`, so an unknown occupied spool can look like confirmed PLA.
An unrecognized type can also reach the visible-preset fallback and be paired
with an unrelated visible preset. Treat the resulting preset as a suggestion,
not printer-ground truth.
Snapmaker U1 camera support starts the printer's monitor RPC, then serves the
still JPEG through a small local HTML page that reloads it after each load or
error. The wrapper is required because a direct still-image URL looks frozen.
The RPC is sent from a detached thread so the UI timer does not block on socket
I/O. That thread captures `this` directly, so agent destruction can race with
the camera command. Do not widen this pattern. Route future asynchronous work
through owned lifetime-managed work where possible.
## Source locations
- `src/slic3r/Utils/MoonrakerPrinterAgent.cpp`
- `src/slic3r/Utils/QidiPrinterAgent.cpp`
- `src/slic3r/Utils/SnapmakerPrinterAgent.cpp`

View File

@@ -1,158 +0,0 @@
# Architecture
*Owns the structural rules: what the objects are, who owns them, what an
agent must implement, and which behaviors are compatibility contracts.
Defers the runtime sequence - selecting, connecting, receiving status,
sending commands - to [Connection and status](connection-and-status.md).*
## The compatibility boundary
The Device tab was built around Bambu-style commands and status. A printer
agent is the translation boundary between that existing contract and a
vendor's native protocol:
```text
Device tab <-> MachineObject <-> NetworkAgent <-> IPrinterAgent
<-> vendor protocol
```
Note: end goal is to move beyond this and achieve a truly vendor-neutral translation layer.
The GUI builds commands and reads `MachineObject` state. An agent owns the
vendor request, response, connection, and status translation. Keep vendor
details on the agent side of this boundary.
Status translation is deliberately Bambu-shaped. Agents deliver payloads
through the callbacks used by the existing Bambu path, and
`MachineObject::parse_json()` interprets them. This preserves the Device
tab's established behavior, but it is not a vendor-neutral protocol.
Important (again): end goal is to move beyond this and achieve a truly vendor-neutral translation layer.
## Runtime objects and ownership
`NetworkAgent` is the facade used by the application. It holds one live
`IPrinterAgent` pointer, which is initially null and may return to null
when a selected ID is unavailable. Callers must handle the null case. An
absent agent is an inert state, not permission to fall back to another
printer agent. A fallback would connect to a different implementation than
the one selected by the preset, and could therefore send commands or status
work to the wrong printer.
`NetworkAgentFactory` registers built-in and plugin implementations by
agent ID. It creates and caches one implementation for each ID. The ID
selects a printer agent implementation, while a `MachineObject` selects one
printer by device ID. The resulting cardinality is one active agent to many
machines.
For example, suppose two Moonraker printers are on the LAN at
`192.168.1.20` and `192.168.1.21`. In the Device tab machine-select popup, the
user chooses **Bind with Access Code**; `PinCodePanel::on_mouse_left_up` opens
`InputIpAddressDialog`, and each entered address is bound as a separate
printer. Both presets store the same agent ID, `moonraker`, so
`NetworkAgentFactory::create_printer_agent_by_id` returns the same cached
`IPrinterAgent` pointer for both presets. Each printer nevertheless has its
own `MachineObject` and device ID. For the Moonraker family,
`MoonrakerPrinterAgent::bind_detect` calls `init_device_info` with the entered
address as both the device ID and address, so the two device IDs are the two
addresses.
That is what one active agent to many machines means. Per-printer state must
be keyed by device ID rather than held only on the agent instance, because one
agent object is shared by both printers. State stored only on that object
would be shared between two different machines and could route status or
commands to the wrong one. The same sharing explains why
`GUI_App::switch_printer_agent` compares device IDs even when the agent pointer
is unchanged: otherwise its unchanged-agent early return would skip
reselection when the user switches between these presets, leaving status and
filament work aimed at the previous printer.
> **Do not make an agent instance per printer just to hold device state.**
> Keep per-printer state keyed by device ID, because one agent object is
> shared by every printer of that type - state held on the instance would
> route status or commands to the wrong `MachineObject`.
> **Do not fall back to another printer agent when the live one is null.**
> An absent agent is an inert state. A fallback would connect to a
> different implementation than the preset selected.
## Commands and unsupported work
An agent must either translate a Device-tab command or return an explicit
error. `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` means no translation exists.
`ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE` means a translation exists but this
printer cannot use it. `MachineObject::publish_json()` turns either result
into the user-visible unsupported-command response.
Every Device-tab command must leave by one of these four exits. The fifth
path is the one to watch for in review:
```mermaid
flowchart TD
CMD["Device-tab command JSON"] --> PUSH{"pushing namespace?"}
PUSH -- yes --> OK1["Accept - the status stream already satisfies it"]
PUSH -- no --> TRANS{"Translation exists for this agent?"}
TRANS -- no --> E1["Return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED"]
TRANS -- yes --> CAP{"This printer can use it?"}
CAP -- no --> E2["Return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE"]
CAP -- yes --> OK2["Translate and send to the printer"]
TRANS -. FORBIDDEN .-> BAD["Return success without translating"]
E1 --> PUB["MachineObject::publish_json turns both errors<br/>into the unsupported-command response"]
E2 --> PUB
BAD --> LIE["UI reports an action that never happened"]
```
> **Do not return success for an unhandled command.** That makes an
> unsupported button look as though it worked and hides missing coverage
> from both users and maintainers.
The `pushing` command namespace is the exception. Its request means
"send status"; an active status stream already satisfies it. The Device
Manager sends these requests repeatedly as a keepalive, so rejecting them
would surface a warning repeatedly even though no action is missing.
## Feature gate
`use_printer_agents` enables printer-agent routing. With the gate off,
agent code must have no observable effect. Released profiles can already
contain `printer_agent` values, so activating an agent while the gate is
off would change existing user behavior merely by loading a profile.
Keep the gate at the routing call sites. Do not fold it into general Bambu
vendor checks: slicing and hardware decisions such as AMS, lidar, bed
types, and G-code flavor still describe printer capabilities, not the
selected printer agent.
## Backward compatibility
`printer_agent` remains a `coString`, even when the ID is currently
unregistered. A preset may refer to an optional plugin that is not
installed. The unknown string must load, remain unchanged, and round-trip
without making the preset dirty. The UI may show it as missing, but must
not rewrite it to a fallback ID.
Keep the feature gate's off-path behavior unchanged, preserve stored agent
IDs, and treat Bambu-shaped payloads as a compatibility contract.
The reason these three are grouped is that each looks like a local code
change and is not. Switching which printer agent handles a preset edits no
profile and no project file, so it reads in review as contained to the
agent layer. But a user's stored presets and `.3mf` projects already carry
`printer_agent` values and were saved against the Bambu-shaped payload. So
a change that is local in the code is not local in effect: it reaches
every previously saved file. That is why the gate must be inert when off,
an unknown ID must survive untouched, and the payload shape is treated as
a contract rather than an implementation detail.
## Threading rule
Agents may perform network work on their own threads, but all mutations of
Device Manager maps and `MachineObject` UI state must run on the UI thread.
Queue incoming status before it reaches `parse_json()` or any operation
that adds, removes, selects, or changes a device. This prevents races
between background network callbacks and UI reads. For example, when a status
callback arrives on an agent's network thread, queue it to the UI thread
before it reaches `MachineObject::parse_json()` or changes a device map or
selection. The Device tab reads those same structures on the UI thread, so
parsing or adding, removing, or selecting a device from the network thread
could race with that read.

View File

@@ -1,96 +0,0 @@
# Camera support
*Owns the three camera ownership models and what each one renders through.
Defers the Snapmaker filament path to [Built-in agents](agents.md), even
though the same subclass owns both.*
Camera support has three ownership models. They share the Device panel,
but not a common frame or stream interface.
Two render surfaces, never one:
```mermaid
flowchart LR
BU["Bambu URL provider<br/>LAN or cloud, not from IPrinterAgent"]
MU["Moonraker webcam discovery<br/>/server/webcams/list stream URL"]
SU["Snapmaker camera page<br/>local HTML that polls monitor.jpg"]
BV["wxMediaCtrl2<br/>native Bambu media pipeline"]
WV["Device-panel wxWebView<br/>stream URL or local polling page"]
BU --> BV
MU --> WV
SU -- overrides normal discovery --> WV
```
## Bambu
Bambu playback uses `wxMediaCtrl2` and the native Bambu media pipeline.
The URL comes from the Bambu LAN or cloud path, not from
`IPrinterAgent`. A printer agent should not attempt to force a Bambu URL
through the Moonraker or WebView path.
## Moonraker live view
On connection, the Moonraker agent obtains the first enabled webcam URL
from `/server/webcams/list`. An absolute HTTP URL is used directly. A
relative URL is resolved against the printer's host web root, with the
Moonraker API port removed. This is necessary because a relative webcam
path may exist on the printer's web server but not on the API port.
The connection generation guards the result. A late request must not
replace the URL after the user has selected a different printer. Failed
discovery clears the URL, which prevents a prior camera from remaining
visible on a printer with no camera.
The agent places the discovered URL in its status payload. The Device
panel renders it in `wxWebView`. It reloads only when the URL changes and
resets the camera-start timestamp at that point. Reloading every update
would loop indefinitely for endpoints that redirect, so an unchanged URL
is shown again without calling `LoadURL()`.
The trade-off is intentional: a WebView that loses an unchanged stream
does not automatically reload. Camera controls beyond live viewing remain
out of scope for Moonraker. Recording, timelapse, settings, and virtual
camera are Bambu-oriented features and must not be presented as supported
merely because live view works.
## Snapmaker polling view
Snapmaker overrides normal webcam discovery. It writes a per-printer local
HTML page that polls the printer's `monitor.jpg` with a cache-busting URL.
Each next request starts after the prior image loads or fails, preventing
requests from piling up on a slow printer. A raw snapshot URL is not used,
because it would display one frozen frame instead of a live-looking view.
The printer must be asked to start its camera capture task. While the
camera view is visible, the Device panel requests this at first display
and then attempts another request every 300 seconds. Other agents reject
the command quietly, so the common timer does not create an error for
Bambu or ordinary Moonraker.
The Snapmaker command is sent from a detached thread because the request
can block on socket I/O and the printer responds over a different channel.
This avoids blocking the UI but leaves a raw-`this` lifetime risk: the
agent can be destroyed while the detached operation still refers to it.
Do not extend this path without addressing that ownership boundary.
The wrapper is written below the application cache with a name derived from
the printer IP. The source contains no cleanup path for those files, so they
can accumulate as different printer IPs are used. This is source-derived and
was not reproduced during this rewrite.
Source code proves 300-second renewal attempts only. The long-running
behavior of the shipped polling and renewal cycle has not yet been tested.
Do not claim that the attempt renews an active capture task or that it
prevents camera expiry until hardware verification establishes both.
## Maintenance checklist
- Keep the three ownership models separate.
- Preserve host-root resolution for relative Moonraker URLs.
- Keep generation guards and stale-URL clearing on every discovery path.
- Reload WebView content only after a URL change.
- Reset the camera-start timestamp when the camera URL changes.
- Treat Moonraker as live-view-only and Snapmaker lifecycle behavior as
not yet verified beyond the observed renewal attempts.

View File

@@ -1,248 +0,0 @@
# Connection and status
*Owns the runtime sequence in order: selecting an agent and machine,
starting a connection, receiving status, sending commands. Defers the
structural rules those steps must obey - ownership, no-fallback, unknown
IDs, threading - to [Architecture](architecture.md), and cites them at the
point where they bite.*
## The four runtime concepts
Keep these concepts separate when tracing a connection problem:
- A preset stores an agent ID and printer address.
- `NetworkAgent` holds the active printer agent for that agent ID.
- `MachineObject` represents the selected printer at that address.
- Freshness and status-confirmed readiness are separate states.
An agent ID selects a printer agent implementation. A device ID selects one
printer within that implementation.
A non-Bambu printer reaches the machine list through **Bind with Access
Code**, the tile in the Device tab's machine-select popup. The user enters
an address and an access code, `bind_detect()` probes the address before
any connect, and `DeviceManager::insert_local_device()` creates the
`MachineObject`. For the Moonraker family the address itself becomes the
device ID: `MoonrakerPrinterAgent::bind_detect()` seeds `dev_name` and
`dev_id` from the entered address, so an unreachable or unnamed printer
still shows up as its IP rather than blank.
Binding is the only route for that family. `MoonrakerPrinterAgent::start_discovery()`
deliberately announces nothing, because a partial discovery implementation
would populate the machine list with stale hosts. Bambu is the exception:
it has its own discovery identity and does not use the address as an ID.
`DeviceManager::selected_machine` is only a selected ID. It can name no
resolvable object. `get_selected_machine()` answers whether an object is
actually available. `set_selected_machine()` accepting an ID therefore
does not prove the printer is connected. The selected ID can remain when its
object is unavailable, so connection state must come from the object itself.
## Selecting the agent and machine
`GUI_App::switch_printer_agent()` reads the edited printer preset and
resolves its stored agent ID through `NetworkAgentFactory`.
1. An empty stored ID is a legacy sentinel. It resolves to `bbl` for a
Bambu vendor preset and to `orca` otherwise.
2. If that effective ID is registered, the factory provides the matching
printer agent implementation.
3. Clear the live printer agent only when a nonempty stored ID is unregistered
or the factory cannot construct the matching registered agent.
4. When the active printer agent changes, clear the current selection, user
selection, stale device discoveries, sidebar state, and AMS state before
installing the replacement.
5. Select the preset's address-derived machine for non-Bambu agents.
The lifetimes are easier to see than to read. Note that the agent pointer
can be unchanged while the machine still must be re-selected - that is the
trap in the same-agent path below:
```mermaid
sequenceDiagram
participant U as User
participant P as Printer preset
participant G as GUI_App::switch_printer_agent
participant F as NetworkAgentFactory
participant N as NetworkAgent
participant M as MachineObject
U->>P: Edit or switch the printer preset
P->>G: printer_agent value, possibly empty
G->>G: resolve_printer_agent_id - empty becomes bbl or orca
G->>F: get_printer_agent_info(effective ID)
alt ID not registered, and stored value was nonempty
F-->>G: no info
G->>N: clear the live printer agent
Note over N: null is inert - never fall back to another agent
else ID registered
F->>F: create once per ID, then serve from cache
F-->>G: the cached IPrinterAgent for this ID
alt Agent pointer changed
G->>N: clear selection, user selection, discoveries, sidebar, AMS
G->>N: install the replacement agent
else Same cached pointer returned
Note over G,F: Two presets can share one agent ID
end
G->>M: compare device ID, re-select if the address differs
end
```
> **Do not use the first available machine as a fallback** (rule owned by
> [Architecture](architecture.md), Runtime objects and ownership). It
> connects to a printer the user did not choose, including one owned by a
> different printer agent.
The same-agent path is important too. Two presets can use one agent type
but point at different addresses, and the factory caches one agent per ID,
so switching between them returns the same pointer and would otherwise
skip reselection entirely. Re-select the machine whenever the preset's
address changes, even when the factory returned the same active agent.
Otherwise status and filament work can continue against the previous
printer.
Note: this is a legacy coupling, not the primary workflow. It reads an
address stored on the printer preset itself (`print_host` and
`printhost_port`, named here only so the keys can be found in the code)
and derives a device ID from it with `dev_id_from_address()`. Those keys
predate printer agents and are edited through `PhysicalPrinterDialog`,
which despite its name writes the printer preset rather than a
`PhysicalPrinter` object - that object is no longer constructed. Printers
normally arrive through Bind with Access Code instead, which does not
touch the preset. Both routes end at `insert_local_device()`, so they must
agree on the device ID: `dev_id_from_address()` strips the URL scheme and
drops an empty port, while the bind path stores the address as the user
typed it.
The unknown-`coString` compatibility rule belongs to `architecture.md` under
Backward compatibility. Keep a nonempty unknown `printer_agent` ID unchanged
and display a missing state if needed; do not rewrite it during plugin unload
or choose an arbitrary replacement, so the preset can round-trip while its
plugin is temporarily unavailable.
## Starting a connection
Machine selection causes `MachineObject::connect()` to invoke the active
agent's `connect_printer()` with the selected address and effective access
code. A success return means that the connection attempt started. It does
not mean that the printer is ready or that a status stream is alive.
Moonraker-family agents must force HTTP. Moonraker and print-host
installations commonly serve plain HTTP, while the generic machine path
can request TLS by default. Passing that default through turns a valid
connection into an HTTPS request the printer will refuse. The agent therefore
must keep the connection on HTTP unless its protocol support changes
deliberately and is verified.
## Access codes: four coordinated slots
One effective access code can live in four places:
| Slot | Location | Purpose |
| --- | --- | --- |
| Device runtime | `MachineObject::access_code` | Code learned from the device. |
| User runtime | `MachineObject::user_access_code` | Code entered by the user. |
| Device config | `access_code[dev_id]` | Persisted device value. |
| User config | `user_access_code[dev_id]` | Persisted user value. |
The effective code prefers the user value when present, then the device
value. Keep user input in the user path and device replies in the device
path. Crossing those paths obscures which value should win.
`set_access_code()` deliberately does not save configuration immediately.
Device replies and polls can update it often; forcing a full config write
for each message adds unnecessary work. The normal deferred config save
persists dirty state later. Do not add an eager save just to make this one
path symmetric: device replies and polls update it often, so a config
write per message is wasted work.
> **Do not erase the user access code when a printer connects.** On the
> LAN reselection path that code can be the only credential that lets the
> machine pass the access check and receive the status or access-code
> reply that would refresh it, so erasing it at connection time can leave
> the machine permanently unable to receive updates. A failed connection
> is the place to handle a proven bad credential.
## Receiving status
An agent receives native status, translates it to the existing payload
shape, and dispatches it to the matching `MachineObject`. The object
parses the payload and records when it last received an update.
Readiness is four states, and three of them look connected:
```mermaid
stateDiagram-v2
[*] --> SelectedIdOnly
SelectedIdOnly: Selected ID only
SelectedIdOnly: selected_machine names no resolvable object
SelectedIdOnly --> FreshWindow: connect_printer returns success
FreshWindow: Fresh window
FreshWindow: reset seeded the update time - no status yet
FreshWindow --> Connecting: still fresh, still no push-status
Connecting: Connecting
Connecting: is_connecting true - the honest state
Connecting --> Ready: first push-status message parsed
Ready: Status-confirmed ready
Ready: the only state that proves a usable printer
Ready --> Stale: update time ages out
Stale: Stale
Stale: is_connected false
Stale --> Ready: a later status message arrives
note right of FreshWindow
is_connected() is true from here on.
It is a freshness test over the update
time - not proof that any status arrived.
end note
```
`is_connected()` is a freshness test over the update time. It does not
describe whether `connect_printer()` returned success or whether any status
message arrived: reset initializes the update time, creating an initial
freshness window. `is_connecting()` distinguishes that window from
status-confirmed readiness: while the object is fresh and no push-status
message has arrived, it remains connecting.
> **Do not treat freshness or a successful connect as proof of readiness.**
> Code that needs a usable printer must wait for status-confirmed
> readiness, because the fresh window exists before any status has been
> parsed.
The UI-thread mutation rule belongs to `architecture.md` under Threading rule.
Dispatch the status callback to the UI thread before changing device maps,
selection, or `MachineObject` state, because network callbacks may run in a
worker thread and mutating these structures there races with the Device tab.
## Sending commands
`MachineObject` builds the established command JSON and sends it through
the active `NetworkAgent`. The agent translates it or returns an explicit
unsupported result. It must not report success when no translation exists.
The `pushing` command exception belongs to `architecture.md` under Commands
and unsupported work. It asks for status, and a working status stream already
supplies it, so accepting it avoids false unsupported warnings from the Device
Manager's repeated keepalive.
## Maintainer constraints
- Preserve same-agent reselection by address, because an agent type can
serve more than one printer.
- Preserve the null-agent, no-fallback, and unknown-`coString` rules in
`architecture.md`; selection must remain an explicit user or preset choice,
and stale state must not belong to a replacement printer agent.
- An empty value is the legacy Bambu-or-Orca sentinel, not a missing printer
agent.
- Preserve deferred access-code saves and the no-on-connect-erase rule;
they prevent excessive config writes and credential-driven status loss.
- Keep Moonraker connections HTTP-only unless the agent's protocol support
changes deliberately and is verified.
- Do not treat freshness as proof that status arrived; wait for
status-confirmed readiness. The UI-thread mutation rule is in
`architecture.md` under Threading rule.

View File

@@ -1,110 +0,0 @@
# Filament synchronization and mapping
*Owns the three filament stages end to end: acquiring printer state,
selecting a mapping, and delivering it at print time. Defers the upload
and start mechanics to [Printing](printing.md), and per-vendor discovery
detail to [Built-in agents](agents.md).*
Filament support has three separate stages. A successful first stage does
not mean that a selected mapping will be delivered to the printer.
Stage 3 is where the two paths diverge, and only one of them reaches the
printer:
```mermaid
flowchart TD
SRC["Filament source<br/>Moonraker lane_data, or the classic MMU object"]
ACQ["1 - Acquire state<br/>Moonraker pull, not subscription"]
ST["DevFilaSystem<br/>Bambu-shaped view, synthetic 4-slot groups"]
SEL["2 - Select mapping<br/>Send dialog matches project filament to slots"]
PAR["PrintParams<br/>ams_mapping fields"]
QIDI["Qidi agent writes enable_box<br/>and value_t per tool, then starts the print"]
BASE["Base Moonraker, Happy Hare, AFC<br/>upload and start ignore the mapping fields"]
OK["Mapping reaches the printer"]
DEAD["Mapping never reaches the printer<br/>the job runs on the printer's own loaded-lane behavior"]
SRC --> ACQ --> ST --> SEL --> PAR
PAR -- Qidi --> QIDI --> OK
PAR -- every other target --> BASE --> DEAD
```
## 1. Acquire printer state
`FilamentSyncMode` declares how the UI obtains filament state:
| Mode | Meaning |
| --- | --- |
| `subscription` | A status stream keeps the state current. |
| `pull` | The UI must request state before it can use it. |
| `none` | The agent has no usable filament state. |
Moonraker uses `pull`. Its ordinary status stream does not supply the
filament data used by this UI. In particular, `lane_data` is a Moonraker
database namespace, not a printer object that the existing subscription
can follow. Changing Moonraker to `subscription` would suppress the pull
that actually populates the UI.
The agent first reads `lane_data`, which can describe AFC and newer Happy
Hare installations. If that is unavailable, it reads the classic Happy
Hare `mmu` object. Those response shapes are source-supported but not yet
verified against current Happy Hare and AFC installations.
The current parser expects lane values as strings and silently skips
numeric values. Whether current AFC or Happy Hare installations emit
numeric lane values is unverified.
The received lanes are converted into a Bambu-shaped model so existing
AMS UI can render them. The model groups numeric lane indexes into
synthetic groups of four slots and passes the result through
`ParseV1_0`. This is a UI compatibility adapter, not evidence that the
printer has a Bambu AMS.
Pull state can be stale. The Send dialog can build a mapping from the
current `DevFilaSystem` without refreshing it first, and a failed pull can
leave older state visible. Do not represent a displayed lane list as a
fresh printer read unless the call site just performed the pull.
## 2. Select a mapping
The Send dialog matches each project filament to a compatible reported
slot. It rejects a mismatched material type and then prefers compatible
slots according to the existing mapping rules. The result is carried in
the legacy linear mapping, the explicit AMS-and-slot mapping, and mapping
metadata for the job.
Treat lane numbers as printer contracts. A numeric lane index is used as a
slot index in the synthetic four-slot view, so an incorrect numbering
assumption can select the wrong physical lane.
The material identity code also retains a defect: ABS and ASA can be
shown as PLA when profile identifiers collide. This is not fixed here,
and multi-color mapping has not received hardware verification.
## 3. Deliver the mapping at print time
`PrintJob` copies the selected mappings into `PrintParams`, but base
Moonraker does not read those fields when it uploads and starts a print.
For plain Moonraker, Happy Hare, and AFC targets using that base path, a
correct-looking mapping in the UI is therefore not delivered to the
printer. The print runs using the printer's own loaded-lane behavior.
Qidi is the implemented exception. Its agent writes its own box mapping
before starting the print. That is a Qidi-specific delivery contract, not
a generic Moonraker solution.
There are deliberately no guessed Happy Hare or AFC write macros. Their
macro and variable names are defined by printer-side configuration, so a
guessed command could silently do nothing or control the wrong setup. Add
a delivery path only after verifying the exact contract against upstream
documentation or a real printer.
## Maintenance checklist
- Keep Moonraker in `pull` mode while `lane_data` remains pull-only.
- Refresh or clearly surface stale state before relying on Send-dialog
mappings.
- Do not claim base Moonraker honors mappings until it consumes them at
print time.
- Preserve Qidi as a distinct delivery implementation.
- Verify lane numbering, material identity, and multi-color behavior on
hardware before expanding the mapping contract.

View File

@@ -1,161 +0,0 @@
# Python printer-agent plugins
*Owns the plugin bridge: the implementation contract, registration and
lifetime, and the audit scope. Defers what the agent must do once live to
[Architecture](architecture.md) and
[Connection and status](connection-and-status.md).*
Python printer agents use the current capability bridge. They do not use a
separate adapter or a Moonraker-specific plugin path.
## What an agent must implement
Not overriding a member of `IPrinterAgent` has four different
consequences depending on which tier it is in. This is the whole plugin
contract:
| Tier | Members | Consequence of not overriding |
| --- | --- | --- |
| Pure virtual | `connect_printer`, `disconnect_printer`, `send_message_to_printer`, the `start_*` print operations, `start_discovery`, `bind`, `bind_detect`, `unbind`, the callback setters, `set_cloud_agent`, `get_agent_info`, and the rest of the pure surface | Compile error |
| Concrete, succeeds | `start_subscribe`, `stop_subscribe`, `add_subscribe`, `del_subscribe` | Silently returns `BAMBU_NETWORK_SUCCESS` |
| Concrete, declines | `command_ams_refresh_rfid`, `command_ams_calibrate`, `command_ams_select_tray`, `command_start_camera` | Silently returns `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` |
| Concrete, inert | `get_filament_sync_mode`, `fetch_filament_info` | Reports `FilamentSyncMode::none` and `false` - no filament capability at all |
The refusal tier is deliberate: those commands carry Bambu-dialect G-code
in their bodies, so the honest default is a refusal that
`MachineObject::publish_json()` turns into a dialog. The success tier is
equally deliberate - a printer whose status already streams needs no
subscription call.
> **Do not assume a missing override quietly inherits useful behavior, and
> do not assume it fails loudly either.** Only the first tier fails at
> compile time. The second silently reports success, the third silently
> declines, and the fourth silently reports no filament capability.
## The plugin contract
A plugin subclasses `printer_agent.PrinterAgentBase`, the Python binding for
`PrinterAgentPluginCapability`. The capability itself is the live native
`IPrinterAgent`; there is no intermediate protocol adapter, because
`PrinterAgentPluginCapability` inherits both `PluginCapabilityInterface`
and `IPrinterAgent` directly.
`get_type()` stays a `PluginCapabilityInterface` method and
`set_cloud_agent()` remains the native host injection point. A plugin must
implement the pure connection, communication, discovery, binding, print,
callback-registration, and filament-refresh operations. The certificate,
bind-ticket, HMS-snapshot, and user-selected-machine members are pure too;
the table above abridges the list.
The only tracked Python printer-agent implementation is the BBL plugin. There
is no Python Moonraker printer agent in the current source tree. Moonraker is
implemented by the built-in C++ class.
## Registration and lifetime
When an enabled plugin advertises a printer-connection capability, the factory
gets its `AgentInfo` and registers a factory under `AgentInfo.id`. This is the
same registry used for built-in agents.
Two similarly named structs are involved, and they are not the same thing.
`AgentInfo` is what the agent says about itself; `PrinterAgentInfo` is the
registry's entry about it:
```mermaid
classDiagram
class AgentInfo {
<<returned by the agent via get_agent_info>>
string id
string name
string version
string description
}
class PrinterAgentInfo {
<<the registry entry>>
string id
string display_name
string plugin_identifier
PrinterAgentFactory factory
}
class PrinterAgentFactory {
<<std::function>>
takes cloud_agent and log_dir
returns shared_ptr~IPrinterAgent~
}
class NetworkAgentFactory {
<<all static>>
register_printer_agent(id, display_name, factory)
create_printer_agent_by_id(id, cloud_agent, log_dir)
clear_printer_agent_cache()
register_python_printer_agent(plugin_key, capability_name)
deregister_python_printer_agent(plugin_key, capability_name)
}
AgentInfo ..> PrinterAgentInfo : id becomes the registry key
PrinterAgentInfo *-- PrinterAgentFactory
NetworkAgentFactory o-- PrinterAgentInfo : one entry per ID
PrinterAgentFactory ..> PrinterAgentPluginCapability : weak reference
```
`plugin_identifier` is empty for built-ins and
`<plugin_key>;<uuid>;<capability_name>` for plugins - that is how the
registry tells the two apart at deregistration time. Built-in IDs are the
constants `ORCA_PRINTER_AGENT_ID` and `BBL_PRINTER_AGENT_ID`.
Agent IDs are global. A plugin cannot replace a built-in agent or another
plugin with the same ID. Registry rejection is unconditional. The conflicting
capability is disabled and the user is shown the conflict only when `wxTheApp`
exists and the app is not closing. Re-registering the same plugin capability
is allowed so a reload can replace its factory with the current capability
instance.
The registered factory holds a weak reference to the capability. If the plugin
has already gone away, creation returns null instead of reviving a destroyed
Python object. Callers must treat that as no active printer agent.
On deregistration, the factory removes the registry entry and cached agent,
disconnects a cached agent, and clears the live agent if it has the same ID.
This order prevents `NetworkAgent` from retaining a Python implementation
whose module is about to unload. The current path is UI-thread oriented. Raw
pointer hazards become relevant only if deregistration moves to another thread
without adding synchronization around the GUI-held active-agent handle.
## Device-tab integration
Plugins share the native Device tab with built-in agents. There is no
printer-agent API for adding custom Device-tab panels and no plugin-owned
`MachineObject` to populate directly.
Instead, the plugin supplies the same callbacks as any `IPrinterAgent`. Its
status messages must use the Bambu-shaped payload that `MachineObject` already
parses. If a required field is absent, the shared native UI shows its default
or incomplete state. A custom protocol is acceptable inside the plugin, but
its boundary with the app must perform this translation.
## Python calls, errors, and audit scope
The C++ trampoline acquires the Python GIL, invokes each pure virtual override,
logs a Python exception, and rethrows it. A missing override is a separate
C++ pure-virtual failure, not a logged Python traceback. Python construction
also bypasses the virtual trampoline, so the bridge logs a constructor failure
at the construction boundary.
Plugin-created threads need their own exception handling. An exception raised
there does not cross the C++ trampoline; it reaches Python's thread exception
handling and is recorded through redirected Python standard error.
The audit hook is defense in depth, not a sandbox. Current printer-agent
trampoline calls use loading audit mode. In that mode, normal reads are
allowed, only some file writes are checked against allowed roots, and many
operations are outside the policy, including network access and process
creation. Work that runs outside an active trampoline scope, including a
plugin-created thread, has no attributed plugin context and is allowed by
default. Do not treat this mechanism as permission to run untrusted code.
## Source locations
- `src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp`
- `src/slic3r/plugin/pluginTypes/printerAgent/`
`PrinterAgentPluginCapabilityTrampoline.hpp`
- `src/slic3r/Utils/NetworkAgentFactory.cpp`
- `resources/orca_plugins/BBLPrinterAgentPlugin.py`

View File

@@ -1,138 +0,0 @@
# Printing through printer agents
*Owns the send path: connection choices, preflight, upload, start, and the
two recovery flows. Defers filament mapping delivery to
[Filament synchronization](filament.md), which is a separate contract even
though it is applied at print time.*
This chapter describes the printer-agent send path. It is separate from
the older print-host implementation, even when both target Moonraker.
Keep the paths separate unless their contracts and failure handling can
be deliberately reconciled.
## Connection choices
Three connection paths are in use:
| Target | Connection path | Use |
| --- | --- | --- |
| Native Bambu | Custom TLS tunnel on port 6000 | Send and optional eMMC preflight |
| Bambu Python agent | Implicit FTPS on port 990 | Upload and Bambu preflight fallback |
| Moonraker family | HTTP | Upload and start print |
The Bambu connection paths are independent. Selecting one does not prove that
the other is available. The Moonraker agent uploads with a multipart
request to its `gcodes` storage and then starts the uploaded filename;
it does not reuse the legacy `Moonraker` print-host class.
## Bambu native tunnel
The native tunnel depends on the versioned networking DLL and its
file-transfer module. `InitFTModule()` is a single-owner initialization:
it rejects a second call. Any future shared initialization must therefore
be idempotent, while `BBLNetworkPlugin` remains the single teardown owner.
It must call `UnloadFTModule()` before freeing the DLL, otherwise the
module's function pointers can point into unloaded code.
There is currently an initialization gap: selecting a printer agent does
not initialize this module. It is initialized only when the
`installed_networking` option causes the native BBL network plugin to
initialize. Calls to the tunnel must continue to fail safely until that
path has initialized the module. The Send UI catches this failure and
reports an initialization error instead of letting an exception leave a
wx event handler.
## Bambu FTPS upload
The Python Bambu agent uses implicit FTPS on port 990. Its live upload
path closes the data connection, then waits at most two seconds for the
control response with `voidresp()`. A `TimeoutError` is accepted as a
completed transfer. An `error_reply` is also accepted when its reply
begins with `200`. This is the behavior to preserve.
Do not describe the path as using TLS `unwrap()`: the live construction
does not enable it. Enabling it without a bounded wait could hang while
waiting for the peer's TLS close notification. The current timeout-based
handling has not been verified on hardware against every printer and FTP
server combination.
## Print preflight and recovery
For normal LAN prints, `PrintJob` performs a preflight before the real
send. When eMMC is eligible it tries the native tunnel, then it sends a
small `verify_job` upload through the selected agent. The latter is a real
upload, not a special protocol command. Non-Bambu agents therefore upload
the probe too.
> **Do not re-enable eMMC by default** without hardware coverage for the
> affected devices. It is opt-in because the tunnel can hang during upload
> on some printers.
The whole send, including the thread hop and the recovery fork:
```mermaid
sequenceDiagram
participant UI as Send UI (UI thread)
participant J as PrintJob (worker)
participant A as Selected IPrinterAgent
participant P as Printer
UI->>J: Start send
opt eMMC eligible - off by default
J->>P: Native Bambu tunnel attempt
Note over J,P: Can hang on some printers
end
J->>A: verify_job preflight
Note over J,A: A real upload, not a protocol probe -<br/>non-Bambu agents upload it too
A->>P: Upload probe
alt Preflight and upload succeed
J->>A: Upload the real job
A->>P: Upload, then request print start
Note over A,P: The start response may stay open while<br/>the printer prepares - a timeout is not<br/>proof of failure, so check reported print state
A-->>UI: Result from the reported print state
else Upload fails
J-->>UI: Failure callback, marshalled to the UI thread
Note over UI: Re-resolve the machine here.<br/>Never reuse a machine pointer captured<br/>on the worker - agent or machine may have changed
alt Printer still connected
UI->>UI: Explain that storage upload failed
else Printer disconnected
UI->>UI: Open the IP or access-code flow
end
end
```
An upload failure and a disconnected printer need different recovery:
| Condition | UI response |
| --- | --- |
| Printer is still connected | Explain that storage upload failed. |
| Printer is disconnected | Open the IP or access-code flow. |
> **Do not retain a machine pointer from a worker callback.** The callback
> that chooses between these two outcomes runs on the UI thread and
> re-resolves the machine there, because the selected agent or machine can
> change first. The connection check is adequate for choosing the message,
> but is not a strong enough signal to authorize a reconnect.
## Moonraker upload and start
`MoonrakerPrinterAgent` uploads through Moonraker HTTP, then requests the
print start separately. The start endpoint may keep its response open
while the printer prepares the job. A timeout after that request is not
automatically proof that the start failed: the agent checks the reported
print state before deciding the result.
The legacy print-host Moonraker path implements its own upload and start
logic. It is not the agent path and should not be changed as an implicit
side effect of agent work.
## Maintenance checklist
- Test the selected connection path, not just another path on the same
printer.
- Preserve cancellation and progress callbacks across upload and start.
- Treat `verify_job` as an actual upload when estimating storage effects.
- Keep eMMC opt-in until its hanging behavior is resolved and verified.
- Keep the connected-upload-failure dialog distinct from the disconnected
recovery flow.

View File

@@ -1,137 +0,0 @@
# Printer-agent capability matrix
This is a compact lookup for the built-in Moonraker family. It combines
implementation state with recorded evidence. It is not a promise that every
firmware configuration behaves the same way. Python plug-in behavior depends
on the plug-in, not on this matrix.
Use [testing and troubleshooting](../testing.md) before calling a live-printer
result complete.
For a quicker tour of the controls users actually see, open the
[annotated Device-tab view](device-tab-annotations.html). The annotations
explain the important routing constraints; this matrix remains the compact
cross-agent reference.
## Status definitions
- Supported - implemented, with a relevant live-printer result recorded.
- Partial - an important condition, limitation, or defect applies.
- Unsupported - no applicable implementation, or deliberate refusal.
- Not verified - implemented or source-inspected, but without a relevant live
result.
`Base` means `MoonrakerPrinterAgent`. Qidi, Creality, and Snapmaker inherit
from it unless a row identifies an override.
## Connection and status
| Capability | Base | Qidi | Creality | Snapmaker |
| --- | --- | --- | --- | --- |
| Direct LAN connection with API key | Supported | Supported | Not verified | Not verified |
| WebSocket status updates | Supported | Supported | Not verified | Not verified |
| Reconnect and fresh status | Partial | Partial | Partial | Partial |
| Discovery and cloud binding | Unsupported | Unsupported | Unsupported | Unsupported |
| Device identity with a configured port | Partial | Partial | Partial | Partial |
- Reconnect completion can leave the Device tab with stale status.
- A bare IP and `host:port` can become separate device identities.
- The Base and Qidi Supported grades come from prior hardware sessions. They
were carried into this rewrite and not rerun.
## Controls
| Capability | Base | Qidi | Creality | Snapmaker |
| --- | --- | --- | --- | --- |
| Home and arbitrary G-code | Not verified | Not verified | Not verified | Not verified |
| Bed and nozzle temperature | Not verified | Not verified | Not verified | Not verified |
| Pause, resume, and cancel | Not verified | Not verified | Not verified | Not verified |
| Configured chamber light | Partial | Partial | Partial | Partial |
| Jog and manual extrusion | Partial | Partial | Not verified | Not verified |
| Legacy part-fan speed control | Partial | Partial | Not verified | Not verified |
| Structured fan, chamber, and AI controls | Unsupported | Unsupported | Unsupported | Unsupported |
| AMS RFID, calibration, and tray control | Unsupported | Unsupported | Unsupported | Unsupported |
- Chamber light needs a recognised light object.
- Base and Qidi jog works, but can leave relative positioning active. Do not
use it as a general safe-control test until its G-code state is restored.
- Base and Qidi part-fan control works through legacy `gcode_line` while
`is_enable_np` is false. Adding `cfg`, `fun`, `aux`, and `stat` flips that
flag and routes fan and extruder controls to unsupported structured commands.
- Creality and Snapmaker inherit the source path but have no separate live
evidence for jog or fan control.
## Printing
| Capability | Base | Qidi | Creality | Snapmaker |
| --- | --- | --- | --- | --- |
| Upload G-code without starting | Not verified | Not verified | Not verified | Not verified |
| Upload and start a local print | Not verified | Not verified | Not verified | Not verified |
| Mapped multi-material print | Unsupported | Not verified | Unsupported | Unsupported |
| Cloud or SD-card start variants | Unsupported | Partial | Unsupported | Unsupported |
| Cancel during upload | Not verified | Not verified | Not verified | Not verified |
| Send with no nozzle identity | Not applicable | Not verified | Not applicable | Not applicable |
- Qidi applies mapping before it routes the real local print path.
- Some Qidi print variants can reach base success stubs after mapping.
- Qidi tolerates missing nozzle data in source, but that Send preflight is not
hardware-verified.
## Filament
| Capability | Base | Qidi | Creality | Snapmaker |
| --- | --- | --- | --- | --- |
| Sync mode | Not verified | Not verified | Not verified | Not verified |
| Read installed material and slots | Partial | Not verified | Not verified | Not verified |
| Slot, material, and colour refresh | Unsupported | Not verified | Not verified | Not verified |
| Cleanup after removed material | Not verified | Not verified | Not verified | Not verified |
| Load, unload, or write a slot | Unsupported | Partial | Unsupported | Unsupported |
| Auto Refill | Unsupported | Unsupported | Unsupported | Unsupported |
- All built-in agents use pull-mode sync.
- Base reads Happy Hare or AFC data when present. Qidi has print-time mapping;
Creality has CFS logic; Snapmaker reads printer arrays and NFC data.
- The Base Device-tab slot refresh uses a proprietary AMS command and has no
generic Moonraker translation.
- Generic write-side macros stay unsupported until their printer contract is
known and verified.
## Camera
| Capability | Base | Qidi | Creality | Snapmaker |
| --- | --- | --- | --- | --- |
| Discover a Moonraker webcam | Not verified | Not verified | Not verified | Unsupported |
| Provide a camera source | Not verified | Not verified | Not verified | Not verified |
| Live camera view | Not verified | Not verified | Not verified | Not verified |
| Snapshot-only camera start | Unsupported | Unsupported | Unsupported | Not verified |
| Camera start renewal and teardown | Unsupported | Unsupported | Unsupported | Partial |
| Print thumbnail | Not verified | Not verified | Not verified | Not verified |
- Snapmaker bypasses webcam discovery with a local snapshot-polling page.
- Its renewal and teardown path has lifetime risks without live evidence.
- Moonraker thumbnail endpoint responses and filename-cache behavior need live
coverage.
## Python plug-ins
| Capability | Python plug-in agent |
| --- | --- |
| Registration and re-registration | Not verified - lifecycle tests cover replacement |
| Duplicate agent ID | Not verified - conflict is rejected and reported |
| Disable or unload | Not verified - deregistration is tested; session teardown needs coverage |
| Capability surface | Defined by the plug-in and exposed Python API |
## Reading the matrix safely
- `Partial` is not a softer form of `Supported`. It names a condition that must
be checked before use. `Not verified` means the code was found, but no
relevant live result is recorded.
- Pair each claim with the evidence grades in the testing guide. This matters
especially for CFS, Snapmaker camera, MMU macros, Qidi Send preflight, and
thumbnail endpoints.
## Background - deliberate exclusions
The matrix excludes Bambu-specific cloud binding, RFID, calibration, and
camera-control features from the Moonraker family. They use different protocol
contracts and are deliberately refused when no safe Klipper equivalent exists.

File diff suppressed because one or more lines are too long

View File

@@ -1,280 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Printer-agent manual checklist</title>
<style>
:root { color-scheme: light dark; --bg:#111827; --panel:#1f2937; --line:#4b5563;
--text:#f9fafb; --muted:#cbd5e1; --pass:#34d399; --fail:#f87171; --block:#fbbf24; }
* { box-sizing:border-box; }
body { max-width:960px; margin:0 auto; padding:24px; font:15px/1.5 system-ui,sans-serif;
color:var(--text); background:var(--bg); }
h1 { margin:0 0 4px; } h2 { margin-top:32px; } p, li { max-width:78ch; }
.muted { color:var(--muted); } .meta { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
gap:12px; margin:20px 0; } label { display:grid; gap:4px; }
input, textarea, button { font:inherit; } input, textarea { width:100%; padding:8px;
border:1px solid var(--line); border-radius:6px; color:inherit; background:var(--panel); }
textarea { min-height:58px; margin-top:8px; } .toolbar { display:flex; flex-wrap:wrap;
gap:8px; align-items:center; margin:16px 0; } button { border:1px solid var(--line);
border-radius:6px; padding:6px 10px; cursor:pointer; color:inherit; background:var(--panel); }
button[data-state="pass"].active { color:#062d1d; background:var(--pass); }
button[data-state="fail"].active { color:#3b0808; background:var(--fail); }
button[data-state="blocked"].active { color:#3b2600; background:var(--block); }
.progress { flex:1 1 240px; height:10px; overflow:hidden; border-radius:999px; background:var(--line); }
.progress > div { height:100%; width:0; background:var(--pass); transition:width .15s; }
.case { margin:10px 0; padding:12px; border:1px solid var(--line); border-left:4px solid var(--line);
border-radius:6px; background:var(--panel); } .case.pass { border-left-color:var(--pass); }
.case.fail { border-left-color:var(--fail); } .case.blocked { border-left-color:var(--block); }
.case-head { display:flex; justify-content:space-between; gap:12px; align-items:start; }
.case h3 { margin:0; font-size:1rem; } .actions { display:flex; gap:5px; white-space:nowrap; }
.restore-warning { padding:8px 10px; border:1px solid var(--block); border-radius:6px;
color:var(--block); background:var(--panel); }
code { overflow-wrap:anywhere; } @media (max-width:600px) { body { padding:16px; }
.meta { grid-template-columns:1fr; } .case-head { display:block; } .actions { margin-top:8px; } }
</style>
</head>
<body>
<h1>Printer-agent manual checklist</h1>
<p class="muted">Record what the printer did, not just what the UI displayed. Mark a case
blocked when the required printer, configuration, or safe test condition is unavailable.</p>
<section class="meta" aria-label="Test context">
<label>Agent and printer<input id="target" placeholder="for example: Moonraker - printer model"></label>
<label>Firmware and configuration<input id="environment" placeholder="firmware, MMU, camera, API key setup"></label>
<label>Build or package<input id="build" placeholder="version or build identifier"></label>
<label>Tester and date<input id="tester" placeholder="name and date"></label>
</section>
<div class="toolbar">
<strong id="summary">0 of 0 cases marked</strong>
<div class="progress" aria-label="Checklist progress"><div id="bar"></div></div>
<button id="export" type="button">Export Markdown</button>
<button id="reset" type="button">Reset checklist</button>
</div>
<p id="restore-warning" class="restore-warning" hidden>Saved checklist data could not be
restored. You can export the current blank checklist or use Reset to remove the saved data.</p>
<p class="muted">Use Pass only after observing the expected result. Fail needs enough evidence
to reproduce it. Include response text, log markers, or firmware behavior in the note.</p>
<section>
<h2>Connect and observe status</h2>
<div class="case" data-id="connect">
<div class="case-head"><div><h3>Connect to the selected printer</h3>
<p>Expected: the Device tab receives a fresh status update after connection. Do not use a
successful connection return alone as the result.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
<div class="case" data-id="status">
<div class="case-head"><div><h3>Observe live status changes</h3>
<p>Expected: temperature and target changes, fan state, print state, filename, progress,
elapsed time, and homing state reach the UI while the printer changes state.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
<div class="case" data-id="reconnect">
<div class="case-head"><div><h3>Disconnect and reconnect</h3>
<p>Expected: a second connection produces new status messages and does not create a duplicate
device. Record the post-reconnect status evidence.</p></div><div class="actions"></div></div><textarea placeholder="Evidence, result, or blocker"></textarea></div>
<div class="case" data-id="network-errors">
<div class="case-head"><div><h3>Handle network and response failures</h3>
<p>Expected: for discovery, status, G-code, upload, and print start, exercise controlled
HTTP 401, 404, and 500 responses, invalid JSON, and refused sockets. Each operation must
fail clearly or offer a retry, without a crash or a false success. Record the operation,
injected failure, UI result, and any retry.</p></div><div class="actions"></div></div><textarea placeholder="Operation, injected failure, UI result, retry, and evidence"></textarea></div>
</section>
<section>
<h2>Controls and printing</h2>
<div class="case" data-id="controls">
<div class="case-head"><div><h3>Exercise safe controls</h3>
<p>Expected: home, bed and nozzle temperature, and a harmless G-code command take effect on
the printer. Do not use Moonraker jog as a safe control test while it can leave relative
positioning active.</p></div><div class="actions"></div></div><textarea placeholder="Commands sent and printer-side result"></textarea></div>
<div class="case" data-id="fifo-order">
<div class="case-head"><div><h3>Verify queued command order under latency</h3>
<p>Expected: queue three harmless, uniquely marked commands while a proxy, network shaper,
or request log introduces or records latency. The printer-side log must show the markers in
the same order they were queued. Record the latency method and observed order.</p></div><div class="actions"></div></div><textarea placeholder="Queued markers, latency method, printer-side order, and result"></textarea></div>
<div class="case" data-id="send-only">
<div class="case-head"><div><h3>Send a file without starting it</h3>
<p>Expected: the file appears on the printer and no print starts.</p></div><div class="actions"></div></div><textarea placeholder="Filename and observed result"></textarea></div>
<div class="case" data-id="print">
<div class="case-head"><div><h3>Start a small print</h3>
<p>Expected: upload completes, the printer starts the selected file, and status transitions
to printing.</p></div><div class="actions"></div></div><textarea placeholder="Filename, response, and observed result"></textarea></div>
<div class="case" data-id="active-print-controls">
<div class="case-head"><div><h3>Pause, resume, and cancel an active print</h3>
<p>Expected: after the small print is actively printing, pause it and observe a paused state
on both printer and UI; resume it and observe printing again; then cancel it and observe the
printer stop and the UI leave the active or paused state.</p></div><div class="actions"></div></div><textarea placeholder="State transitions and printer-side result"></textarea></div>
<div class="case" data-id="print-failure">
<div class="case-head"><div><h3>Check upload failure handling</h3>
<p>Expected: cancellation during upload and a missing input fail clearly and do not begin a
partial or unintended print.</p></div><div class="actions"></div></div><textarea placeholder="Failure path and observed result"></textarea></div>
</section>
<section>
<h2>Filament, camera, and agent-specific checks</h2>
<div class="case" data-id="filament-read">
<div class="case-head"><div><h3>Refresh material-system data</h3>
<p>Expected: populated slots, empty slots, material, colour, and a live change are represented
correctly. Moonraker-family agents pull this data; do not expect a subscription callback.</p></div><div class="actions"></div></div><textarea placeholder="MMU or box configuration and observed result"></textarea></div>
<div class="case" data-id="filament-cleanup">
<div class="case-head"><div><h3>Clear material data when filament or the system is absent</h3>
<p>Expected: remove filament or disable the material system, refresh, and confirm the UI no
longer shows obsolete slots, material names, or colours.</p></div><div class="actions"></div></div><textarea placeholder="Change made, refresh evidence, and remaining or cleared data"></textarea></div>
<div class="case" data-id="filament-write">
<div class="case-head"><div><h3>Verify print-time mapping where supported</h3>
<p>Expected: only agents with a documented mapping implementation change printer-side mapping.
Do not attempt load, unload, or slot-setting macros unless their printer-specific contract is
known and safe.</p></div><div class="actions"></div></div><textarea placeholder="Mapping path, printer configuration, and result"></textarea></div>
<div class="case" data-id="qidi-nozzle-preflight">
<div class="case-head"><div><h3>Check Qidi Send preflight with missing nozzle identity</h3>
<p>Expected: on a Qidi agent and compatible single-nozzle slice, Send proceeds when the
Device tab has no reported nozzle diameter or type. It must not stop with
<code>PrintStatusNozzleDataInvalid</code>. Record any reported identity and any mismatch
result separately; this does not approve a known mismatch.</p></div><div class="actions"></div></div><textarea placeholder="Slice, reported nozzle data, preflight result, and printer-side result"></textarea></div>
<div class="case" data-id="camera">
<div class="case-head"><div><h3>Verify a camera feed</h3>
<p>Expected: frames advance and switching printers does not display a stale feed. For Snapmaker,
observe immediately before and after 300 seconds in one open view. The renewal result is
unknown until hardware evidence exists. Then swap agents and shut down the app to exercise
teardown around the detached callback's raw-<code>this</code> lifetime risk.</p></div><div class="actions"></div></div><textarea placeholder="Camera type, timestamps, agent swap or shutdown result, and evidence"></textarea></div>
<div class="case" data-id="thumbnail">
<div class="case-head"><div><h3>Verify the print thumbnail</h3>
<p>Expected: test a reused filename after its thumbnail changes, response payloads with both
<code>thumbnail_path</code> and <code>relative_path</code>, and a path below the G-code
root. The displayed image must match the current file in each case.</p></div><div class="actions"></div></div><textarea placeholder="Filename, endpoint key and path, displayed image, and observed result"></textarea></div>
<div class="case" data-id="plugin">
<div class="case-head"><div><h3>Reload a Python printer-agent plug-in</h3>
<p>Expected: the capability registers once, duplicate agent IDs are rejected visibly, and
disable or unload removes the agent cleanly.</p></div><div class="actions"></div></div><textarea placeholder="Plug-in identifier, actions, and observed result"></textarea></div>
</section>
<script>
const storageKey = 'orca-printer-agent-manual-checklist-v1';
const cases = [...document.querySelectorAll('.case')];
const inputs = [...document.querySelectorAll('input')];
const restoreWarning = document.querySelector('#restore-warning');
function emptyState() {
return { meta: {}, cases: {} };
}
function loadState() {
const saved = localStorage.getItem(storageKey);
if (!saved) return emptyState();
try {
const parsed = JSON.parse(saved);
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object' ||
(parsed.meta !== undefined && (Array.isArray(parsed.meta) || typeof parsed.meta !== 'object')) ||
(parsed.cases !== undefined && (Array.isArray(parsed.cases) || typeof parsed.cases !== 'object'))) {
throw new Error('incompatible saved checklist state');
}
return { meta: parsed.meta || {}, cases: parsed.cases || {} };
} catch (error) {
restoreWarning.hidden = false;
return emptyState();
}
}
const state = loadState();
function caseText(caseElement) {
return caseElement.querySelector('h3').textContent.trim();
}
function caseCriteria(caseElement) {
return [...caseElement.querySelectorAll('.case-head p')]
.map(paragraph => markdownParagraph(paragraph.textContent))
.filter(Boolean)
.join('\n\n');
}
function save() {
const data = { meta: {}, cases: {} };
inputs.forEach(input => { data.meta[input.id] = input.value; });
cases.forEach(item => {
data.cases[item.dataset.id] = { status: item.dataset.status || '', note: item.querySelector('textarea').value };
});
localStorage.setItem(storageKey, JSON.stringify(data));
updateProgress();
}
function renderStatus(item, status) {
item.dataset.status = status;
item.classList.toggle('pass', status === 'pass');
item.classList.toggle('fail', status === 'fail');
item.classList.toggle('blocked', status === 'blocked');
item.querySelectorAll('button[data-state]').forEach(button => {
button.classList.toggle('active', button.dataset.state === status);
});
}
function applyStatus(item, status) {
renderStatus(item, status);
save();
}
function updateProgress() {
const marked = cases.filter(item => item.dataset.status).length;
document.querySelector('#summary').textContent = `${marked} of ${cases.length} cases marked`;
document.querySelector('#bar').style.width = `${cases.length ? marked * 100 / cases.length : 0}%`;
}
function markdown() {
const meta = stateFromInputs();
const lines = ['# Printer-agent manual verification', '',
`Target: ${markdownInline(meta.target) || 'not recorded'}`,
`Firmware and configuration: ${markdownInline(meta.environment) || 'not recorded'}`,
`Build or package: ${markdownInline(meta.build) || 'not recorded'}`,
`Tester and date: ${markdownInline(meta.tester) || 'not recorded'}`, ''];
cases.forEach(item => {
const status = item.dataset.status || 'unmarked';
const note = markdownInline(item.querySelector('textarea').value);
lines.push(`## ${markdownInline(caseText(item))}`, '',
`Status: ${status}`, '',
'### Test criteria and expected result', '',
caseCriteria(item) || 'Not recorded', '',
'### Notes', '', note || 'None', '');
});
return lines.join('\n');
}
function markdownParagraph(value) {
return markdownInline(value);
}
function markdownInline(value) {
return String(value || '').replace(/\r\n?|\n/g, ' ').replace(/[\\`*_{}[\]<>#+!|]/g, '\\$&')
.replace(/\s+/g, ' ').trim();
}
function stateFromInputs() {
return Object.fromEntries(inputs.map(input => [input.id, input.value]));
}
function downloadMarkdown() {
const blob = new Blob([markdown()], { type: 'text/markdown;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'printer-agent-manual-verification.md';
link.click();
URL.revokeObjectURL(link.href);
}
cases.forEach(item => {
const actions = item.querySelector('.actions');
['pass', 'fail', 'blocked'].forEach(status => {
const button = document.createElement('button');
button.type = 'button';
button.dataset.state = status;
button.textContent = status[0].toUpperCase() + status.slice(1);
button.addEventListener('click', () => applyStatus(item, status));
actions.appendChild(button);
});
const saved = state.cases[item.dataset.id] || {};
item.querySelector('textarea').value = typeof saved.note === 'string' ? saved.note : '';
if (['pass', 'fail', 'blocked'].includes(saved.status)) renderStatus(item, saved.status);
item.querySelector('textarea').addEventListener('input', save);
});
inputs.forEach(input => {
input.value = typeof state.meta[input.id] === 'string' ? state.meta[input.id] : '';
input.addEventListener('input', save);
});
document.querySelector('#export').addEventListener('click', downloadMarkdown);
document.querySelector('#reset').addEventListener('click', () => {
if (!confirm('Clear all saved checklist data for this browser?')) return;
localStorage.removeItem(storageKey);
location.reload();
});
updateProgress();
</script>
</body>
</html>

View File

@@ -1,249 +0,0 @@
# Testing and troubleshooting printer agents
*Owns evidence grades, the automated and manual verification passes, and
the open-defect register. Every "not hardware-verified" note elsewhere in
this guide resolves to a grade defined here.*
This page describes how to assess a printer-agent change without treating
source inspection as a hardware result. Use the
[manual checklist](reference/manual-checklist.html) for a repeatable live-printer
pass, and use the [capability matrix](reference/capability-matrix.md) to decide
which cases apply to the agent being changed.
## Evidence grades
Keep these grades separate in reviews and release notes.
- Source-inspected - the current implementation was read. It establishes
intended behavior, not printer compatibility.
- Automated - a targeted test ran. It covers its inputs and assertions, not a
printer, firmware version, or network failure that it does not model.
- Hardware-verified - the stated behavior was observed on a named class of
live printer. Record the model, firmware, configuration, and result with the
test evidence.
Do not call a capability supported by hardware solely because the code compiles or
a unit test passes. Conversely, a hardware observation should not be generalized
to every Moonraker-family printer without checking its configuration.
## Carried hardware evidence
Prior hardware sessions verified direct Moonraker-family connection, live
WebSocket status, and jog on both a Qidi/Moonraker printer and a generic
Moonraker box. The jog moved the printer but could leave relative positioning
active. This evidence establishes the Moonraker-base and Qidi grades in the
capability matrix. It was carried into this rewrite and was not rerun here.
It does not establish those behaviors for Creality or Snapmaker, and it does
not cover Qidi-specific filament discovery, box mapping, or print wrappers.
## Build and automated tests
Run the smallest relevant test target first, then broaden the run if the change
crosses shared agent, plug-in, or Device-tab code. Set
`<configured-build-dir>` to the CMake build tree that was already configured for
the compiler, generator, and build type you intend to use. Do not replace it
with the source directory or assume a `build` subdirectory exists.
```powershell
cmake --build <configured-build-dir> --config RelWithDebInfo --target slic3rutils_tests
cmake --build <configured-build-dir> --config RelWithDebInfo --target printer_agent_plugin_tests
ctest --test-dir <configured-build-dir>/tests/libslic3r --output-on-failure
```
`--config RelWithDebInfo` is needed for multi-config generators such as Visual
Studio. Omit it only when the configured generator is single-config and its
build type was selected at configure time. Parallel-build options belong to the
generator: for example, pass `--parallel 6` to CMake when the generator
supports it, or use the generator's own trailing arguments only when that
generator documents them. Do not combine a changed working directory, a
generator-specific flag, and an assumed build-tree layout in one command.
On Windows, start from an MSVC developer environment. A shell without the MSVC
include paths can fail in dependencies before it compiles Orca code, with errors
such as `C1083: Cannot open include file: 'stddef.h'`, `'time.h'`, or `'cstdint'`.
Those signatures are environment failures, not evidence against the agent change.
If a machine exhausts MSVC precompiled-header memory, use the documented lower
parallelism command:
```powershell
cmake --build <configured-build-dir> --config RelWithDebInfo --target slic3rutils_tests --parallel 6
```
Errors such as `C3859: Failed to create virtual memory for PCH` and `C1076:
internal heap limit reached` are machine-specific resource failures. If a build
appears hung and file operations are blocked, inspect for idle `cl.exe` processes
holding locks before changing source.
Relevant automated coverage includes:
- `tests/slic3rutils/test_qidi_printer_agent.cpp` validates malformed and null
Qidi slot responses without throwing.
- `tests/slic3rutils/test_printer_agent.cpp` checks the public printer-agent
surface, including filament-sync mode exposure.
- `tests/slic3rutils/test_printer_agent_plugin.cpp` exercises plug-in
registration, replacement, conflict handling, and deregistration.
Do not present a historic test count, failure count, or skipped-test count as the
current state. Run the command above and attach its own output when a current
result is needed.
## Manual hardware verification
Use a small, disposable model and a printer that can safely accept the actions.
The checklist groups the work in the order below.
1. Confirm the printer accepts its configured URL and API key, then select it in
the Device tab. Verify a fresh status update, not merely a successful connect
return code.
2. Observe temperatures, targets, fan state, print state, filename, progress,
elapsed time, and axis homing while the printer changes state.
3. Exercise safe idle controls first: home, bed and nozzle temperature, and a
harmless G-code command. Verify the printer's action as well as the UI
response.
4. Send a small file without starting it, then start a small print. Once the
print is active, pause it, confirm the printer and UI both enter a paused
state, resume it, and confirm both return to printing. Cancel only after
observing an active or paused print, then confirm that the printer stops and
the UI leaves that state. Cancel an upload and retry a missing input so that
failure handling is observed too.
5. For Moonraker command workers, send three harmless, uniquely marked commands
while a proxy, network shaper, or request log introduces or records latency.
Pass only if the printer-side log records the markers in the same order they
were queued. Record the latency method and the observed order.
6. For a material system, verify populated slots, empty slots, material, colour,
refresh after a change, and cleanup when the system is absent or filament is
removed. The latter must remove obsolete slot or material data from the UI.
Do not infer write support from read support.
7. For Qidi, use a compatible single-nozzle slice and Send it while the Device
tab has no reported nozzle diameter or type. Pass only if Send proceeds past
preflight without `PrintStatusNozzleDataInvalid`; record any reported
diameter/type and any mismatch message separately. This checks the intended
tolerance for missing identity data, not that a mismatched known nozzle is
safe.
8. Verify the camera only on hardware that advertises or implements it. Check
that frames advance, switching printers starts the newly selected camera, and
closing or changing the view does not leave misleading stale output. For
Snapmaker, also test immediately before and after 300 seconds in an
uninterrupted view. The expected renewal result is unknown until hardware
evidence exists. Swap agents and shut down the app after the camera cases to
exercise teardown around the detached callback's raw-`this` lifetime risk.
9. For Moonraker thumbnails, test a reused filename after its thumbnail changes,
responses that use `thumbnail_path` and `relative_path`, and a thumbnail in a
subdirectory below the G-code root. Record the endpoint payload and displayed
image for each case.
10. Disconnect and reconnect the printer, then confirm that new status messages
still reach the UI. A reconnect completion alone is insufficient evidence.
11. Test network and response failures for discovery, status, G-code, upload,
and print start. For each operation, exercise HTTP 401, 404, and 500,
invalid JSON, and a refused socket with a controlled proxy or test server.
Each case must fail clearly or offer a retry, without a crash or a false
success. Record the operation, injected failure, UI result, and any retry.
For Moonraker, record whether the thumbnail endpoint returns the response shape
the agent expects. That response has not yet been verified across a live
Moonraker deployment.
## Troubleshooting by symptom
### Connection appears successful but the Device tab stays stale
Treat status freshness as the connection result. Enable `ORCA_NETWORK_DEBUG` and
look for a new `parse_json: dev_id=` entry after the connection or reconnection.
The unresolved reconnect-delivery problem can complete the second connection
without delivering any new parsed messages. Capture an instrumented second
connection before changing dispatch or message-delay logic, because both remain
plausible causes.
Check identity too. One path can use a bare IP address while another uses
`host:port`; configuring a port can therefore create two machine objects. Do not
diagnose a duplicate as a printer-agent failure until the identities are
compared.
### A control reports success but the printer did not change
First establish that the command has a documented translation in the capability
matrix. Unsupported commands are deliberately rejected rather than silently
accepted. For Moonraker, queued controls are asynchronous, so wait for the
printer-side result and capture the request or log before concluding it was lost.
Moonraker jog is a special case. The current path can leave the printer in
relative positioning mode after a jog. Do not use it as a general verification
control until it is changed to save state, issue `G91` and the move, then restore
state with `SAVE_GCODE_STATE` and `RESTORE_GCODE_STATE`. Extruder-relative moves
use a separate `M83` path.
### A thumbnail is missing or belongs to an earlier print
The thumbnail lookup accepts both `thumbnail_path` and `relative_path`, but the
live endpoint response is not yet verified. The cache is keyed by filename, so
reusing a common name can retain the previous image. Test a distinct filename
before changing the lookup. Paths below the G-code root also need live coverage
for the `relative_path` fallback.
### Filament looks stale, blank, or does not follow an edit
Moonraker-family agents use pull-mode filament sync. Verify the pull request and
the resulting Device-tab update rather than expecting a subscription callback.
Read-side discovery does not establish load, unload, slot-setting, or Auto Refill
support. Happy Hare and AFC macro names are printer-side configuration; do not
guess them. A guessed macro can silently do nothing or issue the wrong action.
### A Python agent disappears or cannot be enabled
Check its agent ID first. A duplicate ID is rejected and the conflicting
capability is disabled rather than auto-promoted later, because automatic
promotion could change the active printer implementation without user intent.
Reload and teardown also need a live check: registration tests cover lifecycle
logic, but a plug-in can still be exposed to API drift or a teardown race in a
real session.
## Known defects and safeguards
- Reconnect delivery remains unresolved. Instrument the second connection before
attempting a fix; the observed failure is stale data after a completed reconnect.
- Moonraker jog can leave relative mode active. Keep the future state-save and
restore sequence together so the jog cannot affect later G-code positioning.
- Qidi's nozzle-data Send-preflight tolerance for unreported diameter and type
has not received a hardware verification.
- A configured `host:port` can coexist with a bare-IP machine identity. This can
duplicate devices and confuse selection.
- Moonraker thumbnail caching can show an old image when a filename is reused.
- Moonraker filament data can be stale, and its pull/read path does not provide
safe generic write-side MMU operations.
- Duplicate plug-in agent IDs are rejected. There is no automatic fallback to a
losing capability after the winner unloads.
- Plug-in implementations can drift from the Python printer-agent API. Treat an
import or interface error as a plug-in compatibility issue until proved otherwise.
- Snapmaker camera callbacks can outlive their view during agent replacement or
shutdown because the detached path retains a raw `this` pointer. Treat a crash
or stale callback during those transitions as a source-derived use-after-free
risk until the lifetime is made explicit.
- Snapmaker writes an IP-specific local camera HTML file below the application
cache. The source contains no cleanup path, so residual files can accumulate
for each unique printer IP. This is source-derived and was not reproduced.
## Not yet hardware-verified
- Moonraker command-worker FIFO behavior under recorded or injected network
latency.
- Moonraker thumbnail responses: reused filenames, `thumbnail_path`,
`relative_path`, and subdirectory paths.
- Snapmaker camera behavior on a live U1: frames, renewal across a long-open
view including the 300-second boundary, switching between printers, agent
replacement, and shutdown teardown.
- Whether Snapmaker's renewal cadence prevents a stale-frame interval. Do not
shorten it as a workaround without resolving the printer-side behavior first.
- Creality CFS detection and preset scoring on a real printer.
- Qidi Send preflight when firmware omits nozzle diameter and type.
- Moonraker-family write-side MMU commands. They remain blocked on verified,
printer-specific macro contracts.
## Background - source locations for maintainers
The Moonraker command worker, status stream, print path, and thumbnail
lookup live in `MoonrakerPrinterAgent`. Qidi maps its material box before routing
to the Moonraker base. Snapmaker adds the camera start request and its snapshot
page. Python agent registration and conflict handling live in
`NetworkAgentFactory`.

File diff suppressed because it is too large Load Diff

View File

@@ -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)}")

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,13 @@
{
"name": "Custom Printer",
"version": "02.04.00.01",
"version": "02.04.00.04",
"force_update": "0",
"description": "My configurations",
"machine_model_list": [
{
"name": "Generic Belt Printer",
"sub_path": "machine/MyBeltPrinter.json"
},
{
"name": "Generic Klipper Printer",
"sub_path": "machine/MyKlipper.json"
@@ -62,6 +66,14 @@
"name": "0.16mm Optimal @MyKlipper",
"sub_path": "process/0.16mm Optimal @MyKlipper.json"
},
{
"name": "0.12mm Fine @MyBeltPrinter",
"sub_path": "process/0.12mm Fine @MyBeltPrinter.json"
},
{
"name": "0.20mm Standard @MyBeltPrinter",
"sub_path": "process/0.20mm Standard @MyBeltPrinter.json"
},
{
"name": "0.20mm Standard @MyKlipper",
"sub_path": "process/0.20mm Standard @MyKlipper.json"
@@ -262,18 +274,38 @@
"name": "MyKlipper 0.8 nozzle",
"sub_path": "machine/MyKlipper 0.8 nozzle.json"
},
{
"name": "fdm_belt_common",
"sub_path": "machine/fdm_belt_common.json"
},
{
"name": "fdm_toolchanger_common",
"sub_path": "machine/fdm_toolchanger_common.json"
},
{
"name": "MyRepetier 0.4 nozzle",
"sub_path": "machine/MyRepetier 0.4 nozzle.json"
},
{
"name": "MyRRF 0.4 nozzle",
"sub_path": "machine/MyRRF 0.4 nozzle.json"
},
{
"name": "MyBeltPrinter 0.2 nozzle",
"sub_path": "machine/MyBeltPrinter 0.2 nozzle.json"
},
{
"name": "MyBeltPrinter 0.4 nozzle",
"sub_path": "machine/MyBeltPrinter 0.4 nozzle.json"
},
{
"name": "MyBeltPrinter 0.6 nozzle",
"sub_path": "machine/MyBeltPrinter 0.6 nozzle.json"
},
{
"name": "MyBeltPrinter 0.8 nozzle",
"sub_path": "machine/MyBeltPrinter 0.8 nozzle.json"
},
{
"name": "MyRepetier 0.4 nozzle",
"sub_path": "machine/MyRepetier 0.4 nozzle.json"
},
{
"name": "MyToolChanger 0.2 nozzle",
"sub_path": "machine/MyToolChanger 0.2 nozzle.json"

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,27 @@
{
"type": "machine",
"name": "MyBeltPrinter 0.2 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "3w1uyJdmm14QhDnH",
"instantiation": "true",
"printer_model": "Generic Belt Printer",
"default_print_profile": "0.12mm Fine @MyBeltPrinter",
"nozzle_diameter": [
"0.2"
],
"max_layer_height": [
"0.16"
],
"min_layer_height": [
"0.04"
],
"printer_variant": "0.2",
"printable_area": [
"0x0",
"350x0",
"350x350",
"0x350"
],
"printable_height": "300"
}

View File

@@ -0,0 +1,20 @@
{
"type": "machine",
"name": "MyBeltPrinter 0.4 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "6nRHUtvJOUffocbu",
"instantiation": "true",
"printer_model": "Generic Belt Printer",
"nozzle_diameter": [
"0.4"
],
"printer_variant": "0.4",
"printable_area": [
"0x0",
"350x0",
"350x350",
"0x350"
],
"printable_height": "300"
}

View File

@@ -0,0 +1,26 @@
{
"type": "machine",
"name": "MyBeltPrinter 0.6 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "K0m9HbUNwKT4UCJV",
"instantiation": "true",
"printer_model": "Generic Belt Printer",
"nozzle_diameter": [
"0.6"
],
"max_layer_height": [
"0.4"
],
"min_layer_height": [
"0.12"
],
"printer_variant": "0.6",
"printable_area": [
"0x0",
"350x0",
"350x350",
"0x350"
],
"printable_height": "300"
}

View File

@@ -0,0 +1,26 @@
{
"type": "machine",
"name": "MyBeltPrinter 0.8 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "rHAweDz4eNwttPNA",
"instantiation": "true",
"printer_model": "Generic Belt Printer",
"nozzle_diameter": [
"0.8"
],
"max_layer_height": [
"0.6"
],
"min_layer_height": [
"0.2"
],
"printer_variant": "0.8",
"printable_area": [
"0x0",
"350x0",
"350x350",
"0x350"
],
"printable_height": "300"
}

View File

@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "Generic Belt Printer",
"model_id": "my_belt_01",
"nozzle_diameter": "0.4;0.2;0.6;0.8",
"machine_tech": "FFF",
"family": "MyPrinter",
"bed_model": "Custom_350_bed.stl",
"bed_texture": "orcaslicer_bed_texture.svg",
"hotend_model": "",
"default_materials": "Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System"
}

View File

@@ -0,0 +1,99 @@
{
"type": "machine",
"name": "fdm_belt_common",
"inherits": "fdm_klipper_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"single_extruder_multi_material": "0",
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @MyBeltPrinter",
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"deretraction_speed": [
"30"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"long_retractions_when_cut": [
"0"
],
"nozzle_diameter": [
"0.4"
],
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0"
],
"retract_lift_below": [
"0"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"0.8"
],
"retraction_minimum_travel": [
"1"
],
"retraction_speed": [
"30"
],
"travel_slope": [
"3"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"0.4"
],
"z_hop_types": [
"Normal Lift"
],
"gcode_remap_x": "rev_x",
"gcode_remap_y": "pos_z",
"gcode_remap_z": "pos_y",
"printer_extruder_id": [
"1"
],
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"belt_slice_rotation_global": "1",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
"auxiliary_fan": "0"
}

View File

@@ -0,0 +1,20 @@
{
"type": "process",
"name": "0.12mm Fine @MyBeltPrinter",
"inherits": "fdm_process_klipper_common",
"from": "system",
"setting_id": "EugqqdLJ423bgEwN",
"instantiation": "true",
"layer_height": "0.12",
"initial_layer_print_height": "0.12",
"bottom_shell_layers": "5",
"top_shell_layers": "6",
"support_top_z_distance": "0.08",
"support_bottom_z_distance": "0.08",
"skirt_loops": "0",
"skirt_distance": "0",
"compatible_printers": [
"MyBeltPrinter 0.2 nozzle",
"MyBeltPrinter 0.4 nozzle"
]
}

View File

@@ -0,0 +1,17 @@
{
"type": "process",
"name": "0.20mm Standard @MyBeltPrinter",
"inherits": "fdm_process_klipper_common",
"from": "system",
"setting_id": "YzCDAgH3uLOM53pF",
"instantiation": "true",
"layer_height": "0.2",
"initial_layer_print_height": "0.2",
"skirt_loops": "0",
"skirt_distance": "0",
"compatible_printers": [
"MyBeltPrinter 0.4 nozzle",
"MyBeltPrinter 0.6 nozzle",
"MyBeltPrinter 0.8 nozzle"
]
}

View File

@@ -0,0 +1,54 @@
{
"name": "IdeaFormer",
"version": "02.00.00.03",
"force_update": "0",
"description": "IdeaFormer belt printer configurations",
"machine_model_list": [
{
"name": "IdeaFormer IR3 V2",
"sub_path": "machine/IdeaFormer IR3 V2.json"
}
],
"process_list": [
{
"name": "fdm_process_common",
"sub_path": "process/fdm_process_common.json"
},
{
"name": "0.20mm Standard @IdeaFormer IR3 V2",
"sub_path": "process/0.20mm Standard @IdeaFormer IR3 V2.json"
}
],
"filament_list": [
{
"name": "Generic PLA @IdeaFormer IR3 V2",
"sub_path": "filament/Generic PLA @IdeaFormer IR3 V2.json"
},
{
"name": "eSUN PLA @IdeaFormer IR3 V2",
"sub_path": "filament/eSUN PLA @IdeaFormer IR3 V2.json"
},
{
"name": "Generic PETG @IdeaFormer IR3 V2",
"sub_path": "filament/Generic PETG @IdeaFormer IR3 V2.json"
}
],
"machine_list": [
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_klipper_common",
"sub_path": "machine/fdm_klipper_common.json"
},
{
"name": "fdm_belt_common",
"sub_path": "machine/fdm_belt_common.json"
},
{
"name": "IdeaFormer IR3 V2 0.4 nozzle",
"sub_path": "machine/IdeaFormer IR3 V2 0.4 nozzle.json"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View File

@@ -0,0 +1,113 @@
{
"type": "filament",
"name": "Generic PETG @IdeaFormer IR3 V2",
"inherits": "Generic PETG @System",
"from": "system",
"setting_id": "n4zaXcUUzTqAxq5f",
"instantiation": "true",
"compatible_printers": [
"IdeaFormer IR3 V2 0.4 nozzle"
],
"filament_type": [
"PETG"
],
"filament_vendor": [
"Generic"
],
"filament_settings_id": [
"Generic PETG @IdeaFormer IR3 V2"
],
"filament_diameter": [
"1.75"
],
"filament_density": [
"1.27"
],
"filament_flow_ratio": [
"0.95"
],
"filament_cost": [
"25"
],
"filament_max_volumetric_speed": [
"10"
],
"nozzle_temperature": [
"240"
],
"nozzle_temperature_initial_layer": [
"245"
],
"nozzle_temperature_range_low": [
"220"
],
"nozzle_temperature_range_high": [
"260"
],
"temperature_vitrification": [
"70"
],
"hot_plate_temp": [
"80"
],
"hot_plate_temp_initial_layer": [
"80"
],
"cool_plate_temp": [
"80"
],
"cool_plate_temp_initial_layer": [
"80"
],
"textured_plate_temp": [
"80"
],
"textured_plate_temp_initial_layer": [
"80"
],
"fan_min_speed": [
"40"
],
"fan_max_speed": [
"60"
],
"overhang_fan_threshold": [
"25%"
],
"overhang_fan_speed": [
"80"
],
"close_fan_the_first_x_layers": [
"3"
],
"full_fan_speed_layer": [
"8"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"4"
],
"fan_cooling_layer_time": [
"100"
],
"reduce_fan_stop_start_freq": [
"1"
],
"filament_retraction_length": [
"2"
],
"filament_retraction_speed": [
"40"
],
"filament_deretraction_speed": [
"40"
],
"filament_z_hop": [
"0.4"
],
"filament_start_gcode": [
"; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C"
]
}

View File

@@ -0,0 +1,113 @@
{
"type": "filament",
"name": "Generic PLA @IdeaFormer IR3 V2",
"inherits": "Generic PLA @System",
"from": "system",
"setting_id": "1xjycsEAFh6KQIhp",
"instantiation": "true",
"compatible_printers": [
"IdeaFormer IR3 V2 0.4 nozzle"
],
"filament_type": [
"PLA"
],
"filament_vendor": [
"Generic"
],
"filament_settings_id": [
"Generic PLA @IdeaFormer IR3 V2"
],
"filament_diameter": [
"1.75"
],
"filament_density": [
"1.24"
],
"filament_flow_ratio": [
"0.98"
],
"filament_cost": [
"20"
],
"filament_max_volumetric_speed": [
"12"
],
"nozzle_temperature": [
"215"
],
"nozzle_temperature_initial_layer": [
"220"
],
"nozzle_temperature_range_low": [
"190"
],
"nozzle_temperature_range_high": [
"240"
],
"temperature_vitrification": [
"45"
],
"hot_plate_temp": [
"75"
],
"hot_plate_temp_initial_layer": [
"75"
],
"cool_plate_temp": [
"75"
],
"cool_plate_temp_initial_layer": [
"75"
],
"textured_plate_temp": [
"75"
],
"textured_plate_temp_initial_layer": [
"75"
],
"fan_min_speed": [
"100"
],
"fan_max_speed": [
"100"
],
"overhang_fan_threshold": [
"50%"
],
"overhang_fan_speed": [
"100"
],
"close_fan_the_first_x_layers": [
"3"
],
"full_fan_speed_layer": [
"8"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"4"
],
"fan_cooling_layer_time": [
"100"
],
"reduce_fan_stop_start_freq": [
"1"
],
"filament_retraction_length": [
"1.5"
],
"filament_retraction_speed": [
"35"
],
"filament_deretraction_speed": [
"30"
],
"filament_z_hop": [
"0.4"
],
"filament_start_gcode": [
"; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C"
]
}

View File

@@ -0,0 +1,35 @@
{
"type": "filament",
"name": "eSUN PLA @IdeaFormer IR3 V2",
"inherits": "Generic PLA @IdeaFormer IR3 V2",
"from": "system",
"setting_id": "XqkviBmFHEglXueX",
"instantiation": "true",
"compatible_printers": [
"IdeaFormer IR3 V2 0.4 nozzle"
],
"filament_type": [
"PLA"
],
"filament_vendor": [
"eSUN"
],
"filament_settings_id": [
"eSUN PLA @IdeaFormer IR3 V2"
],
"nozzle_temperature_initial_layer": [
"200"
],
"nozzle_temperature": [
"200"
],
"enable_pressure_advance": [
"1"
],
"pressure_advance": [
"0.12"
],
"filament_max_volumetric_speed": [
"20"
]
}

View File

@@ -0,0 +1,94 @@
{
"type": "machine",
"name": "IdeaFormer IR3 V2 0.4 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "MDQZgwRgg72lmjtu",
"instantiation": "true",
"printer_model": "IdeaFormer IR3 V2",
"printer_variant": "0.4",
"nozzle_diameter": [
"0.4"
],
"printable_area": [
"0x0",
"250x0",
"250x2000",
"0x2000"
],
"printable_height": "250",
"belt_printer_infinite_y": "1",
"thumbnails": [
"48x48/PNG",
"300x300/PNG"
],
"default_filament_profile": [
"Generic PLA @IdeaFormer IR3 V2"
],
"default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2",
"use_relative_e_distances": "1",
"machine_max_acceleration_e": [
"5000"
],
"machine_max_acceleration_extruding": [
"5000"
],
"machine_max_acceleration_retracting": [
"1000"
],
"machine_max_acceleration_travel": [
"9000"
],
"machine_max_acceleration_x": [
"5000"
],
"machine_max_acceleration_y": [
"5000"
],
"machine_max_acceleration_z": [
"100"
],
"machine_max_jerk_e": [
"2.5"
],
"machine_max_jerk_x": [
"10"
],
"machine_max_jerk_y": [
"10"
],
"machine_max_jerk_z": [
"0.4"
],
"machine_max_speed_e": [
"60"
],
"machine_max_speed_x": [
"500"
],
"machine_max_speed_y": [
"500"
],
"machine_max_speed_z": [
"20"
],
"retraction_length": [
"2"
],
"retraction_speed": [
"40"
],
"deretraction_speed": [
"40"
],
"z_hop": [
"0.4"
],
"retract_lift_below": [
"300"
],
"machine_start_gcode": "; === IdeaFormer IR3 V2 Belt Printer Start ===\n; Axes: X=lateral, Y=gantry height (probe), Z=belt\nG90 ; absolute positioning\nM82 ; absolute extruder\nG21 ; millimeters\nG28 ; home all axes\nG1 Y20 F500 ; lift nozzle 20mm from belt\n; Bed + hotend temps come from the active filament profile. Belt PLA requires 75 C bed — use Generic/eSun PLA @IdeaFormer IR3 V2 filament presets to get it automatically.\nM140 S[hot_plate_temp_initial_layer] ; set bed temp\nM104 S[nozzle_temperature_initial_layer] ; hotend temp\nM109 S[nozzle_temperature_initial_layer] ; wait hotend\nM190 S[hot_plate_temp_initial_layer] ; wait bed\n; --- Purge blob ---\nG92 E0 ; zero extruder\nG1 Y.1 ; nozzle 0.1mm above belt\nG1 E15 F1000 ; purge 15mm blob\nG1 Z20 E25 F800 ; belt advance 20mm + extrude\nG1 E23 ; retract 2mm\nG28 Y ; re-probe belt surface\nG1 E25 ; de-retract\n; --- Prime lines (full 250mm bed width) ---\nFMS_on ; filament motion sensor\nG1 X250 E50 F2000 ; prime line 1\nG92 Z0 ; reset belt origin\nG1 Z.4 ; belt advance 0.4mm\nG1 X0 E75 ; prime line 2\nG1 F1000 ; default feedrate\nG92 E0 Z0 ; zero extruder + belt = print origin\n",
"machine_end_gcode": "; === IdeaFormer IR3 V2 Belt Printer End ===\nM400 ; wait for moves to finish\nM104 S0 ; heater off\nM140 S0 ; bed off\nG92 E0 ; zero extruder\nG1 E-5 F300 ; retract 5mm\nG4 P5000 ; wait for ooze\nG91 ; relative mode - keep every end move relative on a belt\nG1 Y20 F1000 ; raise gantry 20mm for clearance over the part\nG1 Z676 F3000 ; advance belt one full machine-depth to eject the part and clean the belt\nG90 ; back to absolute\nG28 X ; home X only - NEVER 'G28' all: that homes Z/belt and reverses the whole print back into the gantry\nFMS_off ; filament motion sensor off\nBED_MESH_CLEAR\nM84 ; disable motors\n",
"machine_pause_gcode": "PAUSE",
"layer_change_gcode": "G92 E0 ; belt: reset extruder at layer change (relative E)"
}

View File

@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "IdeaFormer IR3 V2",
"model_id": "IdeaFormer_IR3_V2",
"nozzle_diameter": "0.4",
"machine_tech": "FFF",
"family": "IdeaFormer",
"bed_model": "",
"bed_texture": "",
"hotend_model": "",
"default_materials": "Generic PLA @IdeaFormer IR3 V2;Generic PETG @IdeaFormer IR3 V2"
}

View File

@@ -0,0 +1,99 @@
{
"type": "machine",
"name": "fdm_belt_common",
"inherits": "fdm_klipper_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"single_extruder_multi_material": "0",
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2",
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"deretraction_speed": [
"30"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"long_retractions_when_cut": [
"0"
],
"nozzle_diameter": [
"0.4"
],
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0"
],
"retract_lift_below": [
"0"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"0.8"
],
"retraction_minimum_travel": [
"1"
],
"retraction_speed": [
"30"
],
"travel_slope": [
"3"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"0.4"
],
"z_hop_types": [
"Normal Lift"
],
"gcode_remap_x": "rev_x",
"gcode_remap_y": "pos_z",
"gcode_remap_z": "pos_y",
"printer_extruder_id": [
"1"
],
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"belt_slice_rotation_global": "1",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
"auxiliary_fan": "0"
}

View File

@@ -0,0 +1,141 @@
{
"type": "machine",
"name": "fdm_klipper_common",
"inherits": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"machine_max_acceleration_e": [
"5000",
"5000"
],
"machine_max_acceleration_extruding": [
"20000",
"20000"
],
"machine_max_acceleration_retracting": [
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"20000",
"20000"
],
"machine_max_acceleration_x": [
"20000",
"20000"
],
"machine_max_acceleration_y": [
"20000",
"20000"
],
"machine_max_acceleration_z": [
"500",
"200"
],
"machine_max_speed_e": [
"25",
"25"
],
"machine_max_speed_x": [
"500",
"200"
],
"machine_max_speed_y": [
"500",
"200"
],
"machine_max_speed_z": [
"12",
"12"
],
"machine_max_jerk_e": [
"2.5",
"2.5"
],
"machine_max_jerk_x": [
"9",
"9"
],
"machine_max_jerk_y": [
"9",
"9"
],
"machine_max_jerk_z": [
"0.2",
"0.4"
],
"machine_min_extruding_rate": [
"0",
"0"
],
"machine_min_travel_rate": [
"0",
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"printer_settings_id": "",
"printer_technology": "FFF",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"1"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"0.8"
],
"retract_length_toolchange": [
"2"
],
"z_hop": [
"0.4"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"30"
],
"deretraction_speed": [
"30"
],
"z_hop_types": "Normal Lift",
"silent_mode": "0",
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @MyKlipper",
"bed_exclude_area": [
"0x0"
],
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n",
"machine_end_gcode": "PRINT_END",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "PAUSE",
"scan_first_layer": "0",
"nozzle_type": "undefine",
"auxiliary_fan": "0"
}

View File

@@ -0,0 +1,119 @@
{
"type": "machine",
"name": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"printer_technology": "FFF",
"deretraction_speed": [
"40"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"gcode_flavor": "marlin",
"silent_mode": "0",
"machine_max_acceleration_e": [
"5000"
],
"machine_max_acceleration_extruding": [
"10000"
],
"machine_max_acceleration_retracting": [
"1000"
],
"machine_max_acceleration_x": [
"10000"
],
"machine_max_acceleration_y": [
"10000"
],
"machine_max_acceleration_z": [
"500"
],
"machine_max_speed_e": [
"60"
],
"machine_max_speed_x": [
"500"
],
"machine_max_speed_y": [
"500"
],
"machine_max_speed_z": [
"10"
],
"machine_max_jerk_e": [
"5"
],
"machine_max_jerk_x": [
"8"
],
"machine_max_jerk_y": [
"8"
],
"machine_max_jerk_z": [
"0.4"
],
"machine_min_extruding_rate": [
"0"
],
"machine_min_travel_rate": [
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"nozzle_diameter": [
"0.4"
],
"printer_settings_id": "",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"2"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"1"
],
"retract_length_toolchange": [
"1"
],
"z_hop": [
"0"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"60"
],
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_print_profile": "",
"machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up",
"machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "M601"
}

View File

@@ -0,0 +1,23 @@
{
"type": "process",
"name": "0.20mm Standard @IdeaFormer IR3 V2",
"inherits": "fdm_process_common",
"from": "system",
"setting_id": "91atcIwv5728phqX",
"instantiation": "true",
"layer_height": "0.2",
"initial_layer_print_height": "0.2",
"initial_layer_line_width": "0.42",
"wall_loops": "2",
"reduce_infill_retraction": "1",
"detect_overhang_wall": "1",
"skirt_loops": "0",
"skirt_distance": "0",
"sparse_infill_pattern": "grid",
"sparse_infill_speed": "200",
"support_base_pattern": "rectilinear",
"support_interface_pattern": "rectilinear",
"compatible_printers": [
"IdeaFormer IR3 V2 0.4 nozzle"
]
}

View File

@@ -0,0 +1,108 @@
{
"type": "process",
"name": "fdm_process_common",
"from": "system",
"instantiation": "false",
"adaptive_layer_height": "0",
"reduce_crossing_wall": "0",
"max_travel_detour_distance": "0",
"bottom_surface_pattern": "monotonic",
"bottom_shell_thickness": "0",
"bridge_speed": "50",
"brim_width": "5",
"brim_object_gap": "0.1",
"compatible_printers": [],
"compatible_printers_condition": "",
"print_sequence": "by layer",
"default_acceleration": "1000",
"initial_layer_acceleration": "500",
"top_surface_acceleration": "1000",
"travel_acceleration": "1000",
"inner_wall_acceleration": "1000",
"outer_wall_acceleration": "700",
"bridge_no_support": "0",
"draft_shield": "disabled",
"elefant_foot_compensation": "0",
"enable_arc_fitting": "0",
"wall_infill_order": "inner wall/outer wall/infill",
"infill_direction": "45",
"sparse_infill_density": "15%",
"sparse_infill_pattern": "crosshatch",
"initial_layer_print_height": "0.2",
"infill_combination": "0",
"infill_wall_overlap": "25%",
"interface_shells": "0",
"ironing_flow": "10%",
"ironing_spacing": "0.15",
"ironing_speed": "30",
"ironing_type": "no ironing",
"reduce_infill_retraction": "1",
"filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode",
"detect_overhang_wall": "1",
"slowdown_for_curled_perimeters": "1",
"overhang_1_4_speed": "0",
"overhang_2_4_speed": "50",
"overhang_3_4_speed": "30",
"overhang_4_4_speed": "10",
"line_width": "110%",
"inner_wall_line_width": "110%",
"outer_wall_line_width": "100%",
"top_surface_line_width": "93.75%",
"sparse_infill_line_width": "110%",
"initial_layer_line_width": "120%",
"internal_solid_infill_line_width": "120%",
"support_line_width": "96%",
"wall_loops": "3",
"print_settings_id": "",
"raft_layers": "0",
"seam_position": "aligned",
"skirt_distance": "2",
"skirt_height": "3",
"min_skirt_length": "4",
"skirt_loops": "0",
"minimum_sparse_infill_area": "15",
"spiral_mode": "0",
"standby_temperature_delta": "-5",
"enable_support": "0",
"resolution": "0.012",
"support_type": "normal(auto)",
"support_on_build_plate_only": "0",
"support_top_z_distance": "0.2",
"support_bottom_z_distance": "0.2",
"support_filament": "0",
"support_interface_loop_pattern": "0",
"support_interface_filament": "0",
"support_interface_top_layers": "2",
"support_interface_bottom_layers": "2",
"support_interface_spacing": "0.5",
"support_interface_speed": "80",
"support_base_pattern": "default",
"support_base_pattern_spacing": "2.5",
"support_speed": "150",
"support_threshold_angle": "30",
"support_object_xy_distance": "0.35",
"tree_support_branch_angle": "30",
"tree_support_wall_count": "0",
"tree_support_with_infill": "0",
"detect_thin_wall": "0",
"top_surface_pattern": "monotonicline",
"top_shell_thickness": "0.8",
"enable_prime_tower": "1",
"wipe_tower_no_sparse_layers": "0",
"prime_tower_width": "60",
"xy_hole_compensation": "0",
"xy_contour_compensation": "0",
"layer_height": "0.2",
"bottom_shell_layers": "3",
"top_shell_layers": "4",
"bridge_flow": "1",
"initial_layer_speed": "45",
"initial_layer_infill_speed": "45",
"outer_wall_speed": "45",
"inner_wall_speed": "80",
"sparse_infill_speed": "150",
"internal_solid_infill_speed": "150",
"top_surface_speed": "50",
"gap_infill_speed": "30",
"travel_speed": "200"
}

View File

@@ -0,0 +1,54 @@
{
"name": "Printcepts",
"version": "01.00.00.01",
"force_update": "0",
"description": "Printcepts belt printer configurations",
"machine_model_list": [
{
"name": "BabyBelt Pro",
"sub_path": "machine/BabyBelt Pro.json"
}
],
"process_list": [
{
"name": "fdm_process_common",
"sub_path": "process/fdm_process_common.json"
},
{
"name": "0.20mm Standard @BabyBelt Pro",
"sub_path": "process/0.20mm Standard @BabyBelt Pro.json"
}
],
"filament_list": [
{
"name": "Generic PLA @BabyBelt Pro",
"sub_path": "filament/Generic PLA @BabyBelt Pro.json"
},
{
"name": "eSUN PLA @BabyBelt Pro",
"sub_path": "filament/eSUN PLA @BabyBelt Pro.json"
},
{
"name": "Generic PETG @BabyBelt Pro",
"sub_path": "filament/Generic PETG @BabyBelt Pro.json"
}
],
"machine_list": [
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_klipper_common",
"sub_path": "machine/fdm_klipper_common.json"
},
{
"name": "fdm_belt_common",
"sub_path": "machine/fdm_belt_common.json"
},
{
"name": "BabyBelt Pro 0.4 nozzle",
"sub_path": "machine/BabyBelt Pro 0.4 nozzle.json"
}
]
}

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="95.0mm" height="500.0mm" viewBox="0 0 95.0 500.0" preserveAspectRatio="xMidYMid meet">
<!-- Printcepts BabyBelt Pro bed texture: 95 x 500 mm belt plate. -->
<!-- Transparent plate; green (#195F30) BabyBelt Pro logo centered along X, near the bottom edge. -->
<rect x="0" y="0" width="95.0" height="500.0" fill="none"/>
<g transform="translate(14.2500,436.3488) scale(0.067538)">
<g transform="translate(-11.000000,692.938562) scale(0.100000,-0.100000)"
fill="#195F30" stroke="none">
<path d="M1963 5604 l-1423 -1324 0 -2050 0 -2050 443 0 c244 0 741 3 1105 7
l662 6 0 746 c-1 575 -4 768 -14 841 -47 324 -179 486 -473 581 -40 12 -73 26
-73 30 0 4 32 17 72 29 212 64 333 166 378 320 35 121 38 191 32 868 l-5 662
-629 0 c-395 0 -628 4 -628 10 0 5 635 601 1410 1325 776 724 1410 1318 1410
1321 0 2 -190 4 -422 3 l-423 0 -1422 -1325z m-334 -2029 c143 -16 174 -96
173 -446 -2 -411 -24 -458 -224 -462 l-93 -2 -3 450 c-1 248 0 456 3 463 3 9
18 12 47 8 24 -3 67 -8 97 -11z m-4 -1556 c160 -29 173 -62 182 -469 9 -455
-14 -593 -108 -641 -39 -19 -193 -44 -210 -33 -10 6 -13 1147 -3 1157 6 6 45
2 139 -14z"/>
<path d="M3464 5979 c-142 -132 -263 -245 -268 -250 -6 -5 69 -9 190 -9 l199
1 268 249 267 250 -198 0 -198 0 -260 -241z"/>
<path d="M3650 5649 c-135 -126 -254 -238 -265 -249 -19 -20 -18 -20 177 -20
l197 0 228 211 c125 116 246 229 268 250 l40 39 -200 -1 -200 0 -245 -230z"/>
<path d="M2537 5089 c-101 -24 -204 -105 -251 -197 -96 -190 -19 -420 172
-514 l67 -33 2670 0 2670 0 57 27 c74 34 146 107 184 183 43 88 43 230 0 322
-35 76 -113 153 -193 191 l-58 27 -2640 2 c-1513 0 -2656 -3 -2678 -8z m5063
-77 c-57 -37 -118 -111 -140 -168 -31 -82 -25 -206 12 -279 26 -49 93 -121
133 -143 15 -8 -640 -11 -2410 -11 l-2430 0 30 21 c200 146 201 425 1 569
l-39 29 2434 -1 c2263 0 2432 -1 2409 -17z m-4890 -43 c270 -122 185 -526
-109 -522 -257 2 -370 324 -170 485 74 60 194 76 279 37z m5201 -12 c94 -55
140 -135 140 -242 0 -285 -393 -374 -517 -117 -26 54 -30 162 -9 219 28 74 97
139 173 164 53 17 166 4 213 -24z"/>
<path d="M2917 3973 c-4 -174 -7 -550 -7 -835 l0 -518 326 0 326 0 -7 150 -7
150 110 0 110 0 11 -32 c5 -18 26 -86 46 -150 l36 -118 325 0 c179 0 323 4
320 9 -3 4 -155 374 -337 822 -182 448 -333 820 -336 827 -4 9 -105 12 -457
12 l-453 0 -6 -317z m773 -745 c0 -5 -47 -8 -104 -8 l-103 0 -7 92 c-3 50 -6
202 -5 337 l1 246 109 -330 c60 -181 109 -333 109 -337z"/>
<path d="M4680 3455 l0 -835 448 0 c693 1 885 16 985 80 99 63 126 132 134
340 12 327 -44 405 -342 476 -28 7 -27 8 25 19 199 42 255 95 267 248 11 146
-32 285 -110 353 -145 126 -329 153 -1049 154 l-358 0 0 -835z m846 520 c36
-23 44 -54 44 -162 0 -152 -29 -183 -170 -183 l-40 0 0 186 0 187 71 -6 c39
-3 82 -13 95 -22z m4 -625 c33 -18 40 -52 40 -208 0 -200 -9 -214 -143 -228
l-67 -7 0 233 0 233 74 -6 c41 -3 84 -10 96 -17z"/>
<path d="M6150 4286 c0 -3 131 -242 290 -531 l290 -526 0 -304 0 -305 385 0
385 0 0 299 0 299 305 533 305 534 -377 3 c-207 1 -381 -2 -385 -6 -16 -16
-110 -258 -172 -444 l-62 -187 -18 77 c-18 75 -139 450 -171 525 l-15 37 -380
0 c-209 0 -380 -2 -380 -4z"/>
<path d="M2910 1355 l0 -1185 830 0 830 0 0 240 0 240 -350 0 -350 0 0 255 0
255 300 0 300 0 0 230 0 230 -300 0 -300 0 0 220 0 220 320 0 320 0 0 240 0
240 -800 0 -800 0 0 -1185z"/>
<path d="M4680 1355 l0 -1185 775 0 775 0 0 240 0 240 -295 0 -295 0 0 945 0
945 -480 0 -480 0 0 -1185z"/>
<path d="M5800 2300 l0 -240 280 0 280 0 0 -945 0 -945 480 0 480 0 0 945 0
945 285 0 285 0 0 240 0 240 -1045 0 -1045 0 0 -240z"/>
<path d="M8032 1358 l-2 -1188 1008 1 c621 1 971 5 912 10 -309 27 -631 139
-885 306 -593 391 -984 1122 -1025 1918 -4 77 -8 -394 -8 -1047z"/>
<path d="M7441 1934 c-43 -36 -59 -70 -70 -148 -18 -124 16 -252 76 -291 32
-21 226 -33 328 -20 114 14 161 97 153 269 -5 96 -24 151 -68 191 -20 18 -39
20 -205 23 l-182 3 -32 -27z m389 -199 c7 -8 10 -22 6 -30 -4 -13 -34 -15
-186 -15 -189 0 -202 3 -186 45 8 22 348 22 366 0z"/>
<path d="M7450 1267 c-14 -6 -35 -32 -47 -57 -21 -41 -23 -58 -23 -222 l0
-178 270 0 270 0 0 105 0 105 -121 0 -120 0 3 28 3 27 118 3 117 3 0 99 0 100
-113 0 c-121 0 -138 -7 -162 -65 -8 -19 -9 -19 -12 2 -10 55 -116 84 -183 50z
m134 -203 c15 -38 8 -44 -54 -44 -62 0 -69 6 -54 44 9 23 99 23 108 0z"/>
<path d="M466 1193 l-29 -43 -163 0 -164 0 0 -235 0 -235 165 0 165 0 27 -42
28 -42 3 163 c1 89 1 233 0 319 l-3 157 -29 -42z"/>
<path d="M7443 620 c-48 -20 -58 -60 -61 -262 l-4 -188 271 0 271 0 0 110 0
110 -110 0 -110 0 0 70 c0 76 -21 145 -51 160 -22 12 -176 12 -206 0z m161
-186 c15 -39 8 -44 -64 -44 -72 0 -79 5 -64 44 9 23 119 23 128 0z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -0,0 +1,113 @@
{
"type": "filament",
"name": "Generic PETG @BabyBelt Pro",
"inherits": "Generic PETG @System",
"from": "system",
"setting_id": "gCzHpDNgVwQR6tgk",
"instantiation": "true",
"compatible_printers": [
"BabyBelt Pro 0.4 nozzle"
],
"filament_type": [
"PETG"
],
"filament_vendor": [
"Generic"
],
"filament_settings_id": [
"Generic PETG @BabyBelt Pro"
],
"filament_diameter": [
"1.75"
],
"filament_density": [
"1.27"
],
"filament_flow_ratio": [
"0.95"
],
"filament_cost": [
"25"
],
"filament_max_volumetric_speed": [
"10"
],
"nozzle_temperature": [
"240"
],
"nozzle_temperature_initial_layer": [
"245"
],
"nozzle_temperature_range_low": [
"220"
],
"nozzle_temperature_range_high": [
"260"
],
"temperature_vitrification": [
"70"
],
"hot_plate_temp": [
"80"
],
"hot_plate_temp_initial_layer": [
"80"
],
"cool_plate_temp": [
"80"
],
"cool_plate_temp_initial_layer": [
"80"
],
"textured_plate_temp": [
"80"
],
"textured_plate_temp_initial_layer": [
"80"
],
"fan_min_speed": [
"40"
],
"fan_max_speed": [
"60"
],
"overhang_fan_threshold": [
"25%"
],
"overhang_fan_speed": [
"80"
],
"close_fan_the_first_x_layers": [
"3"
],
"full_fan_speed_layer": [
"8"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"4"
],
"fan_cooling_layer_time": [
"100"
],
"reduce_fan_stop_start_freq": [
"1"
],
"filament_retraction_length": [
"2"
],
"filament_retraction_speed": [
"40"
],
"filament_deretraction_speed": [
"40"
],
"filament_z_hop": [
"0.4"
],
"filament_start_gcode": [
"; Generic PETG @BabyBelt Pro — belt PETG, bed 80C"
]
}

View File

@@ -0,0 +1,113 @@
{
"type": "filament",
"name": "Generic PLA @BabyBelt Pro",
"inherits": "Generic PLA @System",
"from": "system",
"setting_id": "24PpcnhVx9v5f4fD",
"instantiation": "true",
"compatible_printers": [
"BabyBelt Pro 0.4 nozzle"
],
"filament_type": [
"PLA"
],
"filament_vendor": [
"Generic"
],
"filament_settings_id": [
"Generic PLA @BabyBelt Pro"
],
"filament_diameter": [
"1.75"
],
"filament_density": [
"1.24"
],
"filament_flow_ratio": [
"0.98"
],
"filament_cost": [
"20"
],
"filament_max_volumetric_speed": [
"12"
],
"nozzle_temperature": [
"215"
],
"nozzle_temperature_initial_layer": [
"220"
],
"nozzle_temperature_range_low": [
"190"
],
"nozzle_temperature_range_high": [
"240"
],
"temperature_vitrification": [
"45"
],
"hot_plate_temp": [
"75"
],
"hot_plate_temp_initial_layer": [
"75"
],
"cool_plate_temp": [
"75"
],
"cool_plate_temp_initial_layer": [
"75"
],
"textured_plate_temp": [
"75"
],
"textured_plate_temp_initial_layer": [
"75"
],
"fan_min_speed": [
"100"
],
"fan_max_speed": [
"100"
],
"overhang_fan_threshold": [
"50%"
],
"overhang_fan_speed": [
"100"
],
"close_fan_the_first_x_layers": [
"3"
],
"full_fan_speed_layer": [
"8"
],
"slow_down_min_speed": [
"20"
],
"slow_down_layer_time": [
"4"
],
"fan_cooling_layer_time": [
"100"
],
"reduce_fan_stop_start_freq": [
"1"
],
"filament_retraction_length": [
"1.5"
],
"filament_retraction_speed": [
"35"
],
"filament_deretraction_speed": [
"30"
],
"filament_z_hop": [
"0.4"
],
"filament_start_gcode": [
"; Generic PLA @BabyBelt Pro — belt PLA, bed 75C"
]
}

View File

@@ -0,0 +1,35 @@
{
"type": "filament",
"name": "eSUN PLA @BabyBelt Pro",
"inherits": "Generic PLA @BabyBelt Pro",
"from": "system",
"setting_id": "EH3X7oE0DU5tSpjW",
"instantiation": "true",
"compatible_printers": [
"BabyBelt Pro 0.4 nozzle"
],
"filament_type": [
"PLA"
],
"filament_vendor": [
"eSUN"
],
"filament_settings_id": [
"eSUN PLA @BabyBelt Pro"
],
"nozzle_temperature_initial_layer": [
"200"
],
"nozzle_temperature": [
"200"
],
"enable_pressure_advance": [
"1"
],
"pressure_advance": [
"0.12"
],
"filament_max_volumetric_speed": [
"20"
]
}

View File

@@ -0,0 +1,87 @@
{
"type": "machine",
"name": "BabyBelt Pro 0.4 nozzle",
"inherits": "fdm_belt_common",
"from": "system",
"setting_id": "34OWINlJpJgA9DwQ",
"instantiation": "true",
"printer_model": "BabyBelt Pro",
"printer_variant": "0.4",
"nozzle_diameter": [
"0.4"
],
"default_filament_profile": [
"Generic PLA @BabyBelt Pro"
],
"default_print_profile": "0.20mm Standard @BabyBelt Pro",
"printable_area": [
"0x0",
"95x0",
"95x500",
"0x500"
],
"printable_height": "100",
"best_object_pos": "0.5,0.05",
"nozzle_type": [
"hardened_steel"
],
"printer_extruder_id": [
"1"
],
"printer_extruder_variant": [
"Direct Drive Standard"
],
"thumbnails": [
"48x48/PNG",
"300x300/PNG"
],
"machine_max_acceleration_e": [
"500",
"5000"
],
"machine_max_acceleration_extruding": [
"500",
"20000"
],
"machine_max_acceleration_retracting": [
"500",
"5000"
],
"machine_max_acceleration_x": [
"500",
"20000"
],
"machine_max_acceleration_y": [
"500",
"20000"
],
"machine_max_junction_deviation": [
"0.01"
],
"machine_max_speed_x": [
"50",
"200"
],
"machine_max_speed_y": [
"50",
"200"
],
"machine_max_speed_z": [
"5",
"12"
],
"retraction_length": [
"1.5"
],
"retraction_speed": [
"20"
],
"deretraction_speed": [
"25"
],
"retract_lift_enforce": [
"Top and Bottom"
],
"support_chamber_temp_control": "0",
"machine_start_gcode": ";Start GCode\nPRINT_START ANGLE=[belt_slice_rotation_angle] EXTRUDER=[nozzle_temperature_initial_layer] BED=[hot_plate_temp_initial_layer] MATERIAL=[filament_type]\n"
}

View File

@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "BabyBelt Pro",
"model_id": "Printcepts_BabyBelt_Pro",
"nozzle_diameter": "0.4",
"machine_tech": "FFF",
"family": "Printcepts",
"bed_model": "",
"bed_texture": "BabyBelt Pro_bed_texture.svg",
"hotend_model": "",
"default_materials": "Generic PLA @BabyBelt Pro;Generic PETG @BabyBelt Pro"
}

View File

@@ -0,0 +1,99 @@
{
"type": "machine",
"name": "fdm_belt_common",
"inherits": "fdm_klipper_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"single_extruder_multi_material": "0",
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @BabyBelt Pro",
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"deretraction_speed": [
"30"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"long_retractions_when_cut": [
"0"
],
"nozzle_diameter": [
"0.4"
],
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0"
],
"retract_lift_below": [
"0"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"0.8"
],
"retraction_minimum_travel": [
"1"
],
"retraction_speed": [
"30"
],
"travel_slope": [
"3"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"0.4"
],
"z_hop_types": [
"Normal Lift"
],
"gcode_remap_x": "rev_x",
"gcode_remap_y": "pos_z",
"gcode_remap_z": "pos_y",
"printer_extruder_id": [
"1"
],
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"belt_slice_rotation_global": "1",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
"auxiliary_fan": "0"
}

View File

@@ -0,0 +1,141 @@
{
"type": "machine",
"name": "fdm_klipper_common",
"inherits": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"machine_max_acceleration_e": [
"5000",
"5000"
],
"machine_max_acceleration_extruding": [
"20000",
"20000"
],
"machine_max_acceleration_retracting": [
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"20000",
"20000"
],
"machine_max_acceleration_x": [
"20000",
"20000"
],
"machine_max_acceleration_y": [
"20000",
"20000"
],
"machine_max_acceleration_z": [
"500",
"200"
],
"machine_max_speed_e": [
"25",
"25"
],
"machine_max_speed_x": [
"500",
"200"
],
"machine_max_speed_y": [
"500",
"200"
],
"machine_max_speed_z": [
"12",
"12"
],
"machine_max_jerk_e": [
"2.5",
"2.5"
],
"machine_max_jerk_x": [
"9",
"9"
],
"machine_max_jerk_y": [
"9",
"9"
],
"machine_max_jerk_z": [
"0.2",
"0.4"
],
"machine_min_extruding_rate": [
"0",
"0"
],
"machine_min_travel_rate": [
"0",
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"printer_settings_id": "",
"printer_technology": "FFF",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"1"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"0.8"
],
"retract_length_toolchange": [
"2"
],
"z_hop": [
"0.4"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"30"
],
"deretraction_speed": [
"30"
],
"z_hop_types": "Normal Lift",
"silent_mode": "0",
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_filament_profile": [
"Generic PLA @System"
],
"default_print_profile": "0.20mm Standard @MyKlipper",
"bed_exclude_area": [
"0x0"
],
"machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n",
"machine_end_gcode": "PRINT_END",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "PAUSE",
"scan_first_layer": "0",
"nozzle_type": "undefine",
"auxiliary_fan": "0"
}

View File

@@ -0,0 +1,119 @@
{
"type": "machine",
"name": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"printer_technology": "FFF",
"deretraction_speed": [
"40"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"gcode_flavor": "marlin",
"silent_mode": "0",
"machine_max_acceleration_e": [
"5000"
],
"machine_max_acceleration_extruding": [
"10000"
],
"machine_max_acceleration_retracting": [
"1000"
],
"machine_max_acceleration_x": [
"10000"
],
"machine_max_acceleration_y": [
"10000"
],
"machine_max_acceleration_z": [
"500"
],
"machine_max_speed_e": [
"60"
],
"machine_max_speed_x": [
"500"
],
"machine_max_speed_y": [
"500"
],
"machine_max_speed_z": [
"10"
],
"machine_max_jerk_e": [
"5"
],
"machine_max_jerk_x": [
"8"
],
"machine_max_jerk_y": [
"8"
],
"machine_max_jerk_z": [
"0.4"
],
"machine_min_extruding_rate": [
"0"
],
"machine_min_travel_rate": [
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"nozzle_diameter": [
"0.4"
],
"printer_settings_id": "",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"2"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"1"
],
"retract_length_toolchange": [
"1"
],
"z_hop": [
"0"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"60"
],
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_print_profile": "",
"machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up",
"machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "M601"
}

View File

@@ -0,0 +1,23 @@
{
"type": "process",
"name": "0.20mm Standard @BabyBelt Pro",
"inherits": "fdm_process_common",
"from": "system",
"setting_id": "JGfGtqX6CWjCt437",
"instantiation": "true",
"layer_height": "0.2",
"initial_layer_print_height": "0.2",
"initial_layer_line_width": "0.42",
"wall_loops": "2",
"reduce_infill_retraction": "1",
"detect_overhang_wall": "1",
"skirt_loops": "0",
"skirt_distance": "0",
"sparse_infill_pattern": "grid",
"sparse_infill_speed": "200",
"support_base_pattern": "rectilinear",
"support_interface_pattern": "rectilinear",
"compatible_printers": [
"BabyBelt Pro 0.4 nozzle"
]
}

View File

@@ -0,0 +1,108 @@
{
"type": "process",
"name": "fdm_process_common",
"from": "system",
"instantiation": "false",
"adaptive_layer_height": "0",
"reduce_crossing_wall": "0",
"max_travel_detour_distance": "0",
"bottom_surface_pattern": "monotonic",
"bottom_shell_thickness": "0",
"bridge_speed": "50",
"brim_width": "5",
"brim_object_gap": "0.1",
"compatible_printers": [],
"compatible_printers_condition": "",
"print_sequence": "by layer",
"default_acceleration": "1000",
"initial_layer_acceleration": "500",
"top_surface_acceleration": "1000",
"travel_acceleration": "1000",
"inner_wall_acceleration": "1000",
"outer_wall_acceleration": "700",
"bridge_no_support": "0",
"draft_shield": "disabled",
"elefant_foot_compensation": "0",
"enable_arc_fitting": "0",
"wall_infill_order": "inner wall/outer wall/infill",
"infill_direction": "45",
"sparse_infill_density": "15%",
"sparse_infill_pattern": "crosshatch",
"initial_layer_print_height": "0.2",
"infill_combination": "0",
"infill_wall_overlap": "25%",
"interface_shells": "0",
"ironing_flow": "10%",
"ironing_spacing": "0.15",
"ironing_speed": "30",
"ironing_type": "no ironing",
"reduce_infill_retraction": "1",
"filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode",
"detect_overhang_wall": "1",
"slowdown_for_curled_perimeters": "1",
"overhang_1_4_speed": "0",
"overhang_2_4_speed": "50",
"overhang_3_4_speed": "30",
"overhang_4_4_speed": "10",
"line_width": "110%",
"inner_wall_line_width": "110%",
"outer_wall_line_width": "100%",
"top_surface_line_width": "93.75%",
"sparse_infill_line_width": "110%",
"initial_layer_line_width": "120%",
"internal_solid_infill_line_width": "120%",
"support_line_width": "96%",
"wall_loops": "3",
"print_settings_id": "",
"raft_layers": "0",
"seam_position": "aligned",
"skirt_distance": "2",
"skirt_height": "3",
"min_skirt_length": "4",
"skirt_loops": "0",
"minimum_sparse_infill_area": "15",
"spiral_mode": "0",
"standby_temperature_delta": "-5",
"enable_support": "0",
"resolution": "0.012",
"support_type": "normal(auto)",
"support_on_build_plate_only": "0",
"support_top_z_distance": "0.2",
"support_bottom_z_distance": "0.2",
"support_filament": "0",
"support_interface_loop_pattern": "0",
"support_interface_filament": "0",
"support_interface_top_layers": "2",
"support_interface_bottom_layers": "2",
"support_interface_spacing": "0.5",
"support_interface_speed": "80",
"support_base_pattern": "default",
"support_base_pattern_spacing": "2.5",
"support_speed": "150",
"support_threshold_angle": "30",
"support_object_xy_distance": "0.35",
"tree_support_branch_angle": "30",
"tree_support_wall_count": "0",
"tree_support_with_infill": "0",
"detect_thin_wall": "0",
"top_surface_pattern": "monotonicline",
"top_shell_thickness": "0.8",
"enable_prime_tower": "1",
"wipe_tower_no_sparse_layers": "0",
"prime_tower_width": "60",
"xy_hole_compensation": "0",
"xy_contour_compensation": "0",
"layer_height": "0.2",
"bottom_shell_layers": "3",
"top_shell_layers": "4",
"bridge_flow": "1",
"initial_layer_speed": "45",
"initial_layer_infill_speed": "45",
"outer_wall_speed": "45",
"inner_wall_speed": "80",
"sparse_infill_speed": "150",
"internal_solid_infill_speed": "150",
"top_surface_speed": "50",
"gap_infill_speed": "30",
"travel_speed": "200"
}

View File

@@ -26,6 +26,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform vec4 uniform_color;

View File

@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -73,8 +74,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
// z component of normal vector in world coordinate used for slope shading
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
// dot product of world normal with up direction, used for slope shading
world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {

View File

@@ -37,6 +37,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
@@ -85,7 +86,7 @@ void main()
color = LightBlue;
alpha = 1.0;
}
else if( transformed_normal.z < slope.normal_z - EPSILON)
else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
{
color = color * 0.5 + LightRed * 0.5;
alpha = 1.0;

View File

@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
void main()

View File

@@ -1,7 +1,4 @@
#version 140
// Multisample depth texture for the anti-aliased outline (see 3DScene.cpp render_with_outline).
// Optional on the GLSL 140 path: if unavailable, fallback to a non-multisample depth texture.
#extension GL_ARB_texture_multisample : enable
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
//BBS: add grey and orange
@@ -29,6 +26,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform vec4 uniform_color;
@@ -39,14 +37,7 @@ uniform SlopeDetection slope;
//BBS: add outline_color
uniform bool is_outline;
// The outline is a per-fragment discard mask, which the framebuffer MSAA cannot smooth, so the
// silhouette is resolved per sample from a multisample copy of the outlined model's depth buffer.
#ifdef GL_ARB_texture_multisample
uniform sampler2DMS depth_tex;
uniform int msaa_samples; // samples in depth_tex, 1 when MSAA is off
#else
uniform sampler2D depth_tex;
#endif
uniform vec2 screen_size;
#ifdef ENABLE_ENVIRONMENT_MAP
@@ -109,85 +100,43 @@ float GetTolerance(float d, float k)
return -k*(d+A)*(d+A)/B;
}
// Depth of sample s at integer pixel coord.
#ifdef GL_ARB_texture_multisample
float FetchDepth(ivec2 coord, int s)
{
// texelFetch has no wrap mode, so clamp to the edge texel (sampler2D used CLAMP_TO_EDGE).
ivec2 sz = textureSize(depth_tex);
return abs(texelFetch(depth_tex, clamp(coord, ivec2(0), sz - 1), s).r);
}
#else
float FetchDepth(ivec2 coord, int s)
{
return abs(texture(depth_tex, (vec2(coord) + 0.5) / screen_size).r);
}
#endif
float DetectSilho(ivec2 coord, ivec2 dir, int s)
float DetectSilho(vec2 fragCoord, vec2 dir)
{
// -------------------------------------------
// x0 ___ x1----o
// :\ :
// x0 ___ x1----o
// :\ :
// r0 : \ : r1
// : \ :
// : \ :
// o---x2 ___ x3
//
// r0 and r1 are the differences between actual
// and expected (as if x0..3 where on the same
// plane) depth values.
// -------------------------------------------
float x0 = FetchDepth(coord + dir*-2, s);
float x1 = FetchDepth(coord + dir*-1, s);
float x2 = FetchDepth(coord, s);
float x3 = FetchDepth(coord + dir* 1, s);
float x0 = abs(texture(depth_tex, (fragCoord + dir*-2.0) / screen_size).r);
float x1 = abs(texture(depth_tex, (fragCoord + dir*-1.0) / screen_size).r);
float x2 = abs(texture(depth_tex, (fragCoord + dir* 0.0) / screen_size).r);
float x3 = abs(texture(depth_tex, (fragCoord + dir* 1.0) / screen_size).r);
float d0 = (x1-x0);
float d1 = (x2-x3);
float r0 = x1 + d0 - x2;
float r1 = x2 + d1 - x1;
float tol = GetTolerance(x2, 0.04);
return smoothstep(0.0, tol*tol, max( - r0*r1, 0.0));
}
float DetectSilho(ivec2 coord, int s)
{
return max(
DetectSilho(coord, ivec2(1,0), s), // Horizontal
DetectSilho(coord, ivec2(0,1), s) // Vertical
);
}
// Full response of one sample. Reduce the max() per sample and average only afterwards:
// max(mean) <= mean(max), and averaging first hollows out diagonal and curved lines.
float DetectSilhoSample(ivec2 coord, int s)
{
float v = DetectSilho(coord, s);
// Makes silhouettes thicker.
for (int i = 1; i <= INFLATE; ++i)
{
v = max(v, DetectSilho(coord + ivec2(i, 0), s));
v = max(v, DetectSilho(coord + ivec2(0, i), s));
}
return v;
}
// Average the per-sample coverage into the sub-pixel anti-aliasing of the line.
float DetectSilho(vec2 fragCoord)
{
ivec2 coord = ivec2(fragCoord);
#ifdef GL_ARB_texture_multisample
int n = max(msaa_samples, 1);
#else
const int n = 1;
#endif
float acc = 0.0;
for (int s = 0; s < n; ++s)
acc += DetectSilhoSample(coord, s);
return acc / float(n);
return max(
DetectSilho(fragCoord, vec2(1,0)), // Horizontal
DetectSilho(fragCoord, vec2(0,1)) // Vertical
);
}
// Returns a lighting multiplier in [1 - shadow_intensity, 1]: < 1 where the fragment is
@@ -276,7 +225,14 @@ void main()
//BBS: add outline_color
if (is_outline) {
color = vec4((vec3(intensity.y) + color.rgb * intensity.x) * shade, color.a);
float s = DetectSilho(gl_FragCoord.xy);
vec2 fragCoord = gl_FragCoord.xy;
float s = DetectSilho(fragCoord);
// Makes silhouettes thicker.
for(int i=1;i<=INFLATE; i++)
{
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
}
if (s < 0.01)
discard;
out_color = vec4(mix(color.rgb, getBackfaceColor(color.rgb), s), color.a);

View File

@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -73,8 +74,8 @@ void main()
// Point in homogenous coordinates.
world_pos = volume_world_matrix * vec4(v_position, 1.0);
// z component of normal vector in world coordinate used for slope shading
world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0;
// dot product of world normal with up direction, used for slope shading
world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0;
gl_Position = projection_matrix * position;
if (is_outline) {

View File

@@ -37,6 +37,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
@@ -87,7 +88,7 @@ void main()
color = LightBlue;
alpha = 1.0;
}
else if( transformed_normal.z < slope.normal_z - EPSILON)
else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
{
color = color * 0.5 + LightRed * 0.5;
alpha = 1.0;

View File

@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
void main()

View File

@@ -1,7 +1,4 @@
#version 140
// Multisample depth texture for the anti-aliased outline (see 3DScene.cpp render_with_outline).
// Optional on the GLSL 140 path: if unavailable, fallback to a non-multisample depth texture.
#extension GL_ARB_texture_multisample : enable
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
const vec3 LightRed = vec3(0.78, 0.0, 0.0);
@@ -54,14 +51,7 @@ uniform SlopeDetection slope;
//BBS: add outline_color
uniform bool is_outline;
// The outline is a per-fragment discard mask, which the framebuffer MSAA cannot smooth, so the
// silhouette is resolved per sample from a multisample copy of the outlined model's depth buffer.
#ifdef GL_ARB_texture_multisample
uniform sampler2DMS depth_tex;
uniform int msaa_samples; // samples in depth_tex, 1 when MSAA is off
#else
uniform sampler2D depth_tex;
#endif
uniform vec2 screen_size;
#ifdef ENABLE_ENVIRONMENT_MAP
@@ -110,27 +100,12 @@ float GetTolerance(float d, float k)
return -k*(d+A)*(d+A)/B;
}
// Depth of sample s at integer pixel coord.
#ifdef GL_ARB_texture_multisample
float FetchDepth(ivec2 coord, int s)
float DetectSilho(vec2 fragCoord, vec2 dir)
{
// texelFetch has no wrap mode, so clamp to the edge texel (sampler2D used CLAMP_TO_EDGE).
ivec2 sz = textureSize(depth_tex);
return abs(texelFetch(depth_tex, clamp(coord, ivec2(0), sz - 1), s).r);
}
#else
float FetchDepth(ivec2 coord, int s)
{
return abs(texture(depth_tex, (vec2(coord) + 0.5) / screen_size).r);
}
#endif
float DetectSilho(ivec2 coord, ivec2 dir, int s)
{
float x0 = FetchDepth(coord + dir*-2, s);
float x1 = FetchDepth(coord + dir*-1, s);
float x2 = FetchDepth(coord, s);
float x3 = FetchDepth(coord + dir* 1, s);
float x0 = abs(texture(depth_tex, (fragCoord + dir*-2.0) / screen_size).r);
float x1 = abs(texture(depth_tex, (fragCoord + dir*-1.0) / screen_size).r);
float x2 = abs(texture(depth_tex, (fragCoord + dir* 0.0) / screen_size).r);
float x3 = abs(texture(depth_tex, (fragCoord + dir* 1.0) / screen_size).r);
float d0 = (x1-x0);
float d1 = (x2-x3);
@@ -141,43 +116,15 @@ float DetectSilho(ivec2 coord, ivec2 dir, int s)
float tol = GetTolerance(x2, 0.04);
return smoothstep(0.0, tol*tol, max( - r0*r1, 0.0));
}
float DetectSilho(ivec2 coord, int s)
{
return max(
DetectSilho(coord, ivec2(1,0), s),
DetectSilho(coord, ivec2(0,1), s)
);
}
// Full response of one sample. Reduce the max() per sample and average only afterwards:
// max(mean) <= mean(max), and averaging first hollows out diagonal and curved lines.
float DetectSilhoSample(ivec2 coord, int s)
{
float v = DetectSilho(coord, s);
// Makes silhouettes thicker.
for (int i = 1; i <= INFLATE; ++i)
{
v = max(v, DetectSilho(coord + ivec2(i, 0), s));
v = max(v, DetectSilho(coord + ivec2(0, i), s));
}
return v;
}
// Average the per-sample coverage into the sub-pixel anti-aliasing of the line.
float DetectSilho(vec2 fragCoord)
{
ivec2 coord = ivec2(fragCoord);
#ifdef GL_ARB_texture_multisample
int n = max(msaa_samples, 1);
#else
const int n = 1;
#endif
float acc = 0.0;
for (int s = 0; s < n; ++s)
acc += DetectSilhoSample(coord, s);
return acc / float(n);
return max(
DetectSilho(fragCoord, vec2(1,0)),
DetectSilho(fragCoord, vec2(0,1))
);
}
float compute_ssao_factor(vec3 normal, vec3 view_dir, vec3 eye_pos)
@@ -323,7 +270,13 @@ void main()
if (is_outline) {
vec3 shaded_rgb = (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade;
vec4 shaded_color = vec4(clamp(shaded_rgb, vec3(0.0), vec3(1.0)), color.a);
float s = DetectSilho(gl_FragCoord.xy);
vec2 fragCoord = gl_FragCoord.xy;
float s = DetectSilho(fragCoord);
for(int i=1;i<=INFLATE; i++)
{
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
}
if (s < 0.01)
discard;
out_color = vec4(mix(shaded_color.rgb, getBackfaceColor(shaded_color.rgb), s), shaded_color.a);

View File

@@ -575,7 +575,7 @@ function CapabilityCanRun(plugin, capability) {
}
function IsPluginChecked(plugin) {
return plugin.is_loaded;
return GetStatus(plugin) === "Activated";
}
function HasMixedCapabilityState(plugin) {
@@ -1347,8 +1347,6 @@ function StatusDescription(plugin) {
return "This plugin is still loading.";
case "Error":
return "This plugin is blocked until its error is fixed.";
case "RuntimeError":
return "This plugin is loaded but a capability reported an error.";
case "Inactive":
default:
return "This plugin is inactive. Activate it to install or load it.";

View File

@@ -424,11 +424,6 @@ body.pane-resizing {
font-weight: 600;
}
.status-cell.status-runtimeerror {
color: var(--plugin-status-warn);
font-weight: 600;
}
.status-cell.status-loading {
color: var(--plugin-status-warn);
font-weight: 600;
@@ -685,11 +680,6 @@ body.pane-resizing {
color: var(--plugin-status-danger);
}
.detail-status-chip.status-runtimeerror {
background: var(--plugin-status-warn-bg);
color: var(--plugin-status-warn);
}
.detail-status-chip.status-loading {
background: var(--plugin-status-warn-bg);
color: var(--plugin-status-warn);

View File

@@ -626,12 +626,6 @@ void AppConfig::set_defaults()
set_bool("window_buttons_on_left", false);
#endif
if (get("use_printer_agents").empty())
{
// false = legacy behavior using print hosts
set_bool("use_printer_agents", false);
}
// Remove legacy window positions/sizes
erase("app", "main_frame_maximized");
erase("app", "main_frame_pos");

View File

@@ -0,0 +1,76 @@
#include "BeltGCode.hpp"
#include "BeltGCodeWriter.hpp"
#include "BeltTransform.hpp"
#include "Print.hpp"
namespace Slic3r {
void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers)
{
if (!print.config().belt_printer.value)
return;
auto belt_writer = std::make_unique<BeltGCodeWriter>();
belt_writer->set_is_bbl_machine(is_bbl_printers);
// Axis remap and build volume max are set by base GCode after init_belt_writer returns.
belt_writer->set_belt_back_transform(print.config());
belt_writer->set_machine_frame_transform(print.config());
m_writer = std::move(belt_writer);
}
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
{
if (!print.config().belt_printer.value)
return;
const auto &full_cfg = print.full_print_config();
// Slicing rotation: the belt tilt (axis + angle) and the single source of truth
// for the physical tilt the G-code viewer uses to enable belt view.
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
file.write_format("; belt_slice_rotation_global = %d\n", print.config().belt_slice_rotation_global.value ? 1 : 0);
// Pre-slice remap configs
file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str());
file.write_format("; preslice_remap_y = %s\n", full_cfg.opt_serialize("preslice_remap_y").c_str());
file.write_format("; preslice_remap_z = %s\n", full_cfg.opt_serialize("preslice_remap_z").c_str());
file.write_format("; preslice_remap_global = %d\n", print.config().preslice_remap_global.value ? 1 : 0);
file.write_format("; belt_preslice_global = %d\n", print.config().belt_preslice_global.value ? 1 : 0);
// Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt
// tilt angle (or belt_frame_tilt_angle when decoupled).
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value);
}
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
{
// Global pre-slice mode: adjust origin using computed correction.
// Transform the origin through the belt pipeline so that
// back_transform(T * origin) = origin (correct machine position).
//
// Flags that trigger this path:
// belt_preslice_global — full pipeline (rotation * remap) is global
// preslice_remap_global — only the pre-slice remap is global
// belt_slice_rotation_global — slicing rotation treated as global (matches
// the per-instance Z-offset added in PrintObjectSlice.cpp)
// The XY origin adjustment uses the FULL forward transform, because the
// back_transform applied during G-code emission is always the inverse of
// the full pipeline.
bool use_global = m_config.belt_preslice_global.value
|| (m_config.preslice_remap_global.value
&& BeltTransformPipeline::has_preslice_remap(m_config))
|| (m_config.belt_slice_rotation_global.value
&& m_config.belt_slice_rotation.value != BeltRotationAxis::None
&& std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON);
if (!use_global || !m_config.belt_printer.value)
return;
// Adjust origin: transform through belt forward pipeline so that
// the back-transform correctly recovers model-space positions.
Transform3d T = BeltTransformPipeline::build_forward_transform(m_config);
Vec2d cur_origin = this->origin();
Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.);
Vec3d adjusted = T.linear() * origin3d;
this->set_origin(Vec2d(adjusted.x(), adjusted.y()));
}
} // namespace Slic3r

View File

@@ -0,0 +1,23 @@
#pragma once
#include "GCode.hpp"
namespace Slic3r {
// Belt-printer-specific GCode export.
//
// Inherits from GCode and overrides virtual hooks to:
// - Create a BeltGCodeWriter instead of a plain GCodeWriter
// - Write belt configuration to the G-code header
// - Adjust the origin for global pre-slice transforms when switching instances
// - Disable arc fitting (G2/G3 not supported on belt printers)
class BeltGCode : public GCode
{
protected:
void init_belt_writer(Print &print, bool is_bbl_printers) override;
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
bool should_disable_arc_fitting() const override { return true; }
};
} // namespace Slic3r

View File

@@ -0,0 +1,275 @@
#include "BeltGCodeWriter.hpp"
#include "FirstLayerPlane.hpp"
#include "Geometry.hpp"
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace {
// Decide whether a particular destination point gets first-layer treatment.
// When the plane evaluator is active, distance from the plane wins; otherwise
// fall back to the layer-coarse m_is_first_layer flag set by the caller.
inline bool belt_point_on_first_layer(
const FirstLayerPlane *plane,
double first_layer_thickness_mm,
bool layer_first_flag,
const Vec3d &point_slicing_mm)
{
if (plane && plane->is_active())
return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm);
return layer_first_flag;
}
} // namespace
// ---- Belt configuration ---------------------------------------------------
void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config)
{
m_belt_back_transform.init_from_config(config);
}
void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config)
{
m_machine_frame_transform.init_from_config(config);
}
Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const
{
// Step 1+2: To Cartesian (back_transform + axis_remap).
// In world-coordinates mode (PA line / PA pattern calibration) the input
// already describes a point relative to the belt surface, so the
// slicer->world back-transform is skipped and only the machine kinematics
// (axis remap + frame shear/scale) are applied.
Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos);
Vec3d result = apply_axis_remap(after_back);
Vec3d after_remap = result;
// Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts
// as a global linear transform on the placed coords.
Vec3d final = m_machine_frame_transform.apply(result);
// [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z
// crosses an integer mm boundary) to keep the log volume manageable while
// still capturing one sample per ~5 layers. Shows the full pipeline so
// Case A vs Case B can be compared step-by-step.
static thread_local int s_last_logged_z = std::numeric_limits<int>::min();
int z_bucket = static_cast<int>(std::floor(pos.z() * 5.0)); // every 0.2mm
if (z_bucket != s_last_logged_z) {
s_last_logged_z = z_bucket;
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords"
<< " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")"
<< " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")"
<< " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")"
<< " final=(" << final.x() << "," << final.y() << "," << final.z() << ")"
<< " mft_active=" << m_machine_frame_transform.is_active()
<< " back_active=" << m_belt_back_transform.is_active();
}
return final;
}
// ---- Overridden movement methods ------------------------------------------
std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment)
{
m_pos(0) = point(0);
m_pos(1) = point(1);
this->set_current_position_clear(true);
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
// Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation)
Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
GCodeG1Formatter w;
w.emit_xyz(machine);
const bool first_layer_for_point = belt_point_on_first_layer(
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer,
Vec3d(point.x(), point.y(), m_pos.z()));
auto speed = first_layer_for_point
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
w.emit_f(speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
{
// Belt printer: force NormalLift since SpiralLift and SlopeLift compute
// slope angles that don't account for the YZ coordinate rotation.
return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase);
}
std::string BeltGCodeWriter::eager_lift(const LiftType type)
{
// Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation).
return GCodeWriter::eager_lift(LiftType::NormalLift);
}
std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment)
{
m_pos(2) = z;
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
if (speed == 0.) {
const bool first_layer_for_point = belt_point_on_first_layer(
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer,
Vec3d(m_pos.x(), m_pos.y(), z));
speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
}
// Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords.
Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z));
GCodeG1Formatter w;
w.emit_xyz(machine);
w.emit_f(speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion)
{
m_pos(0) = point(0);
m_pos(1) = point(1);
if (std::abs(dE) <= std::numeric_limits<double>::epsilon())
force_no_extrusion = true;
if (!force_no_extrusion)
filament()->extrude(dE);
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
// Belt printer: transform and emit XYZ (Y and Z are coupled)
Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
GCodeG1Formatter w;
w.emit_xyz(machine);
if (!force_no_extrusion)
w.emit_e(filament()->E());
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion)
{
m_pos = point;
m_lifted = 0;
if (!force_no_extrusion)
filament()->extrude(dE);
Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) };
point_on_plate = to_machine_coords(point_on_plate);
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
if (!force_no_extrusion)
w.emit_e(filament()->E());
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z)
{
// Belt-specific override of travel_to_xyz.
// Key differences from base:
// 1. All coordinates go through to_machine_coords()
// 2. Always emit full XYZ (can't split XY and Z due to coupling)
// 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides)
Vec3d dest_point = point;
const bool first_layer_for_point = belt_point_on_first_layer(
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point);
auto travel_speed =
first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
// Handle pending z_hop
if (std::abs(m_to_lift) > EPSILON) {
assert(std::abs(m_lifted) < EPSILON);
if ((!this->is_current_position_clear() || m_pos != dest_point) &&
m_to_lift + m_pos(2) > point(2)) {
m_lifted = m_to_lift + m_pos(2) - point(2);
dest_point(2) = m_to_lift + m_pos(2);
}
m_to_lift = 0.;
std::string slop_move;
Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) };
Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
Vec3d delta = target - source;
Vec2d delta_no_z = { delta(0), delta(1) };
if (delta(2) > 0 && delta_no_z.norm() != 0.0f) {
// Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift),
// but handle NormalLift and fallthrough.
if (m_to_lift_type == LiftType::SlopeLift &&
this->is_current_position_clear() &&
atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) {
Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope());
Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source;
slope_top_point = to_machine_coords(slope_top_point);
GCodeG1Formatter w0;
w0.emit_xyz(slope_top_point);
w0.emit_f(travel_speed * 60.0);
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
slop_move = w0.string();
}
else if (m_to_lift_type == LiftType::NormalLift && this->is_current_position_clear()) {
// Only lift-in-place when the current position is known. 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 shears into a bogus machine point
// (e.g. X=bed_max, Y=layer_z) far up the gantry. Skipping the separate
// lift here is safe: there is nothing to lift over yet, and the
// xy_z_move below travels straight to the destination with full XYZ,
// establishing the correct position. This mirrors the SlopeLift branch
// above, which already guards on is_current_position_clear().
slop_move = _travel_to_z(target.z(), "normal lift Z");
}
}
std::string xy_z_move;
{
Vec3d emit_target = to_machine_coords(target);
GCodeG1Formatter w0;
// Belt mode: always emit full XYZ since Y and Z are coupled
w0.emit_xyz(emit_target);
w0.emit_f(travel_speed * 60.0);
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
xy_z_move = w0.string();
}
m_pos = dest_point;
this->set_current_position_clear(true);
return slop_move + xy_z_move;
}
else if (!force_z && !this->will_move_z(point(2))) {
double nominal_z = m_pos(2) - m_lifted;
m_lifted -= (point(2) - nominal_z);
if (std::abs(m_lifted) < EPSILON)
m_lifted = 0.;
this->set_current_position_clear(true);
return this->travel_to_xy(to_2d(point));
}
else {
m_lifted = 0;
}
Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
point_on_plate = to_machine_coords(point_on_plate);
// Belt mode: always emit full XYZ
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
m_pos = dest_point;
this->set_current_position_clear(true);
return w.string();
}
} // namespace Slic3r

View File

@@ -0,0 +1,64 @@
#pragma once
#include "GCodeWriter.hpp"
#include "GCode/BeltBackTransform.hpp"
#include "GCode/MachineFrameTransform.hpp"
namespace Slic3r {
class FirstLayerPlane;
// Belt-printer-specific GCode writer.
//
// Inherits from GCodeWriter and overrides movement methods to apply
// coordinate transformation (back-transform, axis remap, machine-frame
// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt).
class BeltGCodeWriter : public GCodeWriter
{
public:
BeltGCodeWriter() : GCodeWriter() {}
// Belt configuration (axis remap is inherited from GCodeWriter)
void set_belt_back_transform(const PrintConfig &config);
void set_machine_frame_transform(const PrintConfig &config);
Vec3d to_machine_coords(const Vec3d &pos) const;
// World-coordinates mode: incoming coordinates are treated as points
// relative to the physical belt surface (X across, Y along the belt,
// Z height above it) instead of slicing-frame coordinates — the
// slicer->world back-transform is skipped. Used by the PA line / PA
// pattern calibration generators, whose logical bed coordinates describe
// first-layer drawings on the build surface.
void set_world_coordinates(bool enable) { m_world_coordinates = enable; }
// First-layer plane: when set to a non-null active evaluator, travel
// speed selection consults the plane per-move and uses
// initial_layer_travel_speed for points within first_layer_height_mm
// of the plane (regardless of slicing layer index).
void set_first_layer_plane(const FirstLayerPlane *plane,
double first_layer_height_mm) {
m_first_layer_plane = plane;
m_first_layer_thickness_mm = first_layer_height_mm;
}
// Overridden movement methods
std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override;
std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override;
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override;
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override;
std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override;
std::string eager_lift(const LiftType type) override;
protected:
std::string _travel_to_z(double z, const std::string &comment) override;
private:
BeltBackTransform m_belt_back_transform;
MachineFrameTransform m_machine_frame_transform;
bool m_world_coordinates = false;
// Borrowed pointer; lifetime owned by GCode. null = inactive.
const FirstLayerPlane *m_first_layer_plane = nullptr;
double m_first_layer_thickness_mm = 0.;
};
} // namespace Slic3r

View File

@@ -0,0 +1,143 @@
#include "BeltSliceStrategy.hpp"
#include "Model.hpp"
#include <limits>
#include <boost/log/trivial.hpp>
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
#include <iomanip>
#include <sstream>
#include <thread>
#endif
namespace Slic3r {
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
const PrintConfig &config,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z)
{
// 1. Standalone pre-slice axis remap (works without belt mode).
const bool has_remap = BeltTransformPipeline::has_preslice_remap(config);
if (has_remap)
trafo = BeltTransformPipeline::build_preslice_remap(config) * trafo;
// 2. Belt rotation — the sole mesh-side belt transform (matching
// BeltTransformPipeline::build_forward_transform). Only active in
// belt-printer mode.
bool has_rotation = false;
if (config.belt_printer.value) {
const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation);
if (has_rotation) {
Transform3d belt_xform = Transform3d::Identity();
belt_xform.linear() = rot;
trafo = belt_xform * trafo;
}
}
if (!has_remap && !has_rotation)
return;
// 3. Z-shift — detect if the mesh clips below the build plate after the
// transforms and lift it. Each mesh vertex must be brought into object space
// via mv->get_matrix() before applying the full trafo (which is in object
// space). Missing this on assemblies (where per-volume get_matrix() positions
// each volume within the object) would compute min_z against mesh-local vertex
// coordinates rather than object-space coordinates, so volumes translated along
// the slicer's Z axis would be silently excluded from the bound check.
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
// Capture the incoming trafo for diagnostic logging.
// This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift.
const Transform3d trafo_pre_shift = trafo;
auto log_mat = [](const Matrix3d &m) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(4);
ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "],"
<< "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "],"
<< "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]";
return ss.str();
};
auto log_vec3 = [](const Vec3d &v) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(4);
ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")";
return ss.str();
};
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter"
<< " has_rotation=" << has_rotation
<< " has_remap=" << has_remap
<< " trafo.linear=" << log_mat(trafo_pre_shift.linear())
<< " trafo.translation=" << log_vec3(trafo_pre_shift.translation())
<< " volumes=" << model_volumes.size();
#endif
double min_z = std::numeric_limits<double>::max();
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
int vol_idx = 0;
#endif
for (const ModelVolume *mv : model_volumes) {
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
if (!mv->is_model_part()) { ++vol_idx; continue; }
#else
if (!mv->is_model_part()) continue;
#endif
Transform3d vol_trafo = trafo * mv->get_matrix();
const auto &its = mv->mesh().its;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
// Per-volume bbox in mesh-frame and post-trafo slicer-frame.
Vec3d mesh_min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());
Vec3d mesh_max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
Vec3d slicer_min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());
Vec3d slicer_max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
double vol_min_z = std::numeric_limits<double>::max();
#endif
for (const stl_vertex &v : its.vertices) {
Vec3d vm = v.cast<double>();
Vec3d pt = vol_trafo * vm;
min_z = std::min(min_z, pt.z());
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
mesh_min = mesh_min.cwiseMin(vm);
mesh_max = mesh_max.cwiseMax(vm);
slicer_min = slicer_min.cwiseMin(pt);
slicer_max = slicer_max.cwiseMax(pt);
vol_min_z = std::min(vol_min_z, pt.z());
#endif
}
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx
<< "] id=" << mv->id().id << " name='" << mv->name << "'"
<< " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max)
<< " get_matrix.translation=" << log_vec3(mv->get_matrix().translation())
<< " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max)
<< " vol_min_z=" << vol_min_z;
++vol_idx;
#endif
}
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z
<< " z_shift_val=" << z_shift_val;
#endif
if (z_shift_val > 0.) {
Transform3d z_shift = Transform3d::Identity();
z_shift.matrix()(2, 3) = z_shift_val;
trafo = z_shift * trafo;
}
// out_belt_min_z is only meaningful in belt mode; the standalone-remap path
// never reported it.
if (out_belt_min_z && config.belt_printer.value) {
const double new_val = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id()
<< " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val;
#endif
*out_belt_min_z = new_val;
}
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit"
<< " final_trafo.linear=" << log_mat(trafo.linear())
<< " final_trafo.translation=" << log_vec3(trafo.translation());
#endif
}
} // namespace Slic3r

View File

@@ -0,0 +1,36 @@
#pragma once
#include "libslic3r.h"
#include "Point.hpp"
#include "BeltTransform.hpp"
#include "PrintConfig.hpp"
#include "Model.hpp"
namespace Slic3r {
// Belt printer / pre-slice transform strategy.
//
// Composes, in order, the pre-slice mesh transforms applied before slicing:
// 1. Pre-slice axis remap (standalone — works without belt mode)
// 2. Belt rotation (the sole mesh-side belt transform; shear & scale are a
// g-code-side stage, see MachineFrameTransform)
// 3. Per-object Z-shift that lifts the mesh above the build plate
//
// Isolates this belt/remap-specific logic from the generic slicing pipeline in
// PrintObjectSlice.cpp.
class BeltSliceStrategy
{
public:
// Apply the pre-slice remap + belt rotation + Z-shift to `trafo` in place.
// No-op when neither a remap nor a belt rotation is configured.
//
// out_belt_min_z (if non-null) receives the minimum mesh Z after the
// transforms, but only in belt-printer mode — the standalone-remap path
// never reported it.
static void apply_preslice_transforms(Transform3d &trafo,
const PrintConfig &config,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z = nullptr);
};
} // namespace Slic3r

View File

@@ -0,0 +1,223 @@
#include "BeltTransform.hpp"
#include "Model.hpp"
#include <limits>
namespace Slic3r {
// ---- Matrix builders ------------------------------------------------------
Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &config)
{
Transform3d pre_remap = Transform3d::Identity();
if (!has_preslice_remap(config))
return pre_remap;
int pre_rx = int(config.preslice_remap_x.value);
int pre_ry = int(config.preslice_remap_y.value);
int pre_rz = int(config.preslice_remap_z.value);
// Each remap value selects a source axis and sign.
auto remap_column = [](int r) -> Vec3d {
int axis = r % 3;
Vec3d col = Vec3d::Zero();
if (r < 3) col[axis] = 1.0; // +axis
else if (r < 6) col[axis] = -1.0; // -axis
else col[axis] = -1.0; // Rev: max - pos = -(pos - max)
return col;
};
Matrix3d remap_lin;
remap_lin.col(0) = remap_column(pre_rx);
remap_lin.col(1) = remap_column(pre_ry);
remap_lin.col(2) = remap_column(pre_rz);
pre_remap.linear() = remap_lin;
// Translation for Rev modes (needs build volume extents).
if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) {
BoundingBoxf bbox_bed(config.printable_area.values);
Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(),
config.printable_height.value);
Vec3d remap_trans = Vec3d::Zero();
auto add_rev = [&](int r, int out) {
if (r >= 6) remap_trans[out] = vol_max[r % 3];
};
add_rev(pre_rx, 0);
add_rev(pre_ry, 1);
add_rev(pre_rz, 2);
pre_remap.translation() = remap_trans;
}
return pre_remap;
}
Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
{
BeltRotationAxis axis = config.belt_slice_rotation.value;
double angle_deg = config.belt_slice_rotation_angle.value;
bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON;
if (has_rot_out) *has_rot_out = active;
if (!active)
return Matrix3d::Identity();
double angle_rad = Geometry::deg2rad(angle_deg);
Vec3d unit_axis;
switch (axis) {
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
default: return Matrix3d::Identity();
}
return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
}
Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
{
// Mesh-side belt transform: rotation applied after the pre-slice axis remap.
// (Shear & scale are a g-code-side stage, not part of the mesh transform.)
Transform3d pre_remap = build_preslice_remap(config);
Matrix3d rot = build_rotation_matrix(config);
Transform3d combined = Transform3d::Identity();
combined.linear() = rot;
combined = combined * pre_remap;
return combined;
}
// ---- Bounding box remap ---------------------------------------------------
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config)
{
int pre_rx = int(config.preslice_remap_x.value);
int pre_ry = int(config.preslice_remap_y.value);
int pre_rz = int(config.preslice_remap_z.value);
if (pre_rx == int(RemapAxis::PosX) &&
pre_ry == int(RemapAxis::PosY) &&
pre_rz == int(RemapAxis::PosZ))
return bb; // Identity remap.
auto remap_coord = [](int r, const Vec3d &v) -> double {
int axis = r % 3;
if (r < 3) return v[axis];
return -v[axis];
};
Vec3d mn = bb.min.cast<double>(), mx = bb.max.cast<double>();
BoundingBoxf3 rbb;
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? mx.x() : mn.x(),
(i & 2) ? mx.y() : mn.y(),
(i & 4) ? mx.z() : mn.z());
Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c));
if (i == 0) rbb = BoundingBoxf3(rc, rc);
else rbb.merge(rc);
}
return rbb;
}
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const ModelObject &model_object, const PrintConfig &config)
{
return remap_bbox(model_object.raw_bounding_box(), config);
}
// ---- Belt floor parameters ------------------------------------------------
// Shared implementation for both PrintConfig and DynamicPrintConfig.
// Template avoids duplicating the math for the two config types.
namespace {
template<typename Config>
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
const Config &config, const BoundingBoxf3 &bb, double original_height)
{
BeltTransformPipeline::BeltHeightResult result;
result.object_height = original_height;
// Extract the mesh rotation from config (the sole mesh-side belt transform).
BeltRotationAxis rot_axis;
double rot_angle;
if constexpr (std::is_same_v<Config, PrintConfig>) {
rot_axis = config.belt_slice_rotation.value;
rot_angle = config.belt_slice_rotation_angle.value;
} else {
// DynamicPrintConfig path
auto get_float = [&](const char *key) {
auto *opt = config.template option<ConfigOptionFloat>(key);
return opt ? opt->value : 0.0;
};
auto get_rot_axis = [&](const char *key) {
auto *opt = config.template option<ConfigOptionEnum<BeltRotationAxis>>(key);
return opt ? opt->value : BeltRotationAxis::None;
};
rot_axis = get_rot_axis("belt_slice_rotation");
rot_angle = get_float("belt_slice_rotation_angle");
}
bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON;
if (!has_rotation)
return result;
// Rotation path: sweep the 8 bbox corners through R to get the rotated height,
// then derive the belt floor (the image of machine-Z = 0 under R).
double angle_rad = Geometry::deg2rad(rot_angle);
Vec3d unit_axis;
switch (rot_axis) {
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
default: unit_axis = Vec3d::UnitX(); break;
}
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
double min_rz = std::numeric_limits<double>::max();
double max_rz = std::numeric_limits<double>::lowest();
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
(i & 2) ? bb.max.y() : bb.min.y(),
(i & 4) ? bb.max.z() : bb.min.z());
double z = (R * c).z();
min_rz = std::min(min_rz, z);
max_rz = std::max(max_rz, z);
}
result.object_height = max_rz - min_rz;
// Belt floor in slicer-frame is the image of z_machine = 0 under R.
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
switch (rot_axis) {
case BeltRotationAxis::X:
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
result.floor_params.from_axis = 1; // Y
break;
case BeltRotationAxis::Y:
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
result.floor_params.from_axis = 0; // X
break;
case BeltRotationAxis::Z:
default:
result.floor_params.shear_factor = 0.0;
result.floor_params.from_axis = 1;
break;
}
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
return result;
}
} // anonymous namespace
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height)
{
return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height);
}
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height)
{
return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height);
}
} // namespace Slic3r

View File

@@ -0,0 +1,152 @@
#pragma once
#include "libslic3r.h"
#include "Point.hpp"
#include "BoundingBox.hpp"
#include "PrintConfig.hpp"
#include "Geometry.hpp"
#include <cmath>
namespace Slic3r {
class ModelObject;
// Shared belt-printer transform math.
//
// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
// trafo_out = z_shift * rotation * pre_remap * trafo_in
//
// Rotation is the sole mesh-side belt transform; shear & scale are applied
// to the g-code instead (see MachineFrameTransform). This class provides the
// building blocks so every call site uses the same implementation. z_shift is
// object-dependent (computed from mesh vertex bounds) and is NOT included in
// build_forward_transform(). The machine-frame shear/scale is derived directly
// from the tilt angle in MachineFrameTransform and no longer lives here.
//
// Design note: this mesh-rotation approach replaced an earlier pre-shear
// method (now removed). While that initial pre-shear method was instrumental
// in getting belt printer slicing off the ground in the first place, its place is
// in the past. A big thank you goes to the Unlayered3D team, who recommended
// switching to a pre-slice rotation stage instead. Doing so keeps the slicing
// operation isometric — no distortion of the sliced geometry — while the
// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale
// derived from the same tilt angle.
//
// This fixed a number of issues, including several issues noticed by hotcubcar
// regarding adaptive infills not working, gyroid becoming anisotropic, and more
// that were all mostly resolved as a result of the switch.
//
// This also means that the pre-slice rotation transform methodology can be used
// more cleanly on non-belt printers.
// - HarrierPigeon (Joseph Robertson)
class BeltTransformPipeline
{
public:
// ---- Identity checks --------------------------------------------------
static bool has_preslice_remap(const PrintConfig &config)
{
return int(config.preslice_remap_x.value) != int(RemapAxis::PosX) ||
int(config.preslice_remap_y.value) != int(RemapAxis::PosY) ||
int(config.preslice_remap_z.value) != int(RemapAxis::PosZ);
}
// Overload accepting DynamicPrintConfig (used in static slicing_parameters).
static bool has_preslice_remap(const DynamicPrintConfig &config)
{
auto get_int = [&](const char *key) -> int {
auto *opt = config.option<ConfigOptionEnum<RemapAxis>>(key);
return opt ? int(opt->value) : 0;
};
return get_int("preslice_remap_x") != int(RemapAxis::PosX) ||
get_int("preslice_remap_y") != int(RemapAxis::PosY) ||
get_int("preslice_remap_z") != int(RemapAxis::PosZ);
}
static bool has_rotation(const PrintConfig &config)
{
return config.belt_slice_rotation.value != BeltRotationAxis::None &&
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
}
// Physical belt tilt derived from the slicing rotation — the single source of
// truth for bed rendering, support gravity tilt and the bed-exclusion
// projection. Returns the tilt magnitude in degrees split onto the X and Y
// build-plate tilt axes according to the rotation axis:
// rotation about X → tilt_x = angle (gantry tilts in the YZ plane)
// rotation about Y → tilt_y = angle (gantry tilts in the XZ plane)
// rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt)
// The magnitude uses abs(angle) so a negative rotation still reports a positive
// physical tilt.
struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; };
static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg)
{
PhysicalTilt t;
double mag = std::abs(angle_deg);
switch (axis) {
case BeltRotationAxis::X: t.tilt_x_deg = mag; break;
case BeltRotationAxis::Y: t.tilt_y_deg = mag; break;
default: break; // Z / None: no physical tilt
}
return t;
}
static PhysicalTilt physical_tilt(const PrintConfig &config)
{
return physical_tilt(config.belt_slice_rotation.value,
config.belt_slice_rotation_angle.value);
}
// ---- Matrix builders --------------------------------------------------
// Build the pre-slice axis remap transform (includes Rev-mode translation).
static Transform3d build_preslice_remap(const PrintConfig &config);
// Build the 3x3 rotation matrix from belt_slice_rotation* config.
// Returns Identity if rotation axis is None or angle is ~0.
// Also sets has_rot_out if non-null.
static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
// Combined forward transform (rotation * pre_remap) — the mesh-side belt
// transform that BeltSliceStrategy applies and BeltBackTransform inverts.
// Does NOT include the per-object Z-shift.
static Transform3d build_forward_transform(const PrintConfig &config);
// ---- Bounding box remap -----------------------------------------------
// Remap a bounding box through the pre-slice axis remap.
// Returns the original bbox if remap is identity.
static BoundingBoxf3 remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config);
static BoundingBoxf3 remap_bbox(const ModelObject &model_object, const PrintConfig &config);
// ---- Belt floor parameters --------------------------------------------
struct BeltFloorParams {
double shear_factor = 0.0;
int from_axis = 1;
double z_shift = 0.0;
};
// Result of computing belt height + floor params.
struct BeltHeightResult {
double object_height; // Effective object height after shear/scale
BeltFloorParams floor_params;
};
// Compute effective object height and belt floor parameters from config
// and pre-remapped bounding box. original_height is the input height
// (bb.size().z() or model_object.max_z()).
static BeltHeightResult compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &remapped_bbox,
double original_height);
// Overload for DynamicPrintConfig (used by static slicing_parameters).
static BeltHeightResult compute_belt_height_and_floor(
const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox,
double original_height);
};
} // namespace Slic3r

View File

@@ -25,7 +25,7 @@ public:
min(p1), max(p1), defined(false) { merge(p2); merge(p3); }
template<class It, class = IteratorOnly<It>>
BoundingBoxBase(It from, It to) : BoundingBoxBase()
BoundingBoxBase(It from, It to)
{ construct(*this, from, to); }
BoundingBoxBase(const PointsType &points)

View File

@@ -349,7 +349,7 @@ static ExPolygons make_brim_ears_auto(const ExPolygons& obj_expoly, coord_t size
return mouse_ears_ex;
}
static ExPolygons make_brim_ears(const PrintObject* object)
static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWidth, float brim_offset, Flow &flow, bool is_outer_brim)
{
ExPolygons mouse_ears_ex;
BrimPoints brim_ear_points = object->model_object()->brim_points;
@@ -373,7 +373,12 @@ static ExPolygons make_brim_ears(const PrintObject* object)
Vec3f world_pos = pt.transform(trsf.get_matrix());
if ( world_pos.z() > 0) continue;
Polygon point_round;
const coord_t size_ear = scale_(pt.head_front_radius);
float brim_width = floor(scale_(pt.head_front_radius) / flowWidth / 2) * flowWidth * 2;
if (is_outer_brim) {
double flowWidthScale = flowWidth / SCALING_FACTOR;
brim_width = floor(brim_width / flowWidthScale / 2) * flowWidthScale * 2;
}
coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing());
for (size_t i = 0; i < POLY_SIDE_COUNT; i++) {
double angle = (2.0 * PI * i) / POLY_SIDE_COUNT;
point_round.points.emplace_back(size_ear * cos(angle), size_ear * sin(angle));
@@ -447,8 +452,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
bool has_brim_auto = object->config().brim_type == btAutoBrim;
const bool use_auto_brim_ears = object->config().brim_type == btEar;
const bool use_brim_ears = object->config().brim_type == btPainted;
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_auto_brim_ears || use_brim_ears;
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
@@ -527,7 +531,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
auto innerExpoly = offset_ex(ex_poly.contour, brim_offset, jtRound, SCALED_RESOLUTION);
ExPolygons outerExpoly;
if (use_brim_ears) {
outerExpoly = make_brim_ears(object);
outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, true);
//outerExpoly = offset_ex(outerExpoly, brim_width_mod, jtRound, SCALED_RESOLUTION);
} else if (use_auto_brim_ears) {
coord_t size_ear = (brim_width_mod - brim_offset - flow.scaled_spacing());
@@ -541,7 +545,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
ExPolygons outerExpoly;
auto innerExpoly = offset_ex(ex_poly_holes_reversed, -brim_width - brim_offset);
if (use_brim_ears) {
outerExpoly = make_brim_ears(object);
outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, false);
} else if (use_auto_brim_ears) {
coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing());
outerExpoly = make_brim_ears_auto(offset_ex(ex_poly_holes_reversed, -brim_offset), size_ear, ear_detection_length, brim_ears_max_angle, false);
@@ -864,6 +868,10 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
std::vector<unsigned int>& printExtruders,
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut)
{
// Belt printer: brim is not compatible with belt printing.
if (print.config().belt_printer.value)
return;
std::map<ObjectInstanceID, ExPolygons> brimAreaMap;
Flow flow = print.brim_flow();
ExPolygons islands_area_ex = outer_inner_brim_area(print,

View File

@@ -176,6 +176,31 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name();
}
void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y)
{
m_is_belt_printer = enabled;
m_belt_angle = angle_deg;
m_belt_infinite_y = infinite_y;
// Restart from the unmodified bbox each call. Without this, toggling
// belt mode off (or switching infinite_y true→false) would leave the
// extents inflated and break collision / object_state checks.
BoundingBoxf bboxf = get_extents(m_bed_shape);
m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) };
if (enabled) {
if (infinite_y) {
// Extend the Y bound to a very large value for infinite belt.
m_bboxf.max.y() = 100000.;
}
// Belt printer: the Z extent already equals printable_height (set above), which
// is the usable vertical clearance above the belt. The gantry's axis range is
// sized to reach height/cos(tilt), so no diagonal scaling is applied here — this
// keeps the live "outside build volume" highlight in agreement with Print::validate().
(void) angle_deg;
}
}
#if 0
// Tests intersections of projected triangles, not just their vertices against a bounding box.
// This test also correctly evaluates collision of a non-convex object with the bounding box.
@@ -384,6 +409,11 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i
build_volume.max.z() = std::numeric_limits<double>::max();
if (ignore_bottom)
build_volume.min.z() = -std::numeric_limits<double>::max();
// Belt printer: extend Y bounds for infinite Y.
if (m_is_belt_printer && m_belt_infinite_y) {
build_volume.min.y() = -std::numeric_limits<double>::max();
build_volume.max.y() = std::numeric_limits<double>::max();
}
BoundingBox3Base<Vec3f> build_volumef(build_volume.min.cast<float>(), build_volume.max.cast<float>());
// The following test correctly interprets intersection of a non-convex object with a rectangular build volume.
//return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z());

View File

@@ -57,6 +57,10 @@ public:
// Initialize from PrintConfig::printable_area and PrintConfig::printable_height
BuildVolume(const std::vector<Vec2d> &printable_area, const double printable_height, const std::vector<std::vector<Vec2d>> &extruder_areas, const std::vector<double>& extruder_printable_heights);
// Belt printer configuration.
void set_belt_printer(bool enabled, double angle_deg, bool infinite_y);
bool is_belt_printer() const { return m_is_belt_printer; }
// Source data, unscaled coordinates.
const std::vector<Vec2d>& printable_area() const { return m_bed_shape; }
double printable_height() const { return m_max_print_height; }
@@ -80,7 +84,7 @@ public:
indexed_triangle_set bounding_mesh(bool scale=true) const;
// Center of the print bed, unscaled.
Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
// Convex hull of polygon(), scaled.
const Polygon& convex_hull() const { return m_convex_hull; }
// Smallest enclosing circle of polygon(), scaled.
@@ -139,6 +143,10 @@ private:
// Source definition of the print volume height (PrintConfig::printable_height)
double m_max_print_height { 0.f };
std::vector<double> m_extruder_printable_height;
// Belt printer state.
bool m_is_belt_printer { false };
double m_belt_angle { 0. };
bool m_belt_infinite_y { false };
// Derived values.
BuildVolume_Type m_type { BuildVolume_Type::Invalid };

View File

@@ -80,6 +80,16 @@ set(lisbslic3r_sources
BoundingBox.hpp
BridgeDetector.cpp
BridgeDetector.hpp
BeltGCode.cpp
BeltGCode.hpp
BeltGCodeWriter.cpp
BeltGCodeWriter.hpp
BeltSliceStrategy.cpp
BeltSliceStrategy.hpp
BeltTransform.cpp
BeltTransform.hpp
FirstLayerPlane.cpp
FirstLayerPlane.hpp
Brim.cpp
BrimEarsPoint.hpp
Brim.hpp
@@ -210,6 +220,10 @@ set(lisbslic3r_sources
GCode/AdaptivePAProcessor.hpp
GCode/AvoidCrossingPerimeters.cpp
GCode/AvoidCrossingPerimeters.hpp
GCode/BeltBackTransform.cpp
GCode/BeltBackTransform.hpp
GCode/MachineFrameTransform.cpp
GCode/MachineFrameTransform.hpp
GCode/ConflictChecker.cpp
GCode/ConflictChecker.hpp
GCode/CoolingBuffer.cpp
@@ -418,6 +432,8 @@ set(lisbslic3r_sources
SlicingAdaptive.hpp
Slicing.cpp
Slicing.hpp
Support/BeltFloorContext.cpp
Support/BeltFloorContext.hpp
Support/SupportCommon.cpp
Support/SupportCommon.hpp
Support/SupportLayer.hpp

View File

@@ -2273,8 +2273,6 @@ public:
plugin_picker,
// Raw JSON string value, edited through a dialog behind a button rather than in the row.
plugin_config,
// PrinterAgentChoice
printer_agent_select,
};
// Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map.

View File

@@ -396,6 +396,11 @@ inline void translate(ExPolygons &expolys, const Point &p) {
expoly.translate(p);
}
inline void translate(Polygons &polys, const Point &p) {
for (Polygon &poly : polys)
poly.translate(p);
}
inline void polygons_append(Polygons &dst, const ExPolygon &src)
{
dst.reserve(dst.size() + src.holes.size() + 1);

View File

@@ -278,9 +278,6 @@ struct SurfaceFillParams
// For Gyroid: when true, use the parameterized "optimized" wave.
bool gyroid_optimized = false;
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
bool separated_infills{false};
@@ -319,7 +316,6 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, fill_order);
@@ -352,7 +348,6 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized &&
this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order;
}
};
@@ -969,11 +964,6 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
region_config.sparse_infill_rotate_template.value);
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
// Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill.
// FillHilbertCurve::generate clamps and validates the value itself.
if (params.pattern == ipHilbertCurve)
params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value;
} else {
const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.;
const bool bottom_layer_direction_set = surface.is_bottom() && region_config.bottom_layer_direction.value >= 0.;
@@ -1338,7 +1328,6 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.smooth_factor = surface_fill.params.smooth_factor;
// BBS
params.flow = surface_fill.params.flow;
@@ -1580,7 +1569,6 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.smooth_factor = surface_fill.params.smooth_factor;
for (ExPolygon &expoly : surface_fill.expolygons) {
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.

View File

@@ -1857,12 +1857,12 @@ static inline void base_support_extend_infill_lines(Polylines &infill, BoundaryI
const bool first = graph.first(cp);
int extend_next_idx = -1;
int extend_prev_idx = -1;
coord_t dist_y_prev = 0;
coord_t dist_y_next = 0;
double arc_len_prev = 0;
double arc_len_next = 0;
coord_t dist_y_prev;
coord_t dist_y_next;
double arc_len_prev;
double arc_len_next;
if (! graph.next_vertical(cp)) {
if (! graph.next_vertical(cp)){
size_t i = cp.point_idx;
size_t j = next_idx_modulo(i, contour);
while (j != cp.next_on_contour->point_idx) {

View File

@@ -82,9 +82,6 @@ struct FillParams
// For Gyroid: when true, use the parameterized "optimized" variant.
bool gyroid_optimized { false };
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
// For Lateral lattice
coordf_t lateral_lattice_angle_1 { 0.f };
coordf_t lateral_lattice_angle_2 { 0.f };

View File

@@ -114,12 +114,12 @@ void FillPlanePath::_fill_surface_single(
// Filling in a bounding box over the whole object, clip generated polyline against the snug bounding box.
snug_bounding_box.translate(-shift.x(), -shift.y());
InfillPolylineClipper output(snug_bounding_box, distance_between_lines);
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
this->generate(min_x, min_y, max_x, max_y, resolution, output);
polyline.points = std::move(output.result());
} else {
// Filling in a snug bounding box, no need to clip.
InfillPolylineOutput output(distance_between_lines);
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
this->generate(min_x, min_y, max_x, max_y, resolution, output);
polyline.points = std::move(output.result());
}
}
@@ -288,147 +288,6 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x,
}
}
using QuinticBezier = std::array<Vec2d, 6>;
static bool is_bezier_flat(const QuinticBezier &curve, const double deviation)
{
// A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every
// control point within a deviation-wide strip around the endpoint chord conservatively bounds the
// flattening error. The cross product is the perpendicular distance scaled by the chord length;
// comparing squared values avoids a square root.
const Vec2d chord = curve.back() - curve.front();
const double chord_length_sq = chord.squaredNorm();
const double max_cross_sq = deviation * deviation * chord_length_sq;
for (size_t i = 1; i + 1 < curve.size(); ++i) {
const Vec2d offset = curve[i] - curve.front();
const double cross = chord.x() * offset.y() - chord.y() * offset.x();
if (cross * cross > max_cross_sq)
return false;
}
return true;
}
static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right)
{
// Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one
// control point to the left half and one to the right half; the latter is filled backwards to keep
// both resulting control polygons in their original parameter direction.
QuinticBezier subdivision = curve;
left.front() = subdivision.front();
right.back() = subdivision.back();
for (size_t level = 1; level < curve.size(); ++level) {
for (size_t i = 0; i + level < curve.size(); ++i)
subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]);
left[level] = subdivision.front();
right[curve.size() - level - 1] = subdivision[curve.size() - level - 1];
}
}
static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector<Vec2d> &output)
{
// Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord.
// A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth,
// avoiding abrupt segment-length jumps at adaptive-depth boundaries.
static constexpr size_t max_depth = 16;
std::vector<QuinticBezier> subcurves(2);
subdivide_bezier(curve, subcurves[0], subcurves[1]);
for (size_t depth = 1; depth < max_depth; ++depth) {
bool all_flat = true;
for (const QuinticBezier &c : subcurves)
if (!is_bezier_flat(c, deviation)) {
all_flat = false;
break;
}
if (all_flat)
break;
std::vector<QuinticBezier> finer(subcurves.size() * 2);
for (size_t i = 0; i < subcurves.size(); ++i)
subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]);
subcurves = std::move(finer);
}
// The curve start is deliberately omitted so consecutive curve pieces can share it without duplication.
output.reserve(output.size() + subcurves.size());
for (const QuinticBezier &c : subcurves)
output.emplace_back(c.back());
}
template<typename Output>
static void generate_smooth_hilbert_curve(
coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const double corner_distance, Output &output)
{
// A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed
// generator, expand the larger requested dimension to the next valid Hilbert grid size. The output
// clipper or the later region intersection removes the padded part of the traversal.
size_t sz = 2;
const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y);
while (sz < sz0)
sz <<= 1;
const size_t point_count = sz * sz;
output.reserve(point_count);
// The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance
// if this helper is invoked with an invalid resolution.
const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON;
// Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance).
// At each end, the first three control points are collinear and equally spaced: the tangent follows
// the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore
// zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten
// it only once to the requested chordal-deviation tolerance.
const QuinticBezier corner_curve {{
{-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.},
{0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance}
}};
std::vector<Vec2d> curve_coefficients;
flatten_bezier(corner_curve, deviation, curve_coefficients);
auto translated_point = [min_x, min_y](size_t idx) {
Point p = hilbert_n_to_xy(idx);
return Point(p.x() + min_x, p.y() + min_y);
};
auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); };
bool has_last_output = false;
Vec2d last_output;
// Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates
// to avoid emitting zero-length extrusion segments.
auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) {
if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) {
output.add_point(point);
last_output = point;
has_last_output = true;
}
};
Vec2d previous = to_vec2d(translated_point(0));
Vec2d corner = to_vec2d(translated_point(1));
add_point(previous);
// Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of
// its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline.
for (size_t i = 1; i + 1 < point_count; ++i) {
const Vec2d next = to_vec2d(translated_point(i + 1));
const Vec2d incoming = (corner - previous).normalized();
const Vec2d outgoing = (next - corner).normalized();
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
if (std::abs(cross) < EPSILON) {
add_point(corner);
} else {
add_point(corner - corner_distance * incoming);
for (const Vec2d &coefficient : curve_coefficients)
add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing);
}
previous = corner;
corner = next;
}
add_point(corner);
}
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output)
{
if (output.clips())
@@ -437,24 +296,6 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo
generate_hilbert_curve(min_x, min_y, max_x, max_y, output);
}
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams &params, InfillPolylineOutput &output)
{
const double smooth_factor = std::isfinite(params.smooth_factor) ?
std::clamp(params.smooth_factor, 0., 1.) : 0.;
if (smooth_factor == 0.) {
this->generate(min_x, min_y, max_x, max_y, resolution, output);
return;
}
const double corner_distance = 0.5 * smooth_factor;
if (output.clips())
generate_smooth_hilbert_curve(
min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast<InfillPolylineClipper&>(output));
else
generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output);
}
template<typename Output>
static void generate_octagram_spiral(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, Output &output)
{

View File

@@ -53,11 +53,6 @@ protected:
friend class InfillPolylineClipper;
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) = 0;
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams & /* params */, InfillPolylineOutput &output)
{
this->generate(min_x, min_y, max_x, max_y, resolution, output);
}
};
class FillArchimedeanChords : public FillPlanePath
@@ -80,8 +75,6 @@ public:
protected:
bool centered() const override { return false; }
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override;
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams &params, InfillPolylineOutput &output) override;
};
class FillOctagramSpiral : public FillPlanePath

View File

@@ -0,0 +1,225 @@
#include "FirstLayerPlane.hpp"
#include "BeltTransform.hpp"
#include <algorithm>
#include <climits>
#include <cmath>
namespace Slic3r {
namespace {
// Build the row of the gcode-axis-remap matrix R that produces machine_Z,
// AS A FUNCTION OF a slicing-frame point in the GCode generator's coordinate
// space. Without back-transform this is just R.row(2). With back-transform
// the writer applies F^-1 before R, so the effective row is (R * F^-1).row(2).
//
// Returns a pair (gradient, constant) such that:
// machine_Z(p_slicing) = gradient.dot(p_slicing) + constant
struct MachineZAffine {
Vec3d gradient = Vec3d::UnitZ();
double constant = 0.0;
};
MachineZAffine compute_machine_z_affine(const PrintConfig &config)
{
MachineZAffine out;
// R is the matrix form of GCodeWriter::apply_axis_remap. Each output axis
// i picks one slicing-frame component (with sign + optional Rev mode
// translation) based on m_remap_{x,y,z}. We only need row 2 (the z output)
// since machine_Z is what defines the first-layer plane.
int rz = int(config.gcode_remap_z.value);
int axis = rz % 3;
double sign;
double trans;
if (rz < int(RemapAxis::NegX)) { // 0..2 = PosX/Y/Z
sign = 1.0;
trans = 0.0;
} else if (rz < int(RemapAxis::RevX)) { // 3..5 = NegX/Y/Z
sign = -1.0;
trans = 0.0;
} else { // 6..8 = RevX/Y/Z
sign = -1.0;
BoundingBoxf bbox_bed(config.printable_area.values);
Vec3d vol_max(bbox_bed.max.x(),
bbox_bed.max.y(),
config.printable_height.value);
trans = vol_max[axis];
}
Vec3d r_row = Vec3d::Zero();
r_row[axis] = sign;
// Without back-transform, machine_Z(slicing) = r_row · slicing + trans.
out.gradient = r_row;
out.constant = trans;
if (config.gcode_back_transform.value && config.belt_printer.value) {
// BeltGCodeWriter applies F^-1 before R when back-transform is on.
// So machine_Z(slicing) = r_row · (F^-1 · slicing) + trans
// = (r_row^T · F^-1) · slicing + trans
// We need to compose r_row with F^-1 from the LEFT (treating r_row as
// a row vector). Eigen makes this easy: it's just F^-1.transpose() * r_row.
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
Transform3d inverse = forward.inverse();
// Note: forward.translation() is normally zero (per-print transforms
// don't add a translation; the per-object z_shift is added separately
// in PrintObjectSlice). We still incorporate inverse.translation() in
// case a Rev-mode preslice_remap puts a translation in F.
Vec3d composed_grad = inverse.linear().transpose() * r_row;
double composed_trans =
r_row.dot(inverse.translation()) + trans;
out.gradient = composed_grad;
out.constant = composed_trans;
}
return out;
}
} // namespace
FirstLayerPlane::FirstLayerPlane(const PrintConfig &config)
{
// -------- Resolve Auto -------------------------------------------------
FirstLayerPlaneMode mode = config.first_layer_plane.value;
if (mode == FirstLayerPlaneMode::Auto) {
bool belt_affine_active = config.belt_printer.value &&
config.belt_slice_rotation.value != BeltRotationAxis::None &&
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
mode = belt_affine_active ? FirstLayerPlaneMode::BeltAffine
: FirstLayerPlaneMode::XY;
}
m_mode = mode;
// -------- Band thickness ----------------------------------------------
// Note: layer_height lives in PrintObjectConfig, not PrintConfig, so we
// can't fall back to it from here. initial_layer_print_height is in
// PrintConfig and is the right default anyway (the legacy first-layer
// semantics used initial_layer_print_height, not the regular one).
double thickness = config.first_layer_plane_thickness.value;
if (thickness <= 0.0)
thickness = config.initial_layer_print_height.value;
if (thickness <= 0.0)
thickness = 0.2;
m_thickness_mm = thickness;
const double user_offset = config.first_layer_plane_offset.value;
// -------- Build the plane ---------------------------------------------
auto set_axis_aligned = [&](const Vec3d &n_unit, double offset_along_n) {
m_normal = n_unit;
m_offset = offset_along_n;
};
switch (mode) {
case FirstLayerPlaneMode::XY:
// Legacy XY plane. Inactive: short-circuit to layer-index path.
set_axis_aligned(Vec3d::UnitZ(), user_offset);
m_active = false;
return;
case FirstLayerPlaneMode::YZ:
set_axis_aligned(Vec3d::UnitX(), user_offset);
m_active = true;
return;
case FirstLayerPlaneMode::XZ:
set_axis_aligned(Vec3d::UnitY(), user_offset);
m_active = true;
return;
case FirstLayerPlaneMode::BeltAffine: {
// Compute the slicing-frame plane that maps to machine_Z = user_offset
// under the gcode axis remap (and optional back-transform).
MachineZAffine mz = compute_machine_z_affine(config);
double cmag = mz.gradient.norm();
if (cmag < EPSILON) {
// Degenerate: slicing point doesn't affect machine_Z. Fall back.
set_axis_aligned(Vec3d::UnitZ(), user_offset);
m_active = false;
return;
}
// Plane equation: gradient · slicing = user_offset - constant
const double K = user_offset - mz.constant;
m_normal = mz.gradient / cmag;
m_offset = K / cmag;
m_active = true;
return;
}
case FirstLayerPlaneMode::Auto:
// Should have been resolved above.
m_active = false;
return;
}
m_active = false;
}
double FirstLayerPlane::distance_from_plane(const Vec3d &point_slicing_mm) const
{
return m_normal.dot(point_slicing_mm) - m_offset;
}
bool FirstLayerPlane::is_first_layer(const Vec3d &point_slicing_mm,
double first_layer_height_mm) const
{
if (!m_active)
return false;
return distance_from_plane(point_slicing_mm) < first_layer_height_mm;
}
int FirstLayerPlane::effective_layer_index(const Vec3d &point_slicing_mm) const
{
if (!m_active)
return INT_MAX / 2; // Effectively "way past first layer".
double d = distance_from_plane(point_slicing_mm);
if (d <= 0.0)
return 0;
return int(std::floor(d / m_thickness_mm));
}
int FirstLayerPlane::min_effective_index_for_xy_bbox(
const BoundingBoxf &xy_bbox_mm, double slicing_z_mm) const
{
if (!m_active)
return INT_MAX / 2;
// For the rectangular bbox in (x, y) at fixed z, the smallest value of
// (n.x*x + n.y*y + n.z*z - offset) is achieved at one of the four
// corners, with the smaller component picked when the corresponding
// normal coefficient is positive.
const double x_for_min = (m_normal.x() >= 0.0)
? xy_bbox_mm.min.x() : xy_bbox_mm.max.x();
const double y_for_min = (m_normal.y() >= 0.0)
? xy_bbox_mm.min.y() : xy_bbox_mm.max.y();
const double dmin = m_normal.x() * x_for_min
+ m_normal.y() * y_for_min
+ m_normal.z() * slicing_z_mm
- m_offset;
if (dmin <= 0.0)
return 0;
return int(std::floor(dmin / m_thickness_mm));
}
int FirstLayerPlane::min_effective_index_for_bbox3(
const BoundingBoxf3 &bbox_mm) const
{
if (!m_active)
return INT_MAX / 2;
const double x_for_min = (m_normal.x() >= 0.0)
? bbox_mm.min.x() : bbox_mm.max.x();
const double y_for_min = (m_normal.y() >= 0.0)
? bbox_mm.min.y() : bbox_mm.max.y();
const double z_for_min = (m_normal.z() >= 0.0)
? bbox_mm.min.z() : bbox_mm.max.z();
const double dmin = m_normal.x() * x_for_min
+ m_normal.y() * y_for_min
+ m_normal.z() * z_for_min
- m_offset;
if (dmin <= 0.0)
return 0;
return int(std::floor(dmin / m_thickness_mm));
}
} // namespace Slic3r

View File

@@ -0,0 +1,76 @@
#ifndef slic3r_FirstLayerPlane_hpp_
#define slic3r_FirstLayerPlane_hpp_
#include "libslic3r.h"
#include "Point.hpp"
#include "BoundingBox.hpp"
#include "PrintConfig.hpp"
namespace Slic3r {
// Decides which extrusions get "first layer" treatment (no fan, slow speed,
// initial-layer accel/jerk, deferred temperature drop) by reference to a
// configurable plane in slicing-frame coordinates rather than the slicing
// layer index.
//
// On a normal flat-bed printer the plane is XY at slicing_Z = 0 and the
// evaluator is INACTIVE — every call site short-circuits back to the legacy
// `Layer::id() == 0` test. On a belt printer with a Z-from-Y shear the
// belt surface (machine_Z = 0) maps to a plane in slicing-frame coordinates
// derived from the gcode axis remap, so layer-index-based detection no
// longer matches the physical first printed surface.
//
// Plane representation: unit normal `n` (slicing frame) and offset along
// the normal such that the plane equation is `n · p == offset`. Signed
// perpendicular distance is `d(p) = n · p - offset`. Positive distance
// means "away from the belt surface", negative means "below the plane".
class FirstLayerPlane
{
public:
explicit FirstLayerPlane(const PrintConfig &config);
// Inactive when the legacy XY layer-index path should be used. This
// covers all non-belt printers and any belt printer where the user
// explicitly picked XY mode.
bool is_active() const { return m_active; }
FirstLayerPlaneMode effective_mode() const{ return m_mode; }
double band_thickness_mm() const { return m_thickness_mm; }
const Vec3d & normal() const { return m_normal; }
double plane_offset() const { return m_offset; }
// Signed perpendicular distance from a slicing-frame point to the plane.
double distance_from_plane(const Vec3d &point_slicing_mm) const;
// True if perpendicular distance < first_layer_height_mm. When the
// evaluator is inactive this returns false (call sites should fall back
// to the legacy per-layer path before reaching this function).
bool is_first_layer(const Vec3d &point_slicing_mm,
double first_layer_height_mm) const;
// floor((distance - 0) / band_thickness), clamped to [0, +inf). Used
// for "first N layers" thresholds (fan, slow_down_layers). Returns 0
// for points within the band. Returns INT_MAX/2 when inactive.
int effective_layer_index(const Vec3d &point_slicing_mm) const;
// Min effective index over a 2D bbox at a fixed slicing_Z. Used for
// layer-level decisions (e.g. temperature transition gate) where we
// don't want to walk every extrusion in the layer. For axis-aligned
// planes this is exact; for tilted planes it's a tight lower bound
// (the plane projection of the bbox's extreme corner).
int min_effective_index_for_xy_bbox(const BoundingBoxf &xy_bbox_mm,
double slicing_z_mm) const;
// Same as above but the bbox spans a Z range too.
int min_effective_index_for_bbox3(const BoundingBoxf3 &bbox_mm) const;
private:
bool m_active = false;
FirstLayerPlaneMode m_mode = FirstLayerPlaneMode::XY;
Vec3d m_normal = Vec3d::UnitZ(); // unit, slicing frame
double m_offset = 0.0; // n·p == m_offset
double m_thickness_mm = 0.0;
};
} // namespace Slic3r
#endif // slic3r_FirstLayerPlane_hpp_

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,8 @@
#include "libslic3r.h"
#include "ExPolygon.hpp"
#include "GCodeWriter.hpp"
#include "BeltGCodeWriter.hpp"
#include "FirstLayerPlane.hpp"
#include "Layer.hpp"
#include "Point.hpp"
#include "PlaceholderParser.hpp"
@@ -184,7 +186,7 @@ struct LayerResult {
// It is used for the pressure equalizer because it needs to buffer one layer back.
bool nop_layer_result { false };
static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<size_t>::max(), false, false, true}; }
static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<coord_t>::max(), false, false, true}; }
};
class GCode {
@@ -214,16 +216,18 @@ public:
m_last_obj_copy(nullptr, Point(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max())),
// BBS
m_toolchange_count(0),
m_nominal_z(0.)
m_nominal_z(0.),
m_writer(std::make_unique<GCodeWriter>())
{}
~GCode() = default;
virtual ~GCode() = default;
public:
// throws std::runtime_exception on error,
// throws CanceledException through print->throw_if_canceled().
void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
void export_layer_filaments(GCodeProcessorResult* result);
//BBS: set offset for gcode writer
void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
// Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests.
const Vec2d& origin() const { return m_origin; }
@@ -237,8 +241,8 @@ public:
Vec3d point_to_gcode_quantized(const Point3& point) const;
const FullPrintConfig &config() const { return m_config; }
const Layer* layer() const { return m_layer; }
GCodeWriter& writer() { return m_writer; }
const GCodeWriter& writer() const { return m_writer; }
GCodeWriter& writer() { return *m_writer; }
const GCodeWriter& writer() const { return *m_writer; }
PlaceholderParser& placeholder_parser() { return m_placeholder_parser_integration.parser; }
const PlaceholderParser& placeholder_parser() const { return m_placeholder_parser_integration.parser; }
// Process a template through the placeholder parser, collect error messages to be reported
@@ -261,7 +265,7 @@ public:
bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type);
std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone);
// extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract.
std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); }
std::string unretract(float extra_retract = 0.f) { return m_writer->unlift() + m_writer->unretract(extra_retract); }
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1);
bool is_BBL_Printer();
WipeTowerType wipe_tower_type();
@@ -319,7 +323,15 @@ public:
}
};
private:
// Public accessor for the first-layer plane evaluator. Used by
// CoolingBuffer (which is constructed with a GCode reference and needs
// to read the plane for per-segment fan re-evaluation). All other
// first-layer-plane access points (on_first_layer overload, effective
// index helper) are in the protected section since they're called from
// GCode internals only.
const FirstLayerPlane *first_layer_plane() const { return m_first_layer_plane.get(); }
protected:
class GCodeOutputStream {
public:
GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {}
@@ -347,9 +359,17 @@ private:
FILE *f = nullptr;
GCodeProcessor &m_processor;
};
// Virtual hooks for belt printer subclass (BeltGCode).
// No-ops in base GCode; overridden in BeltGCode.
virtual void init_belt_writer(Print &print, bool is_bbl_printers) {}
virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {}
virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {}
virtual bool should_disable_arc_fitting() const { return false; }
void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb);
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object);
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false);
static std::vector<std::pair<coordf_t, std::vector<LayerToPrint>>> collect_layers_to_print(const Print &print);
std::string generate_skirt(const Print &print,
@@ -587,7 +607,7 @@ private:
DynamicConfig m_calib_config;
// scaled G-code resolution
double m_scaled_resolution;
GCodeWriter m_writer;
std::unique_ptr<GCodeWriter> m_writer;
struct PlaceholderParserIntegration {
void reset();
@@ -707,6 +727,11 @@ private:
std::unique_ptr<CoolingBuffer> m_cooling_buffer;
std::unique_ptr<SpiralVase> m_spiral_vase;
// First-layer plane evaluator. Constructed once per print from the
// PrintConfig. is_active() == false on non-belt printers and on belt
// printers without a Z-axis shear; in that case all per-path plane
// checks short-circuit to the legacy Layer::id() == 0 path.
std::unique_ptr<FirstLayerPlane> m_first_layer_plane;
std::unique_ptr<PressureEqualizer> m_pressure_equalizer;
@@ -772,6 +797,25 @@ private:
// On the first printing layer. This flag triggers first layer speeds.
//BBS
bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0 && abs(m_layer->bottom_z()) < EPSILON; }
// Per-point first-layer test. When the FirstLayerPlane evaluator is
// active, the result depends on the supplied slicing-frame point;
// otherwise we delegate to the legacy per-layer test. This is the
// entry point used by per-path call sites in _extrude.
bool on_first_layer(const Vec3d &point_slicing_mm) const {
if (m_first_layer_plane && m_first_layer_plane->is_active())
return m_first_layer_plane->is_first_layer(
point_slicing_mm, m_config.initial_layer_print_height.value);
return on_first_layer();
}
// "Effective layer index" used to drive layer-count thresholds like
// slow_down_layers. When the evaluator is active this returns the
// perpendicular distance to the plane in band_thickness_mm units;
// otherwise it returns the legacy slicing layer index.
int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const {
if (m_first_layer_plane && m_first_layer_plane->is_active())
return m_first_layer_plane->effective_layer_index(point_slicing_mm);
return on_first_layer() ? 0 : layer_id();
}
int layer_id() const {
if (m_layer == nullptr)
return -1;

View File

@@ -0,0 +1,40 @@
#include "BeltBackTransform.hpp"
#include "../BeltTransform.hpp"
namespace Slic3r {
bool BeltBackTransform::init_from_config(const PrintConfig &config)
{
m_active = false;
m_inverse = Transform3d::Identity();
if (!config.belt_printer.value || !config.gcode_back_transform.value)
return false;
// Require at least one active transform to proceed.
bool has_global_rotation = config.belt_slice_rotation_global.value
&& config.belt_slice_rotation.value != BeltRotationAxis::None;
bool has_preslice_global = config.belt_preslice_global.value
|| config.preslice_remap_global.value;
if (!has_global_rotation && !has_preslice_global
&& !BeltTransformPipeline::has_preslice_remap(config))
return false;
// Build the forward pipeline (rotation * pre_remap) and store its inverse.
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
if (forward.isApprox(Transform3d::Identity()))
return false;
m_inverse = forward.inverse();
m_active = true;
return true;
}
Vec3d BeltBackTransform::apply(const Vec3d &pos) const
{
if (!m_active)
return pos;
return m_inverse * pos;
}
} // namespace Slic3r

View File

@@ -0,0 +1,45 @@
#ifndef slic3r_BeltBackTransform_hpp_
#define slic3r_BeltBackTransform_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Reverses the pre-slice remap + shear + scale transforms that
// PrintObjectSlice.cpp applies to belt printer geometry, converting G-code
// coordinates from the sliced (remapped/sheared/scaled) frame back to the
// machine's real coordinate space.
//
// Initialized once from PrintConfig, then applied per-point in
// GCodeWriter::to_machine_coords() before axis remapping.
//
// Active when gcode_back_transform is true AND at least one of:
// - a shear axis has global mode enabled, or
// - a pre-slice axis remap is non-identity.
class BeltBackTransform
{
public:
BeltBackTransform() = default;
// Initialize from belt printer config. Rebuilds the same pre-slice remap,
// shear, and scale matrices as PrintObjectSlice.cpp and precomputes the
// affine inverse. Returns true if a non-identity back-transform was computed.
bool init_from_config(const PrintConfig &config);
// Apply the inverse transform to a point. Returns pos unchanged if
// no back-transform is active.
Vec3d apply(const Vec3d &pos) const;
// True if a non-identity back-transform is active.
bool is_active() const { return m_active; }
private:
bool m_active = false;
Transform3d m_inverse = Transform3d::Identity();
};
} // namespace Slic3r
#endif // slic3r_BeltBackTransform_hpp_

View File

@@ -1,10 +1,14 @@
#include "../GCode.hpp"
#include "../FirstLayerPlane.hpp"
#include "CoolingBuffer.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <float.h>
#include <string_view>
#include <system_error>
#include <unordered_map>
@@ -28,6 +32,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t
m_num_extruders = std::max(ex.id() + 1, m_num_extruders);
m_extruder_ids.emplace_back(ex.id());
}
// Borrow the first-layer plane from the GCode generator. When inactive
// (non-belt printers and belt printers without Z shear), per-line fan
// re-evaluation is skipped and behavior is bit-identical to the legacy
// per-layer path.
m_first_layer_plane = gcodegen.first_layer_plane();
}
void CoolingBuffer::reset(const Vec3d &position)
@@ -328,6 +338,13 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b
std::vector<PerExtruderAdjustments> per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos);
float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments);
out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments);
// First-layer plane: per-segment fan re-evaluation post-pass. Walks
// the cooled-down gcode and inserts inline M106 commands at band
// crossings (where the path's perpendicular distance to the plane
// crosses close_fan_the_first_x_layers thresholds). No-op when
// the evaluator is inactive.
if (m_first_layer_plane && m_first_layer_plane->is_active())
out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched);
m_gcode.clear();
}
return out;
@@ -1058,4 +1075,214 @@ std::string CoolingBuffer::apply_layer_cooldown(
return new_gcode;
}
// Pure helper: compute the main fan speed for a given effective layer index.
// Mirrors the inline logic in change_extruder_set_fan but is callable from
// per-line code in apply_first_layer_plane_fan_eval.
int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time,
unsigned int extruder_id) const
{
#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id)
float fan_min_speed = EXTRUDER_CFG(fan_min_speed);
float fan_max_speed = EXTRUDER_CFG(fan_max_speed);
bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq);
int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers);
int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer);
float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time));
float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time));
#undef EXTRUDER_CFG
if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0)
close_fan_the_first_x_layers = 1;
float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f;
if (effective_layer_id >= close_fan_the_first_x_layers) {
if (layer_time < slow_down_layer_time) {
fan_speed_new = fan_max_speed;
} else if (layer_time < fan_cooling_layer_time) {
double t = (layer_time - slow_down_layer_time) /
(fan_cooling_layer_time - slow_down_layer_time);
fan_speed_new = float(int(floor(t * fan_min_speed +
(1. - t) * fan_max_speed) + 0.5));
}
if (effective_layer_id + 1 < full_fan_speed_layer) {
float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers)
/ float(full_fan_speed_layer - close_fan_the_first_x_layers);
fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255));
}
} else {
fan_speed_new = 0.f;
}
return int(fan_speed_new);
}
// Post-pass: walk the cooled-down gcode line by line, track XYZ position,
// and insert M106 commands at first-layer-plane band crossings so the fan
// follows perpendicular distance to the plane rather than the slicing-layer
// index. Only invoked when the FirstLayerPlane evaluator is active.
//
// This implementation is intentionally minimal: it overrides only the MAIN
// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc
// special fans remain at their layer-level values from apply_layer_cooldown.
// That keeps the per-line logic small while still giving the user precise
// fan control near the belt surface, which is the main quality concern.
std::string CoolingBuffer::apply_first_layer_plane_fan_eval(
std::string &&gcode_in, size_t /*layer_id*/, float layer_time)
{
if (!m_first_layer_plane || !m_first_layer_plane->is_active())
return std::move(gcode_in);
const std::string &gcode = gcode_in;
std::string out;
out.reserve(gcode.size() + 256);
// Match the PWM floor applied at every other set_fan call in this file so
// band-crossing M106 emissions start the fan reliably at low speeds.
const unsigned int part_cooling_fan_min_pwm = static_cast<unsigned int>(std::max(0, m_config.part_cooling_fan_min_pwm.value));
// Track position in slicing-frame mm. Seed from m_current_pos which the
// CoolingBuffer keeps up-to-date across layers.
Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]);
// Track current main fan speed by parsing M106 commands as we walk so
// we can restore it after a band exit.
int current_main_fan = m_fan_speed;
int pre_band_main_fan = current_main_fan;
// Implicit initial state: assume the layer started "out of the band"
// (i.e., the layer-level fan setting from apply_layer_cooldown is in
// effect). The first movement we encounter will reconcile this.
bool in_first_layer_band = false;
unsigned int active_extruder = m_current_extruder;
auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) {
if (line_sv.size() < 3) return false;
if (line_sv[0] != 'G') return false;
if (line_sv[1] != '0' && line_sv[1] != '1') return false;
if (line_sv[2] != ' ' && line_sv[2] != '\t') return false;
const char *c = line_sv.data() + 3;
const char *end = line_sv.data() + line_sv.size();
bool any = false;
while (c < end && *c != ';') {
while (c < end && (*c == ' ' || *c == '\t')) ++c;
if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break;
char axis = *c;
++c;
if (axis == 'X' || axis == 'Y' || axis == 'Z') {
char *next;
double v = std::strtod(c, &next);
if (next != c) {
if (axis == 'X') p.x() = v;
else if (axis == 'Y') p.y() = v;
else p.z() = v;
c = next;
any = true;
continue;
}
}
// Skip unrecognized word.
while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n')
++c;
}
return any;
};
auto parse_m106 = [](const std::string_view &line_sv) -> int {
// Returns -1 if not an M106, otherwise the S value (0..255).
if (line_sv.size() < 4 || line_sv[0] != 'M') return -1;
if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6'))
return -1;
// Find S<value>
size_t s_pos = line_sv.find('S');
if (s_pos == std::string_view::npos) return -1;
const char *c = line_sv.data() + s_pos + 1;
char *next;
long v = std::strtol(c, &next, 10);
if (next == c) return -1;
return int(std::clamp<long>(v, 0, 255));
};
auto parse_m107 = [](const std::string_view &line_sv) -> bool {
return line_sv.size() >= 4 && line_sv[0] == 'M' &&
line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7';
};
auto parse_tool_change = [this](const std::string_view &line_sv) -> int {
// Returns the new extruder id, or -1 if not a toolchange.
if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1;
if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0)
return -1;
const char *c = line_sv.data() + m_toolchange_prefix.size();
char *next;
long v = std::strtol(c, &next, 10);
if (next == c) return -1;
return int(v);
};
const char *p = gcode.c_str();
const char *end = gcode.c_str() + gcode.size();
while (p < end) {
const char *line_end = p;
while (line_end < end && *line_end != '\n') ++line_end;
const char *next_line = line_end;
if (next_line < end) ++next_line; // include the '\n'
std::string_view line_sv(p, line_end - p);
// Track tool changes so the per-line fan eval uses the right extruder.
int new_tool = parse_tool_change(line_sv);
if (new_tool >= 0)
active_extruder = unsigned(new_tool);
// Track existing fan commands so we can restore the right value when
// exiting a band.
int m106_speed = parse_m106(line_sv);
if (m106_speed >= 0) {
current_main_fan = m106_speed;
if (!in_first_layer_band)
pre_band_main_fan = m106_speed;
} else if (parse_m107(line_sv)) {
current_main_fan = 0;
if (!in_first_layer_band)
pre_band_main_fan = 0;
}
// Movement line: parse XYZ, evaluate plane, possibly emit a fan
// change BEFORE this line.
bool moved = parse_xyz_into(line_sv, cur_pos_mm);
if (moved) {
const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm);
const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder);
const bool now_in_band = eff_idx < std::max(close_n, 1);
if (now_in_band != in_first_layer_band) {
// Band crossing: emit a M106 with the appropriate speed.
int target_fan;
if (now_in_band) {
// Entering the first-layer band: fan off.
pre_band_main_fan = current_main_fan;
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
} else {
// Exiting the band: restore the layer's normal fan speed.
// Use compute_main_fan_speed with the effective index so
// the linear ramp factor (close_fan→full_fan_speed_layer)
// also follows distance from the plane.
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
if (target_fan == 0)
target_fan = pre_band_main_fan;
}
if (target_fan != current_main_fan) {
out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm);
current_main_fan = target_fan;
m_fan_speed = target_fan;
m_current_fan_speed = target_fan;
}
in_first_layer_band = now_in_band;
}
}
out.append(p, next_line - p);
p = next_line;
}
return out;
}
} // namespace Slic3r

View File

@@ -10,6 +10,7 @@ namespace Slic3r {
class GCode;
class Layer;
class FirstLayerPlane;
struct PerExtruderAdjustments;
// A standalone G-code filter, to control cooling of the print.
@@ -18,7 +19,7 @@ struct PerExtruderAdjustments;
//
// The simple it sounds, the actual implementation is significantly more complex.
// Namely, for a multi-extruder print, each material may require a different cooling logic.
// For example, some materials may not like to print too slowly, while with some materials
// For example, some materials may not like to print too slowly, while with some materials
// we may slow down significantly.
//
class CoolingBuffer {
@@ -36,6 +37,21 @@ private:
// Returns the adjusted G-code.
std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector<PerExtruderAdjustments> &per_extruder_adjustments);
// First-layer plane: per-line fan re-evaluation post-pass. Walks the
// post-cooldown gcode, tracks XYZ position, and inserts M106 commands at
// band-crossing transitions in slicing-frame coordinates. Only runs
// when m_first_layer_plane is active.
std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in,
size_t layer_id,
float layer_time);
// Pure helper: compute the main fan speed for a given effective layer
// index (layer-id units, mapped through the plane evaluator) and the
// current extruder. Mirrors the inline logic in the change_extruder_set_fan
// lambda but is callable from per-line code.
int compute_main_fan_speed(int effective_layer_id, float layer_time,
unsigned int extruder_id) const;
// G-code snippet cached for the support layers preceding an object layer.
std::string m_gcode;
// Internal data.
@@ -58,6 +74,9 @@ private:
unsigned int m_current_nozzle;
//BBS: current fan speed
int m_current_fan_speed;
// First-layer plane evaluator, borrowed from GCode. Null = inactive
// (legacy per-layer fan control).
const FirstLayerPlane *m_first_layer_plane = nullptr;
};
}

View File

@@ -2530,6 +2530,7 @@ void GCodeProcessorResult::reset() {
long_retraction_when_cut = false;
timelapse_warning_code = 0;
printable_height = 0.0f;
machine_frame_transform_active = false;
settings_ids.reset();
filaments_count = 0;
backtrace_enabled = false;
@@ -2765,6 +2766,32 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
return ps;
};
// Belt-printer post-gcode shear/scale/post_remap is applied as the final
// step of BeltGCodeWriter::to_machine_coords, so MoveVertex.position is
// in the printer's machine frame. Undo it here so XY area and Z height
// checks operate in the build-volume frame that printable_area /
// printable_height are defined in. For non-belt printers
// (is_active() == false) apply_inverse is identity and behaviour is
// unchanged from before.
const bool machine_frame_active = m_machine_frame_transform.is_active();
auto compare_pos = [&](const GCodeProcessorResult::MoveVertex &move) -> Vec3d {
Vec3d pos = move.position.cast<double>();
if (!machine_frame_active)
return pos;
Vec3d extruder_off = Vec3d::Zero();
if (size_t(move.extruder_id) < m_extruder_offsets.size())
extruder_off = m_extruder_offsets[move.extruder_id].cast<double>();
// Strip plate + extruder offsets to recover the raw machine-frame
// coordinate that was emitted into the G-code (see store_move_vertex).
Vec3d machine(pos.x() - m_x_offset - extruder_off.x(),
pos.y() - m_y_offset - extruder_off.y(),
pos.z() - extruder_off.z() + m_z_offset);
Vec3d build = m_machine_frame_transform.apply_inverse(machine);
// Re-apply plate offset so the result matches plate_printable_poly,
// which is translated by plate_offset below.
return Vec3d(build.x() + m_x_offset, build.y() + m_y_offset, build.z());
};
struct GCodePosInfo
{
Points pos;
@@ -2775,28 +2802,23 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
std::map<int, std::map<int, GCodePosInfo>> gcode_path_pos; // object_id, filament_id, pos
for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) {
// sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/)
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) {
const Vec3d cp = compare_pos(move);
// For belt printers we read Z from the inverse-transformed position
// (post-origin-snap, pre-machine-frame). Otherwise keep the
// original print_z source (the slicer's layer-Z comment) so
// non-belt behaviour is bit-for-bit unchanged.
const float z_for_height = machine_frame_active ? float(cp.z()) : move.print_z;
if (move.extrusion_role == ExtrusionRole::erCustom) {
/*if (move.is_arc_move_with_interpolation_points()) {
for (int i = 0; i < move.interpolation_points.size(); i++) {
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.interpolation_points[i].cast<double>()));
}
} else {*/
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.position.cast<double>()));
//}
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(cp));
gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom =
std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, move.print_z);
std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, z_for_height);
} else {
/*if (move.is_arc_move_with_interpolation_points()) {
for (int i = 0; i < move.interpolation_points.size(); i++) {
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.interpolation_points[i].cast<double>()));
}
} else {*/
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.position.cast<double>()));
//}
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(cp));
gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z = std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z,
move.print_z);
z_for_height);
}
}
}
bool valid = true;
@@ -3036,6 +3058,12 @@ void GCodeProcessor::apply_config(const PrintConfig& config)
m_result.printable_height = config.printable_height;
// Belt printer: cache the post-gcode machine-frame transform so the
// multi-extruder validator can undo it and compare against build-volume
// bounds rather than machine-frame positions.
m_machine_frame_transform.init_from_config(config);
m_result.machine_frame_transform_active = m_machine_frame_transform.is_active();
auto filament_maps = config.option<ConfigOptionInts>("filament_map");
if (filament_maps != nullptr) {
m_filament_maps = filament_maps->values;
@@ -4154,6 +4182,44 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
return;
}
// Belt printer: derive the physical tilt magnitude from the slicing-rotation
// angle header comment (used to enable the preview's belt view).
if (boost::starts_with(comment, " belt_slice_rotation_angle = ")) {
try {
m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29))));
} catch (...) {}
return;
}
// Belt printer: parse pre-slice axis remap from header comments.
{
auto trim = [](const std::string &s) -> std::string {
size_t start = s.find_first_not_of(" \t\r\n");
size_t end = s.find_last_not_of(" \t\r\n");
return (start == std::string::npos) ? "" : s.substr(start, end - start + 1);
};
// Pre-slice axis remap
auto parse_remap_axis = [](const std::string &s) -> RemapAxis {
if (s == "pos_x") return RemapAxis::PosX;
if (s == "pos_y") return RemapAxis::PosY;
if (s == "pos_z") return RemapAxis::PosZ;
if (s == "neg_x") return RemapAxis::NegX;
if (s == "neg_y") return RemapAxis::NegY;
if (s == "neg_z") return RemapAxis::NegZ;
if (s == "rev_x") return RemapAxis::RevX;
if (s == "rev_y") return RemapAxis::RevY;
if (s == "rev_z") return RemapAxis::RevZ;
return RemapAxis::PosX;
};
if (boost::starts_with(comment, " preslice_remap_x = ")) {
m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
}
if (boost::starts_with(comment, " preslice_remap_y = ")) {
m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
}
if (boost::starts_with(comment, " preslice_remap_z = ")) {
m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
}
}
// wipe start tag
if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) {
m_wiping = true;
@@ -5926,11 +5992,8 @@ void GCodeProcessor::process_G10(const GCodeReader::GCodeLine& line)
GCodeReader::GCodeLine g10;
g10.set(Axis::E, -this->m_parser.config().retraction_length.get_at(m_extruder_id));
g10.set(Axis::F, this->m_parser.config().retraction_speed.get_at(m_extruder_id) * 60);
//Orca: Firmware retract emulation must not change the modal G1 feedrate.
const float feedrate = m_feedrate;
--m_g1_line_id;
process_G1(g10);
m_feedrate = feedrate;
}
void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line)
@@ -5939,11 +6002,8 @@ void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line)
GCodeReader::GCodeLine g11;
g11.set(Axis::E, this->m_parser.config().retraction_length.get_at(m_extruder_id) + this->m_parser.config().retract_restart_extra.get_at(m_extruder_id));
g11.set(Axis::F, this->m_parser.config().deretraction_speed.get_at(m_extruder_id) * 60);
// Orca: Firmware unretract emulation must not change the modal G1 feedrate.
const float feedrate = m_feedrate;
--m_g1_line_id;
process_G1(g11);
m_feedrate = feedrate;
}
void GCodeProcessor::process_G20(const GCodeReader::GCodeLine& line)
@@ -6022,6 +6082,13 @@ void GCodeProcessor::process_G92(const GCodeReader::GCodeLine& line)
if (line.has_z()) {
m_origin[Z] = m_end_position[Z] - line.z() * lengths_scale_factor;
any_found = true;
// Belt only: the start G-code's purge-blob advance + G92 Z0 resets leave a constant
// machine-Z origin offset here; the designed-view back-transform subtracts it so
// toolpaths map to the model's belt coordinate (gcode Z). Gated on belt_tilt_angle
// (set from the belt header, parsed before the body) so non-belt G-code processing
// is byte-identical — no unconditional work on the shared path.
if (m_result.belt_tilt_angle != 0.f)
m_result.belt_z_origin = m_origin[Z];
}
if (line.has_e()) {
@@ -7000,6 +7067,22 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
m_result.print_statistics.total_travel_distance += m_travel_dist;
}
// During the start G-code "prepare" stage the toolhead Z is not yet a real
// print height on a normal printer, so it is pinned to the first-layer height
// to keep the preview tidy. Belt printers are the exception: there the Z is
// written explicitly by BeltGCodeWriter and the designed-view back-transform
// couples machine Z into the rendered model Y (the belt tilt mixes the height
// and belt-feed axes). Overriding Z therefore back-transforms the last
// prepare-stage move (the unretract before the first extrusion) to model
// Y ~= 0, and the libvgcode path builder then draws a phantom extrusion
// segment from Y ~= 0 to the first real toolpath. Keep the real Z for belt
// printers so prepare-stage moves map correctly. Gated on belt_tilt_angle (set
// from the G-code header before the body is processed) so non-belt processing
// is byte-identical.
const float store_z = (m_processing_start_custom_gcode && m_result.belt_tilt_angle == 0.f)
? m_first_layer_height
: m_end_position[Z] - m_z_offset;
m_result.moves.push_back({
m_last_line_id,
type,
@@ -7007,7 +7090,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
static_cast<unsigned char>(filament_id),
m_cp_color.current,
//BBS: add plate's offset to the rendering vertices
Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, m_processing_start_custom_gcode ? m_first_layer_height : m_end_position[Z]- m_z_offset) + m_extruder_offsets[filament_id],
Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, store_z) + m_extruder_offsets[filament_id],
static_cast<float>(m_end_position[E] - m_start_position[E]),
m_feedrate,
0.0f, // actual feedrate

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