diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml
index 2a6ed8f0a4..422513442e 100644
--- a/.github/workflows/check_profiles.yml
+++ b/.github/workflows/check_profiles.yml
@@ -74,12 +74,26 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]}
+ # The validator above is the nightly build of main, so it cannot slice profiles that use
+ # settings a PR adds to the engine: it reports their placeholders as undefined. A PR that
+ # changes src/ also runs Build all, whose Slice check runs this same sweep with the
+ # validator built from the PR, so the sweep below only runs for the other PRs.
+ - name: Detect engine changes
+ id: engine_changes
+ if: ${{ github.event_name == 'pull_request' }}
+ run: |
+ base=${{ github.event.pull_request.base.sha }}
+ if git fetch --no-tags --depth=1 origin "$base" && ! git diff --quiet "$base" HEAD -- src/; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ echo "::notice::This PR changes src/, so Build all's Slice check slices the profiles with the PR-built validator."
+ fi
# Slice a two-colour cube through every printer, and through every system process/filament whose
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
# invalid-flow bugs the static checks above cannot see.
- name: validate slice (expand custom g-code)
id: validate_slice
+ if: ${{ steps.engine_changes.outputs.changed != 'true' }}
continue-on-error: true
run: |
set +e
diff --git a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp
index dc7733ebd6..62d231e375 100644
--- a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp
+++ b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp
@@ -88,6 +88,18 @@ struct NfpPConfig {
*/
bool explore_holes = false;
+ /**
+ * @brief Keep the final pile on the bin.
+ *
+ * The final alignment centres the pile on the alignment target. A target
+ * near an edge (a belt printer starts its parts at the leading end of the
+ * belt) would push part of a pile that is larger than the room around that
+ * point off the bed; with this set the pile stops at the edge instead, and a
+ * pile that does not fit along an axis is centred on it. Off by default, so
+ * the alignment of every other printer is unchanged.
+ */
+ bool clamp_to_bin = false;
+
/**
* @brief If true, use all CPUs available. Run on a single core otherwise.
*/
@@ -1111,7 +1123,24 @@ private:
default: ; // DONT_ALIGN
}
- auto d = cb - ci;
+ auto d = cb - ci;
+
+ // Keep the pile on the bin (see Config::clamp_to_bin). The items' boxes carry
+ // their inflation, which is the margin left at the edge.
+ if (config_.clamp_to_bin) {
+ auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) {
+ if (hi - lo >= bin_hi - bin_lo)
+ return (bin_lo + bin_hi) / 2 - (lo + hi) / 2;
+ if (lo + shift < bin_lo)
+ shift = bin_lo - lo;
+ if (hi + shift > bin_hi)
+ shift = bin_hi - hi;
+ return shift;
+ };
+ setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d)));
+ setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d)));
+ cb = ci + d;
+ }
// BBS make sure the item won't clash with excluded regions
// do we have wipe tower after arranging?
diff --git a/resources/calib/temperature_tower/belt_temp_provino_unit.stl b/resources/calib/temperature_tower/belt_temp_provino_unit.stl
new file mode 100644
index 0000000000..556cf313d2
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_provino_unit.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_230_190.stl b/resources/calib/temperature_tower/belt_temp_tower_230_190.stl
new file mode 100644
index 0000000000..0496fd1522
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_230_190.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_240_210.stl b/resources/calib/temperature_tower/belt_temp_tower_240_210.stl
new file mode 100644
index 0000000000..31801c1744
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_240_210.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_250_230.stl b/resources/calib/temperature_tower/belt_temp_tower_250_230.stl
new file mode 100644
index 0000000000..b03f057884
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_250_230.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_270_230.stl b/resources/calib/temperature_tower/belt_temp_tower_270_230.stl
new file mode 100644
index 0000000000..7a0bfbd1da
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_270_230.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_280_240.stl b/resources/calib/temperature_tower/belt_temp_tower_280_240.stl
new file mode 100644
index 0000000000..1276d5ce04
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_280_240.stl differ
diff --git a/resources/calib/temperature_tower/belt_temp_tower_320_280.stl b/resources/calib/temperature_tower/belt_temp_tower_320_280.stl
new file mode 100644
index 0000000000..957e038067
Binary files /dev/null and b/resources/calib/temperature_tower/belt_temp_tower_320_280.stl differ
diff --git a/resources/calib/temperature_tower/gen_belt_temp_tower.py b/resources/calib/temperature_tower/gen_belt_temp_tower.py
new file mode 100644
index 0000000000..9182647acb
--- /dev/null
+++ b/resources/calib/temperature_tower/gen_belt_temp_tower.py
@@ -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)}")
diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json
index afbcdacbe8..ab2084d898 100644
--- a/resources/profiles/Custom.json
+++ b/resources/profiles/Custom.json
@@ -1,9 +1,13 @@
{
"name": "Custom Printer",
- "version": "02.04.00.07",
+ "version": "02.04.00.08",
"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"
@@ -50,6 +54,10 @@
"name": "0.08mm Extra Fine @MyKlipper",
"sub_path": "process/0.08mm Extra Fine @MyKlipper.json"
},
+ {
+ "name": "0.12mm Fine @MyBeltPrinter",
+ "sub_path": "process/0.12mm Fine @MyBeltPrinter.json"
+ },
{
"name": "0.12mm Fine @MyKlipper",
"sub_path": "process/0.12mm Fine @MyKlipper.json"
@@ -62,6 +70,10 @@
"name": "0.16mm Optimal @MyKlipper",
"sub_path": "process/0.16mm Optimal @MyKlipper.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,6 +274,10 @@
"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"
@@ -274,6 +290,22 @@
"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": "MyToolChanger 0.2 nozzle",
"sub_path": "machine/MyToolChanger 0.2 nozzle.json"
diff --git a/resources/profiles/Custom/Generic Belt Printer_cover.png b/resources/profiles/Custom/Generic Belt Printer_cover.png
new file mode 100644
index 0000000000..d42f2ef228
Binary files /dev/null and b/resources/profiles/Custom/Generic Belt Printer_cover.png differ
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json
new file mode 100644
index 0000000000..1226f4dd68
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json
@@ -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"
+}
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json
new file mode 100644
index 0000000000..e8149509ca
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.4 nozzle.json
@@ -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"
+}
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json
new file mode 100644
index 0000000000..b8ca999dca
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.6 nozzle.json
@@ -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"
+}
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json b/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json
new file mode 100644
index 0000000000..b546b5b288
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.8 nozzle.json
@@ -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"
+}
diff --git a/resources/profiles/Custom/machine/MyBeltPrinter.json b/resources/profiles/Custom/machine/MyBeltPrinter.json
new file mode 100644
index 0000000000..0f6e0c5891
--- /dev/null
+++ b/resources/profiles/Custom/machine/MyBeltPrinter.json
@@ -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"
+}
diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json
new file mode 100644
index 0000000000..96b3138832
--- /dev/null
+++ b/resources/profiles/Custom/machine/fdm_belt_common.json
@@ -0,0 +1,95 @@
+{
+ "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"
+ ],
+ "z_hop_types": [
+ "Normal Lift"
+ ],
+ "gcode_remap_x": "rev_x",
+ "gcode_remap_y": "pos_z",
+ "gcode_remap_z": "pos_y",
+ "belt_printer": "1",
+ "belt_slice_rotation": "x",
+ "belt_slice_rotation_angle": "45",
+ "build_plate_tilt_x": "45",
+ "purge_in_prime_tower": "0",
+ "scan_first_layer": "0",
+ "auxiliary_fan": "0"
+}
diff --git a/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json b/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json
new file mode 100644
index 0000000000..15814e6725
--- /dev/null
+++ b/resources/profiles/Custom/process/0.12mm Fine @MyBeltPrinter.json
@@ -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"
+ ]
+}
diff --git a/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json b/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json
new file mode 100644
index 0000000000..6c10b250d6
--- /dev/null
+++ b/resources/profiles/Custom/process/0.20mm Standard @MyBeltPrinter.json
@@ -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"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json
new file mode 100644
index 0000000000..ebed0bc9e0
--- /dev/null
+++ b/resources/profiles/IdeaFormer.json
@@ -0,0 +1,54 @@
+{
+ "name": "IdeaFormer",
+ "version": "02.00.00.06",
+ "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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png b/resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png
new file mode 100644
index 0000000000..71485f54f5
Binary files /dev/null and b/resources/profiles/IdeaFormer/IdeaFormer IR3 V2_cover.png differ
diff --git a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..28da3959f4
--- /dev/null
+++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json
@@ -0,0 +1,77 @@
+{
+ "type": "filament",
+ "name": "Generic PETG @IdeaFormer IR3 V2",
+ "inherits": "Generic PETG @System",
+ "from": "system",
+ "setting_id": "n4zaXcUUzTqAxq5f",
+ "instantiation": "true",
+ "filament_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "compatible_printers": [
+ "IdeaFormer IR3 V2 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PETG"
+ ],
+ "filament_vendor": [
+ "Generic"
+ ],
+ "filament_settings_id": [
+ "Generic PETG @IdeaFormer IR3 V2"
+ ],
+ "filament_flow_ratio": [
+ "0.95"
+ ],
+ "filament_cost": [
+ "25"
+ ],
+ "nozzle_temperature": [
+ "240"
+ ],
+ "nozzle_temperature_initial_layer": [
+ "245"
+ ],
+ "cool_plate_temp": [
+ "80"
+ ],
+ "cool_plate_temp_initial_layer": [
+ "80"
+ ],
+ "fan_min_speed": [
+ "40"
+ ],
+ "fan_max_speed": [
+ "60"
+ ],
+ "overhang_fan_threshold": [
+ "25%"
+ ],
+ "overhang_fan_speed": [
+ "80"
+ ],
+ "full_fan_speed_layer": [
+ "8"
+ ],
+ "slow_down_min_speed": [
+ "20"
+ ],
+ "slow_down_layer_time": [
+ "4"
+ ],
+ "fan_cooling_layer_time": [
+ "100"
+ ],
+ "filament_retraction_length": [
+ "2"
+ ],
+ "filament_retraction_speed": [
+ "40"
+ ],
+ "filament_deretraction_speed": [
+ "40"
+ ],
+ "filament_start_gcode": [
+ "; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..da5d4af50a
--- /dev/null
+++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json
@@ -0,0 +1,65 @@
+{
+ "type": "filament",
+ "name": "Generic PLA @IdeaFormer IR3 V2",
+ "inherits": "Generic PLA @System",
+ "from": "system",
+ "setting_id": "1xjycsEAFh6KQIhp",
+ "instantiation": "true",
+ "filament_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "compatible_printers": [
+ "IdeaFormer IR3 V2 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PLA"
+ ],
+ "filament_vendor": [
+ "Generic"
+ ],
+ "filament_settings_id": [
+ "Generic PLA @IdeaFormer IR3 V2"
+ ],
+ "nozzle_temperature": [
+ "215"
+ ],
+ "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"
+ ],
+ "close_fan_the_first_x_layers": [
+ "3"
+ ],
+ "full_fan_speed_layer": [
+ "8"
+ ],
+ "slow_down_min_speed": [
+ "20"
+ ],
+ "filament_retraction_length": [
+ "1.5"
+ ],
+ "filament_retraction_speed": [
+ "35"
+ ],
+ "filament_deretraction_speed": [
+ "30"
+ ],
+ "filament_start_gcode": [
+ "; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..b39a4c22fc
--- /dev/null
+++ b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json
@@ -0,0 +1,36 @@
+{
+ "type": "filament",
+ "name": "eSUN PLA @IdeaFormer IR3 V2",
+ "inherits": "Generic PLA @IdeaFormer IR3 V2",
+ "from": "system",
+ "setting_id": "XqkviBmFHEglXueX",
+ "filament_id": "OFkrxQC4",
+ "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"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json
new file mode 100644
index 0000000000..f993631921
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json
@@ -0,0 +1,98 @@
+{
+ "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_extruding": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_retracting": [
+ "1000",
+ "1000"
+ ],
+ "machine_max_acceleration_travel": [
+ "9000",
+ "9000"
+ ],
+ "machine_max_acceleration_x": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_y": [
+ "5000",
+ "5000"
+ ],
+ "machine_max_acceleration_z": [
+ "100",
+ "100"
+ ],
+ "machine_max_jerk_x": [
+ "10",
+ "10"
+ ],
+ "machine_max_jerk_y": [
+ "10",
+ "10"
+ ],
+ "machine_max_jerk_z": [
+ "0.4",
+ "0.4"
+ ],
+ "machine_max_speed_e": [
+ "60",
+ "60"
+ ],
+ "machine_max_speed_x": [
+ "500",
+ "500"
+ ],
+ "machine_max_speed_y": [
+ "500",
+ "500"
+ ],
+ "machine_max_speed_z": [
+ "20",
+ "20"
+ ],
+ "retraction_length": [
+ "2"
+ ],
+ "retraction_speed": [
+ "40"
+ ],
+ "deretraction_speed": [
+ "40"
+ ],
+ "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)"
+}
diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..bc6d1a6e77
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2.json
@@ -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"
+}
diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json
new file mode 100644
index 0000000000..aacd36e118
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json
@@ -0,0 +1,98 @@
+{
+ "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"
+ ],
+ "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",
+ "build_plate_tilt_x": "45",
+ "purge_in_prime_tower": "0",
+ "scan_first_layer": "0",
+ "auxiliary_fan": "0"
+}
diff --git a/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json
new file mode 100644
index 0000000000..5297f646ff
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json
@@ -0,0 +1,140 @@
+{
+ "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",
+ "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"
+}
diff --git a/resources/profiles/IdeaFormer/machine/fdm_machine_common.json b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json
new file mode 100644
index 0000000000..bf8f2249cf
--- /dev/null
+++ b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json
@@ -0,0 +1,118 @@
+{
+ "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",
+ "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"
+}
diff --git a/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json
new file mode 100644
index 0000000000..c2e46e4154
--- /dev/null
+++ b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json
@@ -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"
+ ]
+}
diff --git a/resources/profiles/IdeaFormer/process/fdm_process_common.json b/resources/profiles/IdeaFormer/process/fdm_process_common.json
new file mode 100644
index 0000000000..0d558129af
--- /dev/null
+++ b/resources/profiles/IdeaFormer/process/fdm_process_common.json
@@ -0,0 +1,106 @@
+{
+ "type": "process",
+ "name": "fdm_process_common",
+ "from": "system",
+ "instantiation": "false",
+ "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",
+ "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"
+}
diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json
new file mode 100644
index 0000000000..70338174d6
--- /dev/null
+++ b/resources/profiles/Printcepts.json
@@ -0,0 +1,54 @@
+{
+ "name": "Printcepts",
+ "version": "01.00.00.04",
+ "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"
+ }
+ ]
+}
diff --git a/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg b/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg
new file mode 100644
index 0000000000..3193c6a284
--- /dev/null
+++ b/resources/profiles/Printcepts/BabyBelt Pro_bed_texture.svg
@@ -0,0 +1,70 @@
+
+
diff --git a/resources/profiles/Printcepts/BabyBelt Pro_cover.png b/resources/profiles/Printcepts/BabyBelt Pro_cover.png
new file mode 100644
index 0000000000..aaf5dd266f
Binary files /dev/null and b/resources/profiles/Printcepts/BabyBelt Pro_cover.png differ
diff --git a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json
new file mode 100644
index 0000000000..9b71a420b6
--- /dev/null
+++ b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json
@@ -0,0 +1,77 @@
+{
+ "type": "filament",
+ "name": "Generic PETG @BabyBelt Pro",
+ "inherits": "Generic PETG @System",
+ "from": "system",
+ "setting_id": "gCzHpDNgVwQR6tgk",
+ "instantiation": "true",
+ "filament_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "compatible_printers": [
+ "BabyBelt Pro 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PETG"
+ ],
+ "filament_vendor": [
+ "Generic"
+ ],
+ "filament_settings_id": [
+ "Generic PETG @BabyBelt Pro"
+ ],
+ "filament_flow_ratio": [
+ "0.95"
+ ],
+ "filament_cost": [
+ "25"
+ ],
+ "nozzle_temperature": [
+ "240"
+ ],
+ "nozzle_temperature_initial_layer": [
+ "245"
+ ],
+ "cool_plate_temp": [
+ "80"
+ ],
+ "cool_plate_temp_initial_layer": [
+ "80"
+ ],
+ "fan_min_speed": [
+ "40"
+ ],
+ "fan_max_speed": [
+ "60"
+ ],
+ "overhang_fan_threshold": [
+ "25%"
+ ],
+ "overhang_fan_speed": [
+ "80"
+ ],
+ "full_fan_speed_layer": [
+ "8"
+ ],
+ "slow_down_min_speed": [
+ "20"
+ ],
+ "slow_down_layer_time": [
+ "4"
+ ],
+ "fan_cooling_layer_time": [
+ "100"
+ ],
+ "filament_retraction_length": [
+ "2"
+ ],
+ "filament_retraction_speed": [
+ "40"
+ ],
+ "filament_deretraction_speed": [
+ "40"
+ ],
+ "filament_start_gcode": [
+ "; Generic PETG @BabyBelt Pro — belt PETG, bed 80C"
+ ]
+}
diff --git a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json
new file mode 100644
index 0000000000..3345b5f509
--- /dev/null
+++ b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json
@@ -0,0 +1,65 @@
+{
+ "type": "filament",
+ "name": "Generic PLA @BabyBelt Pro",
+ "inherits": "Generic PLA @System",
+ "from": "system",
+ "setting_id": "24PpcnhVx9v5f4fD",
+ "instantiation": "true",
+ "filament_extruder_variant": [
+ "Direct Drive Standard"
+ ],
+ "compatible_printers": [
+ "BabyBelt Pro 0.4 nozzle"
+ ],
+ "filament_type": [
+ "PLA"
+ ],
+ "filament_vendor": [
+ "Generic"
+ ],
+ "filament_settings_id": [
+ "Generic PLA @BabyBelt Pro"
+ ],
+ "nozzle_temperature": [
+ "215"
+ ],
+ "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"
+ ],
+ "close_fan_the_first_x_layers": [
+ "3"
+ ],
+ "full_fan_speed_layer": [
+ "8"
+ ],
+ "slow_down_min_speed": [
+ "20"
+ ],
+ "filament_retraction_length": [
+ "1.5"
+ ],
+ "filament_retraction_speed": [
+ "35"
+ ],
+ "filament_deretraction_speed": [
+ "30"
+ ],
+ "filament_start_gcode": [
+ "; Generic PLA @BabyBelt Pro — belt PLA, bed 75C"
+ ]
+}
diff --git a/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json
new file mode 100644
index 0000000000..1f93703e1c
--- /dev/null
+++ b/resources/profiles/Printcepts/filament/eSUN PLA @BabyBelt Pro.json
@@ -0,0 +1,36 @@
+{
+ "type": "filament",
+ "name": "eSUN PLA @BabyBelt Pro",
+ "inherits": "Generic PLA @BabyBelt Pro",
+ "from": "system",
+ "setting_id": "EH3X7oE0DU5tSpjW",
+ "filament_id": "OFkrxQC4",
+ "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"
+ ]
+}
diff --git a/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json
new file mode 100644
index 0000000000..84218476cb
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/BabyBelt Pro 0.4 nozzle.json
@@ -0,0 +1,88 @@
+{
+ "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",
+ "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"
+}
diff --git a/resources/profiles/Printcepts/machine/BabyBelt Pro.json b/resources/profiles/Printcepts/machine/BabyBelt Pro.json
new file mode 100644
index 0000000000..eb711541bd
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/BabyBelt Pro.json
@@ -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"
+}
diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json
new file mode 100644
index 0000000000..96d38ac31a
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json
@@ -0,0 +1,98 @@
+{
+ "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"
+ ],
+ "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",
+ "build_plate_tilt_x": "45",
+ "purge_in_prime_tower": "0",
+ "scan_first_layer": "0",
+ "auxiliary_fan": "0"
+}
diff --git a/resources/profiles/Printcepts/machine/fdm_klipper_common.json b/resources/profiles/Printcepts/machine/fdm_klipper_common.json
new file mode 100644
index 0000000000..5297f646ff
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/fdm_klipper_common.json
@@ -0,0 +1,140 @@
+{
+ "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",
+ "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"
+}
diff --git a/resources/profiles/Printcepts/machine/fdm_machine_common.json b/resources/profiles/Printcepts/machine/fdm_machine_common.json
new file mode 100644
index 0000000000..bf8f2249cf
--- /dev/null
+++ b/resources/profiles/Printcepts/machine/fdm_machine_common.json
@@ -0,0 +1,118 @@
+{
+ "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",
+ "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"
+}
diff --git a/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json b/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json
new file mode 100644
index 0000000000..f592cfe87e
--- /dev/null
+++ b/resources/profiles/Printcepts/process/0.20mm Standard @BabyBelt Pro.json
@@ -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"
+ ]
+}
diff --git a/resources/profiles/Printcepts/process/fdm_process_common.json b/resources/profiles/Printcepts/process/fdm_process_common.json
new file mode 100644
index 0000000000..0d558129af
--- /dev/null
+++ b/resources/profiles/Printcepts/process/fdm_process_common.json
@@ -0,0 +1,106 @@
+{
+ "type": "process",
+ "name": "fdm_process_common",
+ "from": "system",
+ "instantiation": "false",
+ "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",
+ "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"
+}
diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs
index c5efeef277..50789327c5 100644
--- a/resources/shaders/110/gouraud.fs
+++ b/resources/shaders/110/gouraud.fs
@@ -26,6 +26,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform vec4 uniform_color;
diff --git a/resources/shaders/110/gouraud.vs b/resources/shaders/110/gouraud.vs
index 37be529b09..5ee685c070 100644
--- a/resources/shaders/110/gouraud.vs
+++ b/resources/shaders/110/gouraud.vs
@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -77,8 +78,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) {
diff --git a/resources/shaders/110/mm_gouraud.fs b/resources/shaders/110/mm_gouraud.fs
index 821af13f03..431c936e9c 100644
--- a/resources/shaders/110/mm_gouraud.fs
+++ b/resources/shaders/110/mm_gouraud.fs
@@ -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;
diff --git a/resources/shaders/110/mm_gouraud.vs b/resources/shaders/110/mm_gouraud.vs
index b0cea9cfd6..c54ae4bff5 100644
--- a/resources/shaders/110/mm_gouraud.vs
+++ b/resources/shaders/110/mm_gouraud.vs
@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform SlopeDetection slope;
void main()
diff --git a/resources/shaders/110/phong.fs b/resources/shaders/110/phong.fs
index a47a24fdea..7d1e7bfe8c 100644
--- a/resources/shaders/110/phong.fs
+++ b/resources/shaders/110/phong.fs
@@ -41,6 +41,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform vec4 uniform_color;
diff --git a/resources/shaders/110/phong.vs b/resources/shaders/110/phong.vs
index 10d36e233f..d857147c28 100644
--- a/resources/shaders/110/phong.vs
+++ b/resources/shaders/110/phong.vs
@@ -7,6 +7,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -46,8 +47,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) {
diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs
index 7b315f90c0..f609495597 100644
--- a/resources/shaders/140/gouraud.fs
+++ b/resources/shaders/140/gouraud.fs
@@ -29,6 +29,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform vec4 uniform_color;
diff --git a/resources/shaders/140/gouraud.vs b/resources/shaders/140/gouraud.vs
index 11fc4b70c8..7f753a3059 100644
--- a/resources/shaders/140/gouraud.vs
+++ b/resources/shaders/140/gouraud.vs
@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -77,8 +78,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) {
diff --git a/resources/shaders/140/mm_gouraud.fs b/resources/shaders/140/mm_gouraud.fs
index c5fe86efc5..7c77d5565e 100644
--- a/resources/shaders/140/mm_gouraud.fs
+++ b/resources/shaders/140/mm_gouraud.fs
@@ -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;
diff --git a/resources/shaders/140/mm_gouraud.vs b/resources/shaders/140/mm_gouraud.vs
index b191e35fa8..191613f957 100644
--- a/resources/shaders/140/mm_gouraud.vs
+++ b/resources/shaders/140/mm_gouraud.vs
@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform SlopeDetection slope;
void main()
diff --git a/resources/shaders/140/phong.fs b/resources/shaders/140/phong.fs
index 809621b76d..4cfae1e03e 100644
--- a/resources/shaders/140/phong.fs
+++ b/resources/shaders/140/phong.fs
@@ -44,6 +44,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform vec4 uniform_color;
diff --git a/resources/shaders/140/phong.vs b/resources/shaders/140/phong.vs
index c7570edb95..7120b62c66 100644
--- a/resources/shaders/140/phong.vs
+++ b/resources/shaders/140/phong.vs
@@ -7,6 +7,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
+ vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -46,8 +47,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) {
diff --git a/scripts/orca_profile_tool.py b/scripts/orca_profile_tool.py
index f7f622c8c0..ec94eb7653 100755
--- a/scripts/orca_profile_tool.py
+++ b/scripts/orca_profile_tool.py
@@ -167,6 +167,11 @@ OBSOLETE_KEYS = {
"filament_load_time", "filament_unload_time", "smooth_coefficient",
"overhang_totally_speed", "silent_mode", "overhang_speed_classic",
"anisotropic_surfaces",
+ # Belt printer options retired before the feature shipped (#16236).
+ "belt_slice_rotation_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z",
+ "preslice_remap_global", "belt_support_z_offset_mode", "first_layer_plane",
+ "first_layer_plane_offset", "belt_preslice_global", "gcode_back_transform",
+ "belt_support_floor_mode", "first_layer_plane_thickness",
}
# Keys renamed at some point, whose old and new spellings must never co-exist:
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f0de57f12c..0acbd610f8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -91,6 +91,12 @@ if (SLIC3R_GUI)
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
find_package(wxInspector REQUIRED)
+ # wxInspector 1.0.0 installs its headers but accidentally declares the
+ # INSTALL_INTERFACE include directory PRIVATE, so its imported target does
+ # not expose them to consumers. Restore the package prefix include path until
+ # the upstream export is fixed.
+ get_filename_component(WXINSPECTOR_PREFIX "${wxInspector_DIR}/../../.." ABSOLUTE)
+ target_include_directories(wxInspector::wxInspector INTERFACE "${WXINSPECTOR_PREFIX}/include")
# wxInspector's exported interface names the release wxWidgets import
# libraries, which a Debug build cannot link. wx is linked above instead.
@@ -186,7 +192,7 @@ endif ()
# Add the Slic3r GUI library, libcurl, OpenGL and GLU libraries.
if (SLIC3R_GUI)
# target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES})
-target_link_libraries(OrcaSlicer libslic3r_gui)
+target_link_libraries(OrcaSlicer libslic3r_gui wxInspector::wxInspector)
if (MSVC)
# Generate debug symbols even in release mode.
target_link_options(OrcaSlicer PUBLIC "$<$:/DEBUG>")
diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp
index 4c4c001bfe..1b764fe468 100644
--- a/src/OrcaSlicer.cpp
+++ b/src/OrcaSlicer.cpp
@@ -3435,9 +3435,14 @@ int CLI::run(int argc, char **argv)
max_self_index = std::max(max_self_index, v);
min_self_index = std::min(min_self_index, v);
}
- if (max_self_index > filament_count || min_self_index < 1) {
- BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating")
- % min_self_index % max_self_index % filament_count;
+ // And a project saved with FEWER filaments than are now loaded (a
+ // one-filament project sliced with two --load-filaments) leaves the tables half filled:
+ // the variant matching below then reads past filament_extruder_variant and
+ // set_with_restore_2 throws an uncaught size error. Regenerate in that case too.
+ if (max_self_index > filament_count || min_self_index < 1 || max_self_index < filament_count
+ || (int) filament_self_index_opt->values.size() < filament_count) {
+ BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] (size %4%) is invalid for filament_count %3%, regenerating")
+ % min_self_index % max_self_index % filament_count % filament_self_index_opt->values.size();
need_regenerate_self_index = true;
}
}
@@ -3528,6 +3533,10 @@ int CLI::run(int argc, char **argv)
std::vector& filament_variants = curr_variant_opt->values;
filament_variants.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
}
+ // See the filament_self_index note above: one variant per filament for
+ // the filaments the project did not know about.
+ if ((int) curr_variant_opt->values.size() < filament_count)
+ curr_variant_opt->values.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
const ConfigOptionStrings *new_variant_opt = dynamic_cast(config.option("filament_extruder_variant", true));
std::vector new_variant_indice;
@@ -3536,7 +3545,7 @@ int CLI::run(int argc, char **argv)
for (int i = 0; i < new_variant_count; i++)
{
- for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count; j++)
+ for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count && j < (int) curr_variant_opt->values.size(); j++)
{
if (curr_variant_opt->values[j] == new_variant_opt->values[i]) {
new_variant_indice[i] = j;
@@ -3588,7 +3597,18 @@ int CLI::run(int argc, char **argv)
ConfigOptionVectorBase* opt_vec_dst = static_cast(opt);
const ConfigOptionVectorBase* opt_vec_src = static_cast(source_opt);
//set with index
- opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
+ try {
+ // A project with fewer filaments than are loaded: grow the
+ // destination to the filament's slot first (set_with_restore_2 only restores).
+ if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count))
+ opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
+ opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
+ } catch (const std::exception &ex) { // Was an uncaught abort
+ BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
+ boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
+ record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
+ flush_and_exit(CLI_CONFIG_FILE_ERROR);
+ }
}
continue;
@@ -3637,7 +3657,16 @@ int CLI::run(int argc, char **argv)
if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) {
std::vector temp_variant_indice;
temp_variant_indice.resize(new_variant_count, -1);
- opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
+ try {
+ if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) // See above
+ opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
+ opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
+ } catch (const std::exception &ex) { // Was an uncaught abort
+ BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
+ boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
+ record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
+ flush_and_exit(CLI_CONFIG_FILE_ERROR);
+ }
if (opt_key == "filament_extruder_variant")
new_variant_counts[filament_index - 1] = opt_vec_src->size();
@@ -4153,6 +4182,10 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to wrapping detect")%__LINE__;
}
+ // Belt printers never get the classic wipe tower (see Print::has_wipe_tower()), so reserve no space for it.
+ const ConfigOptionBool* belt_printer_opt = m_print_config.option("belt_printer");
+ const bool is_belt_printer = belt_printer_opt && belt_printer_opt->value;
+
auto timelapse_type_opt = m_print_config.option("timelapse_type");
bool is_smooth_timelapse = false;
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
@@ -4390,11 +4423,11 @@ int CLI::run(int argc, char **argv)
}
};
- auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
+ auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, is_belt_printer](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
- if (!print_config.has("wipe_tower_x")) {
+ if (is_belt_printer || !print_config.has("wipe_tower_x")) {
plate_obj_size_info.has_wipe_tower = false;
BOOST_LOG_TRIVIAL(info) << boost::format("can not found wipe_tower_x in config, set to no wipe tower");
return;
@@ -5265,7 +5298,7 @@ int CLI::run(int argc, char **argv)
}
}
- if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
+ if (!is_belt_printer && ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)) || (enable_wrapping_detect && !current_wrapping_exclude_area.empty())))
{
//prepare the wipe tower
int plate_count = partplate_list.get_plate_count();
@@ -5417,7 +5450,7 @@ int CLI::run(int argc, char **argv)
bool is_seq_print = false;
get_print_sequence(cur_plate, m_print_config, is_seq_print);
- if (!is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
+ if (!is_belt_printer && !is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
{
//prepare the wipe tower
auto printer_structure_opt = m_print_config.option>("printer_structure");
@@ -5585,7 +5618,7 @@ int CLI::run(int argc, char **argv)
};
const int max_filament_count = plate_count > 0 ? *std::max_element(plate_filament_counts.begin(), plate_filament_counts.end()) : 0;
- if (plate_needs_wipe_tower(max_filament_count))
+ if (!is_belt_printer && plate_needs_wipe_tower(max_filament_count))
{
//prepare the wipe tower
auto printer_structure_opt = m_print_config.option>("printer_structure");
@@ -5692,7 +5725,7 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found single object mode");
}
- if (m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
+ if (!is_belt_printer && m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
float x;
float y;
if (duplicate_count > 0) {
@@ -6317,7 +6350,7 @@ int CLI::run(int argc, char **argv)
// The stored (or default) tower position may not fit the tower these plates
// need, and no CLI placement site runs on a plain slice - mirror the GUI's
// reload clamp and fit every plate's tower into the printable area first.
- if (m_print_config.option("enable_prime_tower", true)->value) {
+ if (!is_belt_printer && m_print_config.option("enable_prime_tower", true)->value) {
for (int index = 0; index < partplate_list.get_plate_count(); index++) {
if ((plate_to_slice != 0) && (plate_to_slice != (index + 1)))
continue;
diff --git a/src/dev-utils/OrcaSlicer_profile_validator.cpp b/src/dev-utils/OrcaSlicer_profile_validator.cpp
index 687292b8c7..208b950cfb 100644
--- a/src/dev-utils/OrcaSlicer_profile_validator.cpp
+++ b/src/dev-utils/OrcaSlicer_profile_validator.cpp
@@ -202,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
{
const Vec2d center = printable_area_center(cfg);
+ const bool belt = cfg.opt_bool("belt_printer");
std::vector cube_mins;
if (by_object) {
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
@@ -212,6 +213,12 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
cfg.set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional));
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
+ } else if (belt) {
+ // A belt object's slicing Z starts at the belt below its leading end, well below its first
+ // printed layer, so a height range in slicing Z does not map onto the part. Two cubes one
+ // behind the other along the belt, the second on filament 2, give the one filament change
+ // instead (the purge prism is an object the GUI adds, so there is no tower to place).
+ cube_mins = {center - Vec2d(5., 15.), center + Vec2d(-5., 5.)};
} else {
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
if (!cfg.opt_string("wrapping_detection_gcode").empty())
@@ -229,14 +236,20 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
obj->add_volume(m);
obj->add_instance();
- // Filament 2 is used only above z=4, so the upper layers carry a single filament change.
- DynamicPrintConfig range_config;
- range_config.set_key_value("extruder", new ConfigOptionInt(2));
- // Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
- // printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
- // trips Flow::with_spacing.
- range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
- obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
+ if (belt && !by_object) {
+ // The second cube along the belt is on filament 2 (see cube_mins above).
+ if (&cube_min == &cube_mins.back())
+ obj->config.set_key_value("extruder", new ConfigOptionInt(2));
+ } else {
+ // Filament 2 is used only above z=4, so the upper layers carry a single filament change.
+ DynamicPrintConfig range_config;
+ range_config.set_key_value("extruder", new ConfigOptionInt(2));
+ // Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
+ // printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
+ // trips Flow::with_spacing.
+ range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
+ obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
+ }
obj->ensure_on_bed();
print.auto_assign_extruders(obj);
}
@@ -521,11 +534,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
const std::string filament_name = bundle.filaments.get_selected_preset_name();
const std::string what = "Printer \"" + printer + "\"";
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
+ // A belt printer has no wipe tower (it purges into a prism object on the belt), so its
+ // filament change is the plain tool change set_extruder() emits rather than the tower's block.
+ const bool belt = bundle.printers.get_selected_preset().config.opt_bool("belt_printer");
+ const std::string marker = belt ? "\nT1\n" : "CP TOOLCHANGE START";
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
++failures;
- else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
- // The filament change never rode the tower, so change_filament_gcode was not exercised.
- BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
+ else if (out.find(marker) == std::string::npos) {
+ // The filament change never fired, so change_filament_gcode was not exercised.
+ BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no "
+ << (belt ? "T1" : "CP TOOLCHANGE START") << ")";
++failures;
}
cover(bundle.prints.get_selected_preset());
diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp
index b79450a535..eacb122d05 100644
--- a/src/libslic3r/Arrange.cpp
+++ b/src/libslic3r/Arrange.cpp
@@ -299,7 +299,15 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
template
void fill_config(PConf& pcfg, const ArrangeParams ¶ms) {
- if (params.is_seq_print) {
+ if (params.is_belt) {
+ // Pack from the end of the belt that prints first, and keep the pile on the
+ // bed when it is larger than the room around that end.
+ pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
+ params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
+ PConf::Alignment::BOTTOM_RIGHT;
+ pcfg.clamp_to_bin = true;
+ }
+ else if (params.is_seq_print) {
// Start placing the items from the center of the print bed
pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT;
}
@@ -444,6 +452,50 @@ protected:
return bindist;
}
+ // Belt printers pack from the end of the belt that prints first, and a corner
+ // packer's checks (pile inside the bin, pack origin) apply to them as well.
+ bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; }
+
+ static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); }
+
+ // Position along the belt in print order: increasing from the end that prints first.
+ double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); }
+ double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); }
+ double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); }
+
+ // The corner of the bin the belt pile grows from.
+ Box::PointType belt_origin() const
+ {
+ const Box bb = sl::boundingBox(m_bin);
+ auto o = bb.minCorner();
+ if (params.belt_reversed) {
+ if (params.belt_axis == 0) setX(o, getX(bb.maxCorner()));
+ else setY(o, getY(bb.maxCorner()));
+ }
+ return o;
+ }
+
+ // An item's far edge in print order is what it costs (so a row fills across the
+ // belt before the pile advances), with a slight pull toward the near lateral
+ // edge and the same penalty as the bottom-left heuristic for sitting outside
+ // the corner.
+ double dist_along_belt(const Box &ibb)
+ {
+ const Box bin = sl::boundingBox(m_bin);
+ const int l = 1 - params.belt_axis;
+ const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l);
+ double d = belt_end(ibb) - belt_start(bin);
+ d += lat < 0 ? 10 * -lat : 0.1 * lat;
+ if (double behind = belt_start(ibb) - belt_start(bin); behind < 0)
+ d += 10 * -behind;
+ return norm(d);
+ }
+
+ double corner_bindist(const Box &ibb, const Slic3r::Point &origin_pack)
+ {
+ return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack);
+ }
+
double dist_to_bin(const Box& ibb, const Slic3r::Point& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
{
double bindist = 0;
@@ -533,8 +585,8 @@ protected:
// The smalles distance from the arranged pile center:
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
- if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
- double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack);
+ if (corner_packing()) {
+ double bindist = corner_bindist(ibb, origin_pack);
score = 0.2 * dist + 0.8 * bindist;
}
else {
@@ -591,8 +643,8 @@ protected:
break;
}
case LAST_BIG_ITEM: {
- if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
- score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
+ if (corner_packing()) {
+ score = corner_bindist(ibb, origin_pack);
}
else {
if (m_pilebb.defined)
@@ -607,8 +659,8 @@ protected:
// already processed bigger items.
// No need to play around with the anchor points, the center will be
// just fine for small items
- if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
- score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
+ if (corner_packing())
+ score = corner_bindist(ibb, origin_pack);
else {
// Align mainly around existing items
score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack));
@@ -709,6 +761,28 @@ protected:
score += 1 * (new_extruder_cnt-last_extruder_cnt);
}
+ // On a belt the parts print in belt order, so every colour change between
+ // parts is a filament change. Items arrive sorted by extruder and the pile
+ // grows from the leading end; keep each colour's run contiguous by charging
+ // an item for every packed item of another colour it does not fully follow,
+ // counting the tilted layers that reach belt_tilt_slope * height past that
+ // item's far edge.
+ if (params.is_belt && !params.is_seq_print) {
+ const std::set item_colours(item.extrude_ids.begin(), item.extrude_ids.end());
+ const double item_start = belt_start(ibb);
+ for (Item &p : m_items) {
+ if (p.is_virt_object)
+ continue;
+ const std::set p_colours(p.extrude_ids.begin(), p.extrude_ids.end());
+ const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end())
+ || std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end());
+ if (same_colour)
+ continue;
+ if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope))
+ score += 10.;
+ }
+ }
+
return std::make_tuple(score, fullbb);
}
@@ -785,7 +859,8 @@ public:
auto binbb = sl::boundingBox(m_bin);
- auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
+ auto starting_point = this->params.is_belt ? belt_origin() :
+ cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
// if we have wipe tower, items should be arranged around wipe tower
for (Item itm : items) {
if (itm.is_wipe_tower) {
@@ -936,7 +1011,7 @@ std::function AutoArranger::g
auto mp = m_merged_pile;
mp.emplace_back(itm.transformedShape());
auto chull = sl::convexHull(mp);
- if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
+ if (corner_packing())
{
if (!sl::isInside(chull, m_bin))
score += LARGE_COST_TO_REJECT;
diff --git a/src/libslic3r/Arrange.hpp b/src/libslic3r/Arrange.hpp
index ea89640a7b..b63b8c938b 100644
--- a/src/libslic3r/Arrange.hpp
+++ b/src/libslic3r/Arrange.hpp
@@ -146,6 +146,13 @@ struct ArrangeParams {
float nozzle_height = 0;
float printable_height = 256.0;
Vec2d align_center{ 0.5,0.5 };
+ // Belt printer: items print in the order they lie along the belt axis, from
+ // its low end unless belt_reversed, and a part's top prints
+ // belt_tilt_slope * height further along it than its base.
+ bool is_belt = false;
+ int belt_axis = 1; // 0 = X, 1 = Y
+ bool belt_reversed = false;
+ float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted
ArrangePolygons excluded_regions; // regions cant't be used
ArrangePolygons nonprefered_regions; // regions can be used but not prefered
diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp
new file mode 100644
index 0000000000..d51f1248de
--- /dev/null
+++ b/src/libslic3r/BeltBrim.cpp
@@ -0,0 +1,562 @@
+#include
+#include "BeltBrim.hpp"
+
+#include "ClipperUtils.hpp"
+#include "Flow.hpp"
+#include "Layer.hpp"
+#include "Polygon.hpp"
+#include "Print.hpp"
+#include "ShortestPath.hpp"
+#include "Support/BeltFloorContext.hpp"
+#include "BoundingBox.hpp"
+#include "ExPolygon.hpp"
+#include "ExtrusionEntity.hpp"
+#include "ExtrusionEntityCollection.hpp"
+#include "Point.hpp"
+#include "Polyline.hpp"
+#include "PrintConfig.hpp"
+#include "libslic3r.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Slic3r {
+
+// ---------------------------------------------------------------- scaling
+
+static inline Point scale_u_point(const Point &p, int from_axis, double factor)
+{
+ // llround, not a cast: casting truncates toward zero, so a round trip would
+ // walk every vertex toward the origin by up to one unit per pass.
+ return from_axis == 0 ?
+ Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) :
+ Point(p.x(), coord_t(std::llround(double(p.y()) * factor)));
+}
+
+static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor)
+{
+ for (Point &p : poly.points)
+ p = scale_u_point(p, from_axis, factor);
+}
+
+ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor)
+{
+ ExPolygons out = src;
+ for (ExPolygon &ex : out) {
+ scale_u_polygon(ex.contour, frame.from_axis, factor);
+ for (Polygon &hole : ex.holes)
+ scale_u_polygon(hole, frame.from_axis, factor);
+ }
+ return out;
+}
+
+Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor)
+{
+ Polylines out = src;
+ for (Polyline &pl : out)
+ for (Point &p : pl.points)
+ p = scale_u_point(p, frame.from_axis, factor);
+ return out;
+}
+
+// ---------------------------------------------------------------- sweep
+
+ExPolygons sweep_ex(const ExPolygons &src, const Point &t)
+{
+ if (src.empty())
+ return {};
+ if (t == Point(0, 0))
+ return src;
+
+ // One parallelogram per boundary edge. Together with P and P + t these
+ // cover the Minkowski sum exactly: for any q = p + s*t with p in P and
+ // s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P.
+ // Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e,
+ // putting q in that edge's parallelogram. Hole edges must be included, or
+ // holes narrower than t along t would wrongly survive the sweep.
+ Polygons quads;
+ for (const ExPolygon &ex : src)
+ for (size_t c = 0; c < ex.num_contours(); ++ c)
+ for (const Line &e : ex.contour_or_hole(c).lines()) {
+ if (e.a == e.b)
+ continue;
+ Polygon q;
+ q.points = { e.a, e.b, e.b + t, e.a + t };
+ // The non-zero fill rule counts a clockwise ring as -1, which
+ // would punch a hole instead of adding material. Edges parallel
+ // to t give a zero-area quad; Clipper discards those harmlessly.
+ if (q.is_clockwise())
+ q.reverse();
+ quads.emplace_back(std::move(q));
+ }
+
+ ExPolygons shifted = src;
+ for (ExPolygon &ex : shifted)
+ ex.translate(t);
+
+ // union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the
+ // argument above relies on.
+ return union_ex(union_ex(src, shifted), quads);
+}
+
+// ---------------------------------------------------------------- brim region
+
+ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
+ bool has_outer,
+ bool has_inner,
+ coord_t brim_width,
+ coord_t object_gap,
+ coord_t leading,
+ coord_t lateral,
+ const BeltBrimFrame &frame)
+{
+ if (footprint_flat.empty() || (! has_outer && ! has_inner))
+ return {};
+
+ ExPolygons out;
+
+ if (has_outer) {
+ // Offset the outer ring from the contours only, so a hole cannot punch
+ // through it. Same reasoning as the plate brim in Brim.cpp.
+ Polygons contours;
+ contours.reserve(footprint_flat.size());
+ for (const ExPolygon &ex : footprint_flat)
+ contours.emplace_back(ex.contour);
+
+ // Inner and outer boundary offset from the same polygon, to avoid
+ // round-off mismatch between them.
+ ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION);
+
+ // Close the interior before offsetting outwards. A belt contact patch is often a
+ // narrow, broken-up strip, and the offset rings of two islands less than
+ // 2 x brim_width apart merge and fill the space between them - space that lies
+ // UNDER the part, which is not what "outer brim" means. Closing also swallows
+ // holes in the patch for the same reason. Concavity-filling only, so an apron or
+ // any other outward protrusion is untouched.
+ ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner;
+
+ ExPolygons base = envelope;
+ if (leading > 0) {
+ // Sweep downhill from the gapped keep-out, so the apron is contiguous with
+ // the ring instead of starting inside the gap.
+ const Point t = frame.from_axis == 0 ?
+ Point(frame.downhill_sign() * leading, 0) :
+ Point(0, frame.downhill_sign() * leading);
+ base = union_ex(base, sweep_ex(envelope, t));
+ }
+ if (lateral > 0) {
+ // Across the belt, both ways. Swept from `base` so the apron is widened
+ // too, and in the flattened frame the cross-belt axis is unscaled, so this
+ // distance is already a true on-belt distance.
+ const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0);
+ ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y())));
+ base = union_ex(base, to_polygons(widened));
+ }
+ ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION);
+ expolygons_append(out, diff_ex(outer, envelope));
+ }
+
+ if (has_inner) {
+ // Holes reversed so a negative offset grows inward, mirroring Brim.cpp.
+ // No apron here: an apron growing into a hole interior is never useful.
+ Polygons holes;
+ for (const ExPolygon &ex : footprint_flat)
+ polygons_append(holes, ex.holes);
+ polygons_reverse(holes);
+ if (! holes.empty()) {
+ ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap));
+ ExPolygons hole_outer = offset_ex(holes, - float(object_gap));
+ expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes));
+ }
+ }
+
+ return union_ex(out);
+}
+
+// ---------------------------------------------------------------- line lattice
+
+std::vector belt_brim_line_positions(coord_t u_lo,
+ coord_t u_hi,
+ coord_t pitch_u,
+ coord_t u_anchor)
+{
+ std::vector out;
+ if (pitch_u <= 0 || u_hi <= u_lo)
+ return out;
+
+ // Walk the lattice from just below u_lo. Integer arithmetic throughout, so
+ // the half-open interval needs no epsilon: a point landing exactly on u_hi
+ // belongs to the next band.
+ int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1;
+ while (u_anchor + coord_t(k) * pitch_u < u_lo)
+ ++ k;
+ for (;; ++ k) {
+ const coord_t u = u_anchor + coord_t(k) * pitch_u;
+ if (u >= u_hi)
+ break;
+ out.emplace_back(u);
+ }
+ return out;
+}
+
+// ---------------------------------------------------------------- pipeline
+
+// A band of the belt surface as an explicit box, clamped to `bounds` along the
+// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those
+// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to
+// feed through the flattening scale.
+static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi)
+{
+ coord_t lo = scale_(u_lo);
+ coord_t hi = scale_(u_hi);
+ const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y();
+ const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y();
+ lo = std::max(lo, bmin);
+ hi = std::min(hi, bmax);
+ Polygon poly;
+ if (hi <= lo)
+ return poly;
+ if (from_axis == 0)
+ poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()),
+ Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) };
+ else
+ poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo),
+ Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) };
+ return poly;
+}
+
+ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut)
+{
+ if (region.empty())
+ return region;
+ BoundingBox keep_bb = get_extents(region);
+ keep_bb.offset(scale_(1.));
+ const bool low_side = frame.downhill_sign() < 0; // downhill is -u
+ const Polygon keep = band_box(keep_bb, frame.from_axis,
+ low_side ? unscale(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
+ low_side ? u_cut : unscale(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y()));
+ return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep });
+}
+
+// Everything the per-band line generator needs, gathered once per object.
+struct BeltBrimContext
+{
+ BeltFloorContext ctx;
+ BeltBrimFrame frame;
+ ExPolygons region; // brim region, object-local slicing XY
+ BoundingBox region_bbox;
+ Flow brim_flow;
+ coord_t pitch_u = 0;
+ coord_t u_anchor = 0;
+ double in_plane_pitch = 0.; // mm
+};
+
+// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z].
+static void belt_brim_band_paths(const BeltBrimContext &bc,
+ coordf_t print_z,
+ coordf_t height,
+ const Polygons &obstacles,
+ ExtrusionEntityCollection &out,
+ ExPolygons &areas_out)
+{
+ coordf_t u_lo = bc.ctx.cutoff_u(print_z - height);
+ coordf_t u_hi = bc.ctx.cutoff_u(print_z);
+ if (u_lo > u_hi)
+ std::swap(u_lo, u_hi);
+
+ // How wide this band is measured ON the belt, versus one nominal bead.
+ const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch();
+
+ // Fraction of the layer height at which a line sits above the belt. Toward the
+ // downhill edge, so the sheet is reasonably thick while the nozzle stays clear of
+ // the belt itself.
+ static constexpr double BAND_CLEARANCE_FRACTION = 0.75;
+
+ std::vector us;
+ double uniform_clearance = 0.; // 0 => derive per line from its own position
+ double line_pitch = bc.in_plane_pitch;
+ // One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
+ // first layer at 45 degrees): its flow is matched to the band, so the bead is that much
+ // wider. Two lattice lines in such a band would land almost on top of each other.
+ if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
+ // Steep belt, which is the normal case: the band is narrower than one bead, so
+ // exactly one line fits. Place it at a FIXED fraction of the band rather than
+ // on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
+ // point in its band, the clearance sweeps [0, height] from band to band, and the
+ // bead width therefore varies by 2x - visible as ragged, uneven brim lines.
+ // Anchoring to the band makes the clearance identical everywhere, so every bead
+ // is the same width.
+ //
+ // The spacing is then whatever the bands give (height / sin(tilt) on the belt)
+ // rather than the nominal bead spacing, so the flow below is matched to THAT
+ // pitch. Matched flow at the real pitch is what keeps the sheet uniform and
+ // gap-free; using nominal flow at band spacing would over-feed it.
+ us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height)));
+ uniform_clearance = BAND_CLEARANCE_FRACTION * height;
+ line_pitch = band_in_plane;
+ } else {
+ // Shallow belt: the band is wider than a bead, so it takes several lines and they
+ // have to sit on the nominal lattice. Their clearances then differ, and so do
+ // their widths - unavoidable here, but shallow belts are the rare case.
+ us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor);
+ }
+ if (us.empty())
+ return;
+
+ const Polygons region_polys = to_polygons(bc.region);
+
+ // One lattice line at a time: the clearance - and therefore the extrusion
+ // volume - is a property of the line's u, so the pieces of different lines
+ // must not be pooled before the flow is resolved.
+ // Overshoot the region so the clip, not the line's ends, decides the extent.
+ const coord_t margin = coord_t(SCALED_EPSILON) + 1;
+ coord_t u_prev = std::numeric_limits::min();
+ for (coord_t u : us) {
+ // Nozzle-to-belt clearance for this line. Constant along the line, because the
+ // belt height depends only on the shear-axis coordinate. Band-anchored lines
+ // share one clearance by construction; lattice lines (shallow belts, or a first
+ // layer thick enough that the band is wider than a bead) each get their own.
+ //
+ // A lattice line can fall where the belt is only a hair below the band's print_z.
+ // The bead there would be laid scraping the belt while its flow is sized for a
+ // taller cell, so it is moved uphill to the same fraction of the band the
+ // single-line case uses. (The clearance is along slice Z; the real gap under the
+ // nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
+ double clearance = uniform_clearance;
+ if (clearance <= 0.) {
+ const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
+ clearance = print_z - bc.ctx.floor_print_z(probe);
+ if (clearance < BAND_CLEARANCE_FRACTION * height) {
+ clearance = BAND_CLEARANCE_FRACTION * height;
+ u = scale_(bc.ctx.cutoff_u(print_z - clearance));
+ }
+ clearance = std::min(clearance, height);
+ }
+ // A line moved uphill can land on, or almost on, its neighbour; two beads closer
+ // than half a pitch would be laid into the same cell.
+ if (u_prev != std::numeric_limits::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
+ continue;
+ u_prev = u;
+
+ Polyline line;
+ if (bc.frame.from_axis == 0)
+ line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
+ Point(u, coord_t(bc.region_bbox.max.y() + margin)) };
+ else
+ line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u),
+ Point(coord_t(bc.region_bbox.max.x() + margin), u) };
+
+ Polylines pieces = intersection_pl(Polylines{ line }, region_polys);
+ if (! obstacles.empty())
+ pieces = diff_pl(pieces, obstacles);
+ if (pieces.empty())
+ continue;
+
+ // with_cross_section, not with_height: it reaches the prescribed volume while
+ // KEEPING the extrusion spacing, so the bead is sized to fill exactly one
+ // pitch x clearance cell of the sheet.
+ const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance));
+
+ // Footprint of these beads, for the first-layer convex hull and bbox.
+ for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width())))
+ areas_out.emplace_back(ExPolygon(p));
+
+ extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)),
+ erBrim, f.mm3_per_mm(), f.width(), float(clearance));
+ }
+}
+
+// Union of everything extruded at `print_z` that the brim must keep clear of, expressed
+// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can
+// overhang outside the belt footprint and land in the brim ring, which the flattened
+// brim_object_gap - a belt-plane separation - does not cover.
+//
+// SEQUENCING: this reads every object's layers and this object's own support layers.
+// Another object's support step shifts that object's layer Z into the object frame for
+// the duration of the run (PrintObject::_generate_support_material()), so the brims must
+// not overlap with the parallel support step: Print::process() generates them one object
+// after the other once that step is over (PrintObject::generate_belt_brim()), and an
+// object that arrives on or leaves the plate invalidates the other brim owners' support
+// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this
+// object's supports are dodged; another object's support at the same Z is not.
+// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole
+// objects are skipped without materialising their polygons. On a typical plate the
+// objects do not overlap and every foreign object drops out here, which matters because
+// this runs once per band - hundreds of times per object.
+static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self,
+ const BoundingBox ®ion_bbox, coordf_t print_z, coordf_t tol)
+{
+ const Point shift_self = self.instances().empty() ? Point(0, 0)
+ : self.instances().front().shift_without_plate_offset();
+ Polygons out;
+ for (const PrintObject *o : print.objects()) {
+ const bool is_self = (o == &self);
+ for (const PrintInstance &inst : o->instances()) {
+ const Point delta = inst.shift_without_plate_offset() - shift_self;
+ if (const Layer *l = o->get_layer_at_printz(print_z, tol)) {
+ BoundingBox lb = get_extents(l->lslices);
+ lb.translate(delta.x(), delta.y());
+ if (lb.overlap(region_bbox)) {
+ Polygons ps = to_polygons(l->lslices);
+ for (Polygon &p : ps)
+ p.translate(delta);
+ polygons_append(out, std::move(ps));
+ }
+ }
+ if (! is_self)
+ continue;
+ if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) {
+ Polygons ps = sl->support_fills.polygons_covered_by_spacing();
+ for (Polygon &p : ps)
+ p.translate(delta);
+ polygons_append(out, std::move(ps));
+ }
+ }
+ }
+ if (out.size() < 2)
+ return out; // union_() of 0 or 1 polygons is pure overhead
+ return union_(out);
+}
+
+void make_belt_brim(PrintObject &object)
+{
+ object.clear_belt_brim();
+ if (! object.has_belt_brim())
+ return;
+
+ const Print &print = *object.print();
+ BeltBrimContext bc;
+ if (! bc.ctx.init(object.slicing_parameters(), print.config()))
+ return;
+ bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() };
+
+ const size_t nlayers = object.layers().size();
+ if (nlayers == 0)
+ return;
+
+ // 1. Belt footprint: the union of each layer's slice clipped to that layer's
+ // own contact band. This is the object's bottom face, which on a belt is
+ // spread over every layer instead of sitting in layer 0.
+ ExPolygons footprint_acc;
+ // The first layer that touches the belt: where the leading-edge brim is cut.
+ const Layer *first_contact = nullptr;
+ for (size_t i = 0; i < nlayers; ++ i) {
+ const Layer &layer = *object.layers()[i];
+ if (layer.lslices.empty())
+ continue;
+ // print_z - height, not the previous layer's print_z: variable layer
+ // heights make the latter wrong.
+ coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height);
+ coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z);
+ if (u_lo > u_hi)
+ std::swap(u_lo, u_hi);
+ BoundingBox bb = get_extents(layer.lslices);
+ bb.offset(scale_(1.));
+ const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi);
+ if (band.empty())
+ continue;
+ ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band });
+ if (contact.empty())
+ continue;
+ if (first_contact == nullptr)
+ first_contact = &layer;
+ expolygons_append(footprint_acc, std::move(contact));
+ }
+ const ExPolygons footprint = union_ex(footprint_acc);
+ if (footprint.empty())
+ return;
+
+ // 2. Brim region, offset in the flattened (true on-belt) metric.
+ const PrintObjectConfig &cfg = object.config();
+ bc.brim_flow = print.brim_flow();
+ const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR;
+ // Quantize to an even number of lines, as the plate brim does.
+ const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2);
+ const coord_t leading = scale_(cfg.leading_brim_length.value);
+ const coord_t lateral = scale_(cfg.extra_brim_width.value);
+ const coord_t gap = scale_(cfg.brim_object_gap.value);
+
+ // Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width
+ // heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only
+ // is an outer brim too; it is narrowed down to the first contact below.
+ const BrimType bt = cfg.brim_type.value;
+ const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner
+ || bt == btAutoBrim || bt == btEar || bt == btPainted
+ || bt == btLeadingEdgeOnly;
+ const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner;
+
+ bc.region = belt_unflatten(
+ belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner,
+ width, gap, leading, lateral, bc.frame),
+ bc.frame);
+
+ if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty())
+ // The cut is the uphill edge of the first contact's band: everything past it
+ // belongs to later contacts. The first contact is the first layer that touches
+ // the belt (step 1), neither layers().front(), an empty lead-in layer, nor the
+ // first layer with geometry, which is an overhang's tip when the part overhangs
+ // its leading end: both lie ahead of the part.
+ bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z));
+
+ if (bc.region.empty())
+ return;
+ bc.region_bbox = get_extents(bc.region);
+
+ // 3. Line lattice. Fixed pitch in the flattened metric, anchored at the
+ // footprint's leading-most edge so lines stay collinear across
+ // disconnected islands and across the apron prologue.
+ bc.pitch_u = std::max(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt()));
+ bc.in_plane_pitch = unscale(bc.pitch_u) * bc.frame.u_stretch();
+ {
+ const BoundingBox fbb = get_extents(footprint);
+ const bool low_side = bc.frame.shear > 0.;
+ bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x())
+ : (low_side ? fbb.min.y() : fbb.max.y());
+ }
+
+ // 4. Bands coincident with an object layer.
+ std::vector by_layer(nlayers);
+ std::vector areas_by_layer(nlayers);
+ for (size_t i = 0; i < nlayers; ++ i) {
+ const Layer &layer = *object.layers()[i];
+ const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height);
+ belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]);
+ }
+
+ // 5. Apron prologue: the part of the region downhill of the object's first
+ // layer, which has no object layer to ride on.
+ std::vector prologue;
+ {
+ const Layer &first = *object.layers().front();
+ const coordf_t h = first.height;
+ const bool low_side = bc.frame.shear > 0.;
+ const coord_t u_lead_s = bc.frame.from_axis == 0
+ ? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x())
+ : (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y());
+ const coordf_t u_lead = unscale(u_lead_s);
+ // print_z at which the belt surface crosses the region's leading edge.
+ const coordf_t z_lead = bc.ctx.shear_factor() * u_lead
+ + bc.ctx.floor_offset() + bc.ctx.z_shift();
+ if (h > EPSILON)
+ for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) {
+ const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h);
+ BeltBrimBand band;
+ band.print_z = z;
+ band.height = h;
+ belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas);
+ if (! band.fills.empty())
+ prologue.emplace_back(std::move(band));
+ }
+ // Lowest Z first, so collect_layers_to_print sees them in print order.
+ std::reverse(prologue.begin(), prologue.end());
+ }
+
+ object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue));
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltBrim.hpp b/src/libslic3r/BeltBrim.hpp
new file mode 100644
index 0000000000..3f4fcc97de
--- /dev/null
+++ b/src/libslic3r/BeltBrim.hpp
@@ -0,0 +1,178 @@
+#ifndef slic3r_BeltBrim_hpp_
+#define slic3r_BeltBrim_hpp_
+
+#include "ExPolygon.hpp"
+#include "ExtrusionEntityCollection.hpp"
+#include "Point.hpp"
+#include "Polyline.hpp"
+#include "libslic3r.h"
+
+#include
+#include
+
+// Belt-printer brim geometry.
+//
+// A belt printer slices in a ROTATED frame, so the belt surface is not the
+// Z=0 bed plane but a tilted plane in slicing space:
+//
+// z_slicing(u) = shear * u + floor_offset + z_shift, u = X or Y
+//
+// where `shear == tan(tilt)` (SlicingParameters::belt_floor_shear_factor) and
+// the axis is selected by SlicingParameters::belt_floor_from_axis. See
+// Support/BeltFloorContext.hpp for the canonical accessors.
+//
+// Consequences that drive everything in this file:
+//
+// * A horizontal slicing layer touches the belt only along a narrow strip at
+// its leading edge, `layer_height / shear` wide (~0.2 mm at 45 degrees).
+// The object's belt footprint - its bottom face - is therefore spread over
+// every layer, not contained in layer 0.
+// * Distances measured in slicing XY are NOT on-belt distances: moving `du`
+// along the shear axis travels `du / cos(tilt)` across the belt. So brim
+// offsets have to be taken in a "flattened" space where the shear axis is
+// stretched by `1 / cos(tilt)`, then mapped back.
+// * Brim ahead of the part (downhill) lies at slicing Z BELOW the object's
+// first layer, because the object's layer 0 is precisely its leading
+// contact with the belt.
+//
+// Everything here is pure geometry on ExPolygons/Polylines so it can be unit
+// tested without a Print. Keep user-visible strings out of this file: it is
+// not listed in localization/i18n/list.txt.
+
+namespace Slic3r {
+
+// Tilt window within which the BELT plane, not the bed plane, is the adhesion
+// surface. Below ~1 degree a belt is a flat bed as far as adhesion goes, and the
+// contact band would be layer_height/sin(tilt) - tens of millimetres - so the
+// ordinary plate brim is both correct and cheaper. Above ~85 degrees the whole
+// brim compresses into a sliver and is not worth generating.
+inline constexpr double BELT_BRIM_MIN_TILT_DEG = 1.;
+inline constexpr double BELT_BRIM_MAX_TILT_DEG = 85.;
+
+// Description of the tilted belt plane, reduced to what the brim geometry needs.
+struct BeltBrimFrame
+{
+ // tan(tilt). Sign selects which way is downhill.
+ double shear = 0.;
+ // 0 = X, 1 = Y. Matches BeltFloorContext::from_axis().
+ int from_axis = 1;
+
+ // 1 / cos(tilt). Stretch factor that turns a projected distance along
+ // `from_axis` into the true distance travelled across the belt.
+ double u_stretch() const { return std::sqrt(1. + shear * shear); }
+ // cos(tilt). The inverse mapping.
+ double cos_tilt() const { return 1. / this->u_stretch(); }
+ // Downhill is where the belt surface is lower, i.e. printed earlier, i.e.
+ // the leading edge of the part. For shear > 0 that is -u.
+ int downhill_sign() const { return shear > 0. ? -1 : +1; }
+};
+
+// Scale only the `from_axis` component by `factor`, rounding to nearest.
+//
+// Deliberately not MultiPoint::scale(fx, fy) / ExPolygon::scale(fx, fy): those
+// truncate toward zero, which is asymmetric about the origin and loses up to a
+// full coordinate unit per vertex on every round trip.
+ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor);
+Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor);
+
+// Into / out of the space where Euclidean offsets equal true on-belt distances.
+inline ExPolygons belt_flatten(const ExPolygons &src, const BeltBrimFrame &frame)
+ { return belt_scale_u(src, frame, frame.u_stretch()); }
+inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &frame)
+ { return belt_scale_u(src, frame, frame.cos_tilt()); }
+
+// Minkowski sum of `src` with the segment [0, t]: the region swept by sliding
+// `src` along t. Used to grow the brim downhill for "extra brim width".
+//
+// Implemented as union_(P, P + t, {parallelogram per boundary edge}) over ALL
+// contours including holes, with every parallelogram forced counter-clockwise
+// so the non-zero fill rule closes holes narrower than t along the sweep
+// direction. A hole survives exactly when it is wider than |t| measured along
+// t - not when it is wider in its narrowest Euclidean direction.
+ExPolygons sweep_ex(const ExPolygons &src, const Point &t);
+
+// "Leading edge only": keep the part of a brim region (unflattened, slicing XY)
+// at or downhill of the object's first contact with the belt, so the part is
+// supported as it lands and nothing is printed alongside it afterwards. `u_cut`
+// is the uphill edge of the first layer's contact band along `frame.from_axis`,
+// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side
+// `frame.downhill_sign()` points to.
+ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut);
+
+// Brim region for one already-flattened belt footprint. All lengths are scaled
+// and measured in the flattened (true on-belt) metric.
+//
+// `has_outer` / `has_inner` are the resolved BrimType: belt printers collapse
+// Auto / Mouse ear / Painted to outer-only, so the caller does that mapping and
+// this function never needs PrintConfig.
+//
+// Two directional extras are applied to the footprint before the outer offset, so
+// each one buys reach in one direction only:
+//
+// `leading` (leading_brim_length) sweeps the footprint DOWNHILL along the belt,
+// so every leading-facing edge gains an apron ahead of it.
+// `lateral` (extra_brim_width) sweeps it BOTH WAYS across the belt, widening
+// the brim sideways without pushing it further ahead or behind.
+//
+// Neither is applied to the inner (hole) ring.
+ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
+ bool has_outer,
+ bool has_inner,
+ coord_t brim_width,
+ coord_t object_gap,
+ coord_t leading,
+ coord_t lateral,
+ const BeltBrimFrame &frame);
+
+// Brim line positions for one layer band.
+//
+// Lines sit on a fixed lattice `u_anchor + k * pitch_u` so the on-belt spacing
+// between neighbouring brim lines is constant regardless of how the lattice
+// falls across layer bands. Snapping to band centres instead would quantise
+// the spacing to whole bands and under-deposit by ~35% at 45 degrees.
+//
+// The band is half-open, [u_lo, u_hi), so every lattice point belongs to
+// exactly one band: none duplicated at a boundary, none dropped. A band
+// narrower than the pitch simply yields nothing; a band much wider (shallow
+// tilt) yields several lines.
+std::vector belt_brim_line_positions(coord_t u_lo,
+ coord_t u_hi,
+ coord_t pitch_u,
+ coord_t u_anchor);
+
+// ---------------------------------------------------------------- pipeline
+
+// One brim-only layer printed BEFORE the object's first layer, carrying the
+// apron that has to be stuck to the belt ahead of the part.
+//
+// Deliberately not a Layer subclass. A synthetic Layer would inherit id()
+// semantics that leak into initial-layer temperature selection, the spiral vase
+// probe, gradual interpolation, avoid-crossing-perimeters and cooling, all of
+// which key off Layer::id() == 0 or off a layer's regions. A plain record
+// carries only what the emitter needs.
+//
+// `height` is the LAYER height, used for the Z move and ordering metadata only.
+// Each extrusion path inside `fills` carries its own height, equal to that
+// line's nozzle-to-belt clearance, which varies across the band.
+struct BeltBrimBand
+{
+ coordf_t print_z = 0.;
+ coordf_t height = 0.;
+ // erBrim paths in the object's local slicing frame, untranslated.
+ ExtrusionEntityCollection fills;
+ // Footprint of those paths, for the first-layer convex hull / bbox.
+ ExPolygons areas;
+};
+
+class PrintObject;
+
+// Generate the belt brim for one object: fills its per-object-layer bands and
+// its apron prologue. No-op unless PrintObject::has_belt_brim().
+//
+// Runs inside posSupportMaterial rather than the brim step, because the prologue
+// print_z values must exist before ToolOrdering is built at psWipeTower.
+void make_belt_brim(PrintObject &object);
+
+} // namespace Slic3r
+
+#endif // slic3r_BeltBrim_hpp_
diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp
new file mode 100644
index 0000000000..e76e76b84f
--- /dev/null
+++ b/src/libslic3r/BeltGCode.cpp
@@ -0,0 +1,50 @@
+#include "BeltGCode.hpp"
+#include "GCodeWriter.hpp"
+#include "GCode/BeltKinematics.hpp"
+#include "BeltTransform.hpp"
+#include "Print.hpp"
+#include "Point.hpp"
+#include "PrintConfig.hpp"
+#include "libslic3r.h"
+#include
+
+namespace Slic3r {
+
+void BeltGCode::init_belt_writer(Print &print)
+{
+ // Axis remap and build volume max are set by base GCode after init_belt_writer
+ // returns; set_kinematics() replays them, so install order does not matter.
+ install_belt_kinematics(m_writer, print.config());
+ m_writer.set_force_normal_lift(true);
+}
+
+void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
+{
+ 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);
+ // Machine-frame transform: shear (cot) + scale (1/|sin|) 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*/)
+{
+ // Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
+ // the origin through the belt pipeline so that back_transform(T * origin) =
+ // origin (correct machine position). The back_transform applied during
+ // G-code emission is the inverse of the forward transform.
+
+ // 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
diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp
new file mode 100644
index 0000000000..1631a7dc9c
--- /dev/null
+++ b/src/libslic3r/BeltGCode.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "GCode.hpp"
+#include "Point.hpp"
+#include "Print.hpp"
+
+namespace Slic3r {
+
+// Belt-printer-specific GCode export.
+//
+// Inherits from GCode and overrides virtual hooks to:
+// - Install a BeltKinematics on the GCodeWriter
+// - Write belt configuration to the G-code header
+// - Adjust the origin for global pre-slice transforms when switching instances
+// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(),
+// which the base GCode::should_disable_arc_fitting() consults -- no override needed.)
+class BeltGCode : public GCode
+{
+protected:
+ void init_belt_writer(Print &print) override;
+ void write_belt_header(GCodeOutputStream &file, const Print &print) override;
+ void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
+};
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp
new file mode 100644
index 0000000000..5fb925f9aa
--- /dev/null
+++ b/src/libslic3r/BeltPurge.cpp
@@ -0,0 +1,433 @@
+// ORCA-Belt: backend of the belt purge tower (the belt replacement for the
+// classic wipe/prime tower).
+//
+// Kept in its own translation unit so the belt-purge logic stays out of the way
+// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries
+// no regression risk for normal printers: none of these methods do anything
+// unless the print is a belt printer with the belt purge tower enabled.
+//
+// Print::has_belt_purge_tower() - is the belt purge tower active?
+// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the
+// printed objects' grid
+// Print::_plan_belt_purge() - route filament-change purging into the
+// prism (flush-into-objects), no wipe tower
+// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid
+// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap
+//
+// (Declarations live in Print.hpp alongside the rest of the Print interface.)
+
+#include "Print.hpp"
+#include "PrintConfig.hpp"
+#include "Exception.hpp"
+#include "GCode/ToolOrdering.hpp"
+#include "Layer.hpp"
+#include "ExtrusionEntity.hpp"
+#include "ExtrusionEntityCollection.hpp"
+#include "I18N.hpp"
+#include "format.hpp"
+#include "LocalesUtils.hpp"
+#include "libslic3r.h"
+#include "BeltBrim.hpp"
+#include "PrintBase.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Slic3r {
+
+// Belt purge prism: purging after filament changes is routed into a sliced
+// prism object via the flush-into-objects machinery instead of a wipe tower.
+bool Print::has_belt_purge_tower() const
+{
+ // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower
+ // option (not the classic enable_prime_tower).
+ if (!(m_config.belt_printer.value
+ && m_config.enable_belt_purge_tower.value
+ && !m_config.spiral_mode.value
+ && m_config.filament_diameter.values.size() > 1))
+ return false;
+
+ return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
+ return object->config().belt_purge_tower_object.value;
+ });
+}
+
+// Belt mode: align ALL objects on the plate (the printed objects AND the purge
+// prism) onto one common layer grid, so the prism can absorb every toolchange.
+//
+// After belt slicing each object's layer print_z carries a per-object global z
+// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so
+// objects at different belt-Y positions get layer grids with DIFFERENT residues
+// (mod layer height). Purge marking looks absorbers up with
+// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only
+// absorbs into the prism if the prism has a layer at B's print_z. Snapping only
+// the prism to one object therefore worked for a single (assembled) multi-color
+// object but failed with multiple separate objects — the prism could follow only
+// one grid, and toolchanges on the others went unabsorbed ("multiple layer
+// grids" warning).
+//
+// Fix: pick one reference grid (the tallest object) and shift every object onto
+// it. Each shift is at most half a layer height — a sub-100µm move along the
+// belt, the very same mechanism the per-object global_z_offset already uses, and
+// it keeps each object internally consistent (belt_shift_layer_grid moves the
+// object's layers, its support layers, and its belt floor together). Equal layer
+// height across objects is enforced by Print::validate(), so once residues match
+// every object steps on the same lattice {ref_offset + k*h} and every toolchange
+// layer coincides with a prism layer.
+void Print::_align_belt_purge_layers()
+{
+ PrintObject *prism = nullptr;
+ for (PrintObject *po : m_objects)
+ if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
+ prism = po;
+ break;
+ }
+ if (prism == nullptr || prism->layers().empty())
+ return;
+
+ const double h = prism->config().layer_height.value;
+ if (h <= EPSILON)
+ return;
+
+ // Grid residue of an object's layer grid: identical for all of an object's
+ // layers above the first since they step by h.
+ auto grid_offset = [h](const PrintObject *po) -> double {
+ if (po->layers().empty())
+ return 0.;
+ const double z = po->layers().front()->print_z;
+ return z - std::floor(z / h) * h; // in [0, h)
+ };
+
+ // Reference grid: the tallest non-prism object (proxy for the object with
+ // the most toolchange layers — minimizes how far the rest must move).
+ const PrintObject *ref = nullptr;
+ double ref_top = -std::numeric_limits::max();
+ for (const PrintObject *po : m_objects) {
+ if (po->config().belt_purge_tower_object.value || po->layers().empty())
+ continue;
+ const double top = po->layers().back()->print_z;
+ if (top > ref_top) {
+ ref_top = top;
+ ref = po;
+ }
+ }
+ if (ref == nullptr)
+ return;
+
+ const double ref_offset = grid_offset(ref);
+
+ // Snap every object (printed objects AND the prism) onto the reference grid.
+ for (PrintObject *po : m_objects) {
+ if (po->layers().empty())
+ continue;
+ double delta = ref_offset - grid_offset(po);
+ if (delta > 0.5 * h)
+ delta -= h;
+ else if (delta <= -0.5 * h)
+ delta += h;
+ po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
+ }
+}
+
+// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
+// into the belt purge prism (and any other flush_into_* object) using the
+// flush-into-objects machinery, without generating classic wipe tower G-code.
+// The toolchange itself is emitted by GCode::set_extruder() via the
+// change_filament_gcode macro; the overrides marked here make the new
+// filament's first extrusions land in the purge prism.
+void Print::_plan_belt_purge()
+{
+ m_wipe_tower_data.clear();
+
+ // psWipeTower may be invalidated without posSlice (for example after a
+ // filament-map or tool-ordering change). Restore a prism shortened by the
+ // previous plan so a newly higher toolchange can use its original layers.
+ for (PrintObject *po : m_objects)
+ if (po->config().belt_purge_tower_object.value)
+ po->belt_undo_purge_plan();
+
+ // Must run before ToolOrdering is built: LayerTools merge per-object layer
+ // print_z values, and the prism only absorbs purge where its (snapped)
+ // layers coincide with the toolchange layers.
+ this->_align_belt_purge_layers();
+
+ const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size();
+
+ // No initial priming extrusions: there is no tower to prime on.
+ m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false);
+ m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
+
+ if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
+ throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
+
+ // Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
+ // FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
+ // neither object nor support and so never gets the flag even when the print changes filament.
+ {
+ bool any_change = false;
+ unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder();
+ for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools())
+ for (const unsigned int e : lt.extruders)
+ if (e != cur) { any_change = true; cur = e; }
+ if (! any_change)
+ return;
+ }
+
+ this->throw_if_canceled();
+
+ // Flush volumes per filament pair, mirroring the generic wipe tower path:
+ // full flush matrix for single extruder multi material with purging enabled,
+ // plain prime volume otherwise.
+ std::vector flush_matrix(cast(
+ get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size())));
+ std::vector> wipe_volumes;
+ for (unsigned int i = 0; i < number_of_extruders; ++i)
+ wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * number_of_extruders,
+ flush_matrix.begin() + (i + 1) * number_of_extruders));
+ const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
+ const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0);
+
+ // Cancel the purge prism early: pre-scan the tool ordering for the highest
+ // print_z that actually has a toolchange, then drop the prism's layers above
+ // it so the tower stops at the last color swap (saves filament/time). This
+ // MUST happen before the marking loop below: ensure_perimeters_infills_order
+ // force-overrides the prism's extrusions on every layer (it is a dedicated
+ // flush object), so truncating afterwards would leave dangling overrides
+ // pointing into deleted layers.
+ {
+ // The tool ordering covers the WHOLE print, and the prism is a printed
+ // object in it. Left unbounded, the scan below sees the prism's own
+ // toolchanges on layers above every model object -- the prism runs past
+ // them by design (ramp/height compensation at the tilted ends) -- so
+ // last_tc_z lands at the prism's own top and the truncation cancels
+ // nothing. The tower ends up justifying its own existence.
+ //
+ // Nothing above the tallest printed object can require a color change,
+ // so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
+ // z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
+ // 39.4 mm of tower that no swap ever needed.
+ // Support layers count too: on a belt they can extend above the object's
+ // own top, and a toolchange there is a real one.
+ double obj_top_z = -1.;
+ for (const PrintObject *po : m_objects) {
+ if (po->config().belt_purge_tower_object.value)
+ continue;
+ if (!po->layers().empty())
+ obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
+ if (!po->support_layers().empty())
+ obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
+ }
+
+ double last_tc_z = -1.;
+ unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
+ for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) {
+ // layer_tools() is ordered by print_z ascending.
+ if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
+ break;
+ for (const unsigned int e : lt.extruders)
+ if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
+ }
+ // Deliberately NOT cancelling the prism outright when no object toolchange
+ // exists: belt_truncate_layers_above(0.) empties m_layers, and an object
+ // with zero layers is not something the rest of the pipeline expects. The
+ // GUI already declines to create a prism unless more than one filament is
+ // in use, so this case is a stale prism, not a hot path -- leave it whole
+ // rather than risk a zero-layer object.
+ if (last_tc_z >= 0.)
+ for (PrintObject *po : m_objects)
+ if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
+ po->belt_truncate_layers_above(last_tc_z);
+ break;
+ }
+ }
+
+ // The prism only absorbs purge at toolchange layers whose print_z coincides
+ // with one of its own layers.
+ PrintObject *prism_po = nullptr;
+ for (PrintObject *po : m_objects)
+ if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
+
+ float total_leftover = 0.f;
+ float worst_layer_leftover = 0.f;
+ double worst_layer_z = 0.;
+
+ unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
+ for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
+ float layer_leftover = 0.f;
+ for (const unsigned int extruder_id : layer_tools.extruders) {
+ if (extruder_id == current_extruder_id)
+ continue;
+ float volume_to_wipe = use_flush_matrix ?
+ wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
+ (float) m_config.prime_volume;
+ float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
+ volume_to_wipe);
+ layer_leftover += leftover;
+ current_extruder_id = extruder_id;
+ }
+
+ // Plastic saving: drop the prism's fills that no toolchange on this layer
+ // claimed. At this point the prism's OVERRIDDEN fills are exactly the
+ // purge; the rest would print as solid infill in the prism's own filament
+ // for nothing -- which is the whole prism on a layer with no toolchange
+ // (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
+ // left alone so the bar keeps a continuous wall along the belt.
+ //
+ // Non-destructive: the entities are stashed with their positions and put
+ // back by belt_restore_dropped_fills() at the top of the next plan. An
+ // earlier version deleted them outright, which broke replanning when a
+ // later tool ordering needed what this one had not claimed -- that is why
+ // it was removed rather than kept.
+ if (prism_po != nullptr) {
+ const auto &we = layer_tools.wiping_extrusions();
+ prism_po->belt_drop_unclaimed_fills(
+ prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
+ [&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
+ }
+
+ layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
+ if (layer_leftover > 0.f) {
+ total_leftover += layer_leftover;
+ if (layer_leftover > worst_layer_leftover) {
+ worst_layer_leftover = layer_leftover;
+ worst_layer_z = layer_tools.print_z;
+ }
+ }
+ this->throw_if_canceled();
+ }
+
+ if (total_leftover > 1.f) {
+ this->active_step_add_warning(
+ PrintStateBase::WarningLevel::CRITICAL,
+ Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not "
+ "be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. "
+ "Increase the belt purge tower width, or reduce flushing volumes."),
+ int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
+ Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
+ }
+}
+
+// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset
+// application in slice() — layer print_z and belt_floor_z_shift move together
+// so belt floor clipping stays consistent with the shifted grid. Used by
+// Print::_align_belt_purge_layers() to snap the purge prism onto the printed
+// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift
+// of the prism along the belt.
+void PrintObject::belt_shift_layer_grid(double delta)
+{
+ if (std::abs(delta) < EPSILON)
+ return;
+ for (Layer *layer : m_layers)
+ layer->print_z += delta;
+ for (SupportLayer *layer : m_support_layers)
+ layer->print_z += delta;
+ // The brim's apron bands below the first layer carry their own print_z.
+ for (BeltBrimBand &band : m_belt_brim_prologue)
+ band.print_z += delta;
+ m_slicing_params.belt_floor_z_shift += delta;
+ // The grid stays shifted across a support-only or brim-only change (posSlice does
+ // not rerun), so everything slice() derived from it has to follow: the cached floor
+ // that update_slicing_parameters() restores, and the global offset the organic
+ // support layers and the adaptive infill octree are placed with. Left alone, the
+ // next alignment finds a delta of 0 and the floor and the supports sit up to half
+ // a layer off the grid, unlike a fresh slice.
+ if (m_belt_floor_z_shift_cache_valid)
+ m_belt_floor_z_shift_cached += delta;
+ m_belt_global_z_offset += delta;
+}
+
+// Belt mode: drop layers strictly above z (used to cancel the purge prism early
+// once there are no more toolchanges above z, so the tower stops at the last
+// color swap instead of wasting filament up the rest of the belt). Each layer's
+// cross-section is already sliced, so removing upper layers does not affect the
+// last toolchange's coverage. Deletes the Layer objects and clears the new top
+// layer's upper-layer link. Returns the number of layers removed.
+size_t PrintObject::belt_truncate_layers_above(coordf_t z)
+{
+ // A repeated plan always starts from the restored full layer set.
+ assert(m_belt_truncated_layers.empty());
+ size_t keep = m_layers.size();
+ while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON)
+ --keep;
+ if (keep >= m_layers.size())
+ return 0;
+ const size_t removed = m_layers.size() - keep;
+ m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end());
+ m_layers.resize(keep);
+ if (!m_layers.empty())
+ m_layers.back()->upper_layer = nullptr;
+ return removed;
+}
+
+// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
+//
+// Called per layer from _plan_belt_purge(), after the real-purge marking and
+// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
+// remaining fill on the prism (it is a dedicated flush object), so afterwards
+// everything looks claimed and nothing could be distinguished.
+size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function &claimed)
+{
+ if (layer == nullptr)
+ return 0;
+ size_t dropped = 0;
+ for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
+ LayerRegion *lr = layer->get_region(ri);
+ auto &ents = lr->fills.entities;
+ ExtrusionEntitiesPtr keep;
+ keep.reserve(ents.size());
+ for (size_t i = 0; i < ents.size(); ++i) {
+ if (claimed(ents[i])) {
+ keep.emplace_back(ents[i]);
+ } else {
+ // Stash with its original index so the restore is exact.
+ m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
+ ++dropped;
+ }
+ }
+ ents = std::move(keep);
+ }
+ return dropped;
+}
+
+void PrintObject::belt_restore_dropped_fills()
+{
+ if (m_belt_dropped_fills.empty())
+ return;
+ // Ascending index per (layer, region): inserting in that order lands every
+ // entity back at its original position, because each insertion shifts only
+ // the entries after it, which are themselves still to be inserted.
+ std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
+ [](const BeltDroppedFill &a, const BeltDroppedFill &b) {
+ if (a.layer != b.layer) return a.layer < b.layer;
+ if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
+ return a.index < b.index;
+ });
+ for (const BeltDroppedFill &d : m_belt_dropped_fills) {
+ auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
+ ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
+ }
+ m_belt_dropped_fills.clear();
+}
+
+void PrintObject::belt_restore_truncated_layers()
+{
+ if (m_belt_truncated_layers.empty())
+ return;
+
+ m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end());
+ m_belt_truncated_layers.clear();
+ for (size_t i = 0; i < m_layers.size(); ++i) {
+ m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1];
+ m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr;
+ }
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp
new file mode 100644
index 0000000000..7b25ac53ec
--- /dev/null
+++ b/src/libslic3r/BeltSliceStrategy.cpp
@@ -0,0 +1,82 @@
+#include "BeltSliceStrategy.hpp"
+#include "Model.hpp"
+#include "BeltTransform.hpp"
+#include "Point.hpp"
+#include "PrintConfig.hpp"
+
+#include
+#include
+
+namespace Slic3r {
+
+void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
+ const PrintConfig &config,
+ const ModelVolumePtrs &model_volumes,
+ double *out_belt_min_z)
+{
+ // 1. 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_rotation)
+ return;
+
+ // 2. 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.
+
+ //
+ // The lift is measured to the lowest point of the SUPPORT region, not of the
+ // mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
+ // coordinate) runs below every vertex, and under the leading end of an
+ // overhang it lies below the lowest vertex by up to the overhang's length
+ // times the shear. Supports have to reach that floor, and every support
+ // generator works in layers at z >= 0, so z = 0 has to be the lowest floor
+ // point under the footprint. The layers between it and the first vertex
+ // come out empty, which belt slicing already tolerates (the bottom corner
+ // of a tilted part is a point). Vertices on the belt have z == floor, so
+ // for a part resting on the belt this is simply the floor at its leading
+ // extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
+ BeltTransformPipeline::BeltFloorParams floor;
+ const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
+ double min_z = std::numeric_limits::max();
+ for (const ModelVolume *mv : model_volumes) {
+ if (!mv->is_model_part()) continue;
+ Transform3d vol_trafo = trafo * mv->get_matrix();
+ const auto &its = mv->mesh().its;
+ for (const stl_vertex &v : its.vertices) {
+ Vec3d vm = v.cast();
+ Vec3d pt = vol_trafo * vm;
+ min_z = std::min(min_z, pt.z());
+ if (has_floor)
+ min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
+ }
+ }
+ if (has_floor && min_z != std::numeric_limits::max())
+ min_z -= BeltTransformPipeline::frame_margin(floor);
+ const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.;
+ 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.
+ if (out_belt_min_z && config.belt_printer.value) {
+ *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.;
+ }
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp
new file mode 100644
index 0000000000..1385818b5c
--- /dev/null
+++ b/src/libslic3r/BeltSliceStrategy.hpp
@@ -0,0 +1,34 @@
+#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 mesh transforms applied before slicing on a belt printer:
+// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a
+// g-code-side stage, see MachineFrameTransform)
+// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the
+// belt below its footprint
+//
+// Isolates this belt-specific logic from the generic slicing pipeline in
+// PrintObjectSlice.cpp.
+class BeltSliceStrategy
+{
+public:
+ // Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt
+ // rotation is configured.
+ //
+ // out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms.
+ static void apply_preslice_transforms(Transform3d &trafo,
+ const PrintConfig &config,
+ const ModelVolumePtrs &model_volumes,
+ double *out_belt_min_z = nullptr);
+};
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp
new file mode 100644
index 0000000000..9863e80831
--- /dev/null
+++ b/src/libslic3r/BeltTransform.cpp
@@ -0,0 +1,153 @@
+#include "BeltTransform.hpp"
+#include "Model.hpp"
+#include "BoundingBox.hpp"
+#include "Config.hpp"
+#include "Geometry.hpp"
+#include "Point.hpp"
+#include "PrintConfig.hpp"
+#include "libslic3r.h"
+
+#include
+#include
+#include
+#include
+
+namespace Slic3r {
+
+// ---- Matrix builders ------------------------------------------------------
+
+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: the rotation. (Shear & scale are a g-code-side
+ // stage, not part of the mesh transform.)
+ Transform3d combined = Transform3d::Identity();
+ combined.linear() = build_rotation_matrix(config);
+ return combined;
+}
+
+// ---- Belt floor parameters ------------------------------------------------
+
+namespace {
+
+// Belt floor in the rotated slicer frame: 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
+void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
+{
+ double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
+ switch (rot_axis) {
+ case BeltRotationAxis::X:
+ out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
+ out.from_axis = 1; // Y
+ break;
+ case BeltRotationAxis::Y:
+ out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
+ out.from_axis = 0; // X
+ break;
+ case BeltRotationAxis::Z:
+ default:
+ out.shear_factor = 0.0;
+ out.from_axis = 1;
+ break;
+ }
+}
+
+// Z of the belt floor directly under a point of the rotated (unshifted) frame.
+inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
+{
+ return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
+}
+
+
+BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
+ const PrintConfig &config, const BoundingBoxf3 &bb, double original_height)
+{
+ BeltTransformPipeline::BeltHeightResult result;
+ result.object_height = original_height;
+
+ // The mesh rotation (the sole mesh-side belt transform).
+ const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
+ const double rot_angle = config.belt_slice_rotation_angle.value;
+
+ 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();
+ belt_floor_shear(rot_axis, angle_rad, result.floor_params);
+ // The slicing frame starts at the lowest point of the support region: the
+ // lowest belt-floor point under the footprint, not the lowest vertex. The
+ // belt under the leading end of an overhang lies below every vertex of the
+ // part, and supports have to be able to reach it (see
+ // BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
+ // counterpart of this bbox estimate).
+ double min_rz = std::numeric_limits::max();
+ double max_rz = std::numeric_limits::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());
+ Vec3d rc = R * c;
+ double z = rc.z();
+ min_rz = std::min(min_rz, z);
+ max_rz = std::max(max_rz, z);
+ min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
+ }
+ min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
+ result.object_height = max_rz - min_rz;
+
+ 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 &bbox, double original_height)
+{
+ return compute_belt_height_and_floor_impl(config, bbox, original_height);
+}
+
+bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
+{
+ out = BeltFloorParams{};
+ const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
+ const double rot_angle = config.belt_slice_rotation_angle.value;
+ if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
+ return false;
+ belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
+ return std::abs(out.shear_factor) > EPSILON;
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp
new file mode 100644
index 0000000000..1aae8ba39b
--- /dev/null
+++ b/src/libslic3r/BeltTransform.hpp
@@ -0,0 +1,132 @@
+#pragma once
+
+#include "libslic3r.h"
+#include "Point.hpp"
+#include "BoundingBox.hpp"
+#include "PrintConfig.hpp"
+#include "Geometry.hpp"
+#include "Config.hpp"
+
+#include
+
+namespace Slic3r {
+
+class ModelObject;
+
+// Shared belt-printer transform math.
+//
+// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
+// trafo_out = z_shift * rotation * 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 --------------------------------------------------
+
+ // Whether the G-code axis remap applies at all. The remap fields are only
+ // offered in the belt printer group, so a value left in a profile must not
+ // change a non-belt print: with belt mode off every belt-only key is a no-op.
+ // This is the one place to widen if a non-belt use ever needs them.
+ static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; }
+
+ 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;
+ }
+
+ // ---- Matrix builders --------------------------------------------------
+
+ // 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);
+
+ // Forward transform (the rotation) — 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);
+
+ // ---- Belt floor parameters --------------------------------------------
+
+ struct BeltFloorParams {
+ double shear_factor = 0.0;
+ int from_axis = 1;
+ double z_shift = 0.0;
+ };
+
+ // Shear factor and from-axis of the belt floor in the rotated slicer frame
+ // (z_floor = shear_factor * u, u = the from-axis coordinate), for the
+ // rotation the config selects. z_shift is left at 0. Returns false (and
+ // zero shear) when the config has no tilt.
+ static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
+
+ // How far below the lowest belt-floor point under the footprint the slicing
+ // frame starts, in slicing Z. A support column meeting the belt is wider at
+ // its base than at its tip, so under a leading overhang the base reaches ahead
+ // of the part along the belt, and the layers that trim it to the belt plane
+ // lie below that lowest point: 10 mm along the belt.
+ static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
+
+ // 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 the object's 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 &bbox,
+ double original_height);
+};
+
+} // namespace Slic3r
diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp
index d5707ca0de..23b515647b 100644
--- a/src/libslic3r/Brim.cpp
+++ b/src/libslic3r/Brim.cpp
@@ -474,7 +474,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
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_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
+ // btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading
+ // edge, so it degrades to an ordinary outer brim rather than silently to none.
+ const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || 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;
//ORCA: Select brim base slices from EFC-compensated outline when enabled.
@@ -889,6 +891,17 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
std::vector& printExtruders,
std::map* objectBrimAreasByInstanceOut)
{
+ // Belt printers never use the flat plate brim.
+ //
+ // With a tilted belt the brim has to be laid onto the belt plane over many layers,
+ // which BeltBrim.cpp does during posSupportMaterial. With an untilted belt this
+ // could in principle fall through and produce an ordinary brim, but it would never
+ // reach the G-code: the plate brim is emitted out of skirt_brim_groups(), which
+ // _make_skirt() builds, and that returns early for every belt printer. Running the
+ // generator anyway would just burn time on geometry nobody prints.
+ if (print.config().belt_printer.value)
+ return;
+
std::map brimAreaMap;
Flow flow = print.brim_flow();
ExPolygons islands_area_ex = outer_inner_brim_area(print,
diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp
index 8c53242fe8..9c48363cf8 100644
--- a/src/libslic3r/BuildVolume.hpp
+++ b/src/libslic3r/BuildVolume.hpp
@@ -84,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.
diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index 781fd374d4..c909d7e96a 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -85,6 +85,15 @@ set(lisbslic3r_sources
BoundingBox.hpp
BridgeDetector.cpp
BridgeDetector.hpp
+ BeltBrim.cpp
+ BeltBrim.hpp
+ BeltGCode.cpp
+ BeltGCode.hpp
+ BeltPurge.cpp
+ BeltSliceStrategy.cpp
+ BeltSliceStrategy.hpp
+ BeltTransform.cpp
+ BeltTransform.hpp
Brim.cpp
BrimEarsPoint.hpp
Brim.hpp
@@ -232,6 +241,14 @@ 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/BeltKinematics.cpp
+ GCode/BeltKinematics.hpp
+ GCode/MachineKinematics.cpp
+ GCode/MachineKinematics.hpp
GCode/ConflictChecker.cpp
GCode/ConflictChecker.hpp
GCode/CoolingBuffer.cpp
@@ -453,6 +470,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
diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp
index d695171d1e..5df348281a 100644
--- a/src/libslic3r/ExPolygon.hpp
+++ b/src/libslic3r/ExPolygon.hpp
@@ -403,6 +403,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);
diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp
index e555e6ebe5..61d5c48007 100644
--- a/src/libslic3r/Format/bbs_3mf.cpp
+++ b/src/libslic3r/Format/bbs_3mf.cpp
@@ -1041,10 +1041,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
void _stop_object_xml_parser(const std::string& msg = std::string())
{
assert(! obj_parse_error);
- assert(obj_parse_error_message.empty());
assert(object_xml_parser != nullptr);
obj_parse_error = true;
- obj_parse_error_message = msg;
+ if (! msg.empty() || obj_parse_error_message.empty()) // a handler may have set the message already
+ obj_parse_error_message = msg;
XML_StopParser(object_xml_parser, false);
}
@@ -3901,11 +3901,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
// appends the vertex coordinates
// missing values are set equal to ZERO
- if (m_curr_object)
- m_curr_object->geometry.vertices.emplace_back(
- m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
- m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
- m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
+ if (m_curr_object) {
+ const Vec3f v(m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
+ m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
+ m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
+ // A non-finite coordinate ("nan", "inf") used to be accepted and crashed
+ // qhull in ModelVolume's convex hull while the file was still loading. Refuse the file.
+ if (! v.allFinite()) {
+ _stop_xml_parser("Invalid vertex coordinate: not a finite number");
+ return true; // the parser is stopped; returning false would overwrite the message
+ }
+ m_curr_object->geometry.vertices.emplace_back(v);
+ }
return true;
}
@@ -5195,6 +5202,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}
+ for (const Vec3f &v : sub_object->geometry.vertices)
+ if (! v.allFinite()) { // Qhull cannot take a NaN vertex
+ add_error("invalid (non-finite) vertex in object " + std::to_string(sub_object->id));
+ return false;
+ }
its.vertices.assign(sub_object->geometry.vertices.begin(), sub_object->geometry.vertices.end());
// BBS
@@ -5708,11 +5720,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
// appends the vertex coordinates
// missing values are set equal to ZERO
- if (current_object)
- current_object->geometry.vertices.emplace_back(
- object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
- object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
- object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
+ if (current_object) {
+ const Vec3f v(object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
+ object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
+ object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
+ // See _BBS_3MF_Importer::_handle_start_vertex: a non-finite coordinate
+ // crashed qhull while the file loaded. The dispatcher stops this parser on `false`.
+ if (! v.allFinite()) {
+ obj_parse_error_message = "Invalid vertex coordinate: not a finite number";
+ return false;
+ }
+ current_object->geometry.vertices.emplace_back(v);
+ }
return true;
}
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index 62b5897f39..d29363e639 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -29,10 +29,13 @@
#include "libslic3r.h"
#include "I18N.hpp"
#include "GCode.hpp"
+#include
#include "Exception.hpp"
#include "LifecycleEvents.hpp"
#include "ExtrusionEntity.hpp"
#include "EdgeGrid.hpp"
+#include "BeltTransform.hpp"
+#include "Geometry.hpp"
#include "Geometry/ConvexHull.hpp"
#include "GCode/PrintExtents.hpp"
#include "GCode/Thumbnails.hpp"
@@ -55,6 +58,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -101,6 +105,7 @@
#include
#include "calib.hpp"
#include "libslic3r_version.h"
+#include "GCode/BeltKinematics.hpp"
// Intel redesigned some TBB interface considerably when merging TBB with their oneAPI set of libraries, see GH #7332.
// We are using quite an old TBB 2017 U7. Before we update our build servers, let's use the old API, which is deprecated in up to date TBB.
#if ! defined(TBB_VERSION_MAJOR)
@@ -2193,7 +2198,23 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables()
// Collect pairs of object_layer + support_layer sorted by print_z.
// object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON.
-std::vector GCode::collect_layers_to_print(const PrintObject& object)
+// Belt printers: whether an object layer writes anything, its own extrusions or a belt
+// brim band riding on it. Shared by collect_layers_to_print() (which drops the layers
+// that do not) and the layer count.
+static bool belt_object_layer_prints_something(const PrintObject &object, const Layer &layer)
+{
+ if (layer.has_extrusions())
+ return true;
+ if (object.has_belt_brim()) {
+ const auto &by_layer = object.belt_brim_by_layer();
+ const size_t id = layer.id();
+ if (id < by_layer.size() && ! by_layer[id].empty())
+ return true;
+ }
+ return false;
+}
+
+std::vector GCode::collect_layers_to_print(const PrintObject& object, bool skip_empty_first_layer)
{
std::vector layers_to_print;
layers_to_print.reserve(object.layers().size() + object.support_layers().size());
@@ -2217,10 +2238,19 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
std::vector> warning_ranges;
// Pair the object layers with the support layers by z.
+ //
+ // Belt printers add a third stream: brim apron bands, which sit on the belt AHEAD of
+ // the part and so print below the object's first layer. They are merged here rather
+ // than pushed as standalone records, because a band's print_z can coincide with a
+ // support layer of this same object - and the print-wide merge downstream keeps only
+ // one record per object per z, so a standalone band would be silently overwritten.
size_t idx_object_layer = 0;
size_t idx_support_layer = 0;
+ size_t idx_brim_band = 0;
+ const auto &brim_bands = object.belt_brim_prologue(); // ordered by ascending print_z
const LayerToPrint* last_extrusion_layer = nullptr;
- while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()) {
+ while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()
+ || idx_brim_band < brim_bands.size()) {
LayerToPrint layer_to_print;
double print_z_min = std::numeric_limits::max();
if (idx_object_layer < object.layers().size()) {
@@ -2233,6 +2263,11 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
print_z_min = std::min(print_z_min, layer_to_print.support_layer->print_z);
}
+ if (idx_brim_band < brim_bands.size()) {
+ layer_to_print.belt_brim_band = &brim_bands[idx_brim_band++];
+ print_z_min = std::min(print_z_min, layer_to_print.belt_brim_band->print_z);
+ }
+
if (layer_to_print.object_layer && layer_to_print.object_layer->print_z > print_z_min + EPSILON) {
layer_to_print.object_layer = nullptr;
--idx_object_layer;
@@ -2243,16 +2278,29 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
--idx_support_layer;
}
+ if (layer_to_print.belt_brim_band && layer_to_print.belt_brim_band->print_z > print_z_min + EPSILON) {
+ layer_to_print.belt_brim_band = nullptr;
+ --idx_brim_band;
+ }
+
layer_to_print.original_object = &object;
layers_to_print.push_back(layer_to_print);
bool has_extrusions = (layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions())
- || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions());
+ || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions())
+ || (layer_to_print.belt_brim_band && ! layer_to_print.belt_brim_band->fills.empty());
// Check that there are extrusions on the very first layer. The case with empty
// first layer may result in skirt/brim in the air and maybe other issues.
+ // Skip this check for belt printers. The shear transform tilts the
+ // model so the first horizontal layer plane intersects only a thin
+ // sliver of the model (width ≈ first_layer_height / shear_factor).
+ // This sliver is often narrower than the nozzle diameter, producing
+ // zero perimeters and an empty first layer — which is expected, not
+ // an error. In global shear mode the object may also start above
+ // Z=0 on the tilted belt surface.
if (layers_to_print.size() == 1u) {
- if (!has_extrusions)
+ if (!has_extrusions && !skip_empty_first_layer)
throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id);
}
@@ -2283,14 +2331,32 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
+ std::max(0., extra_gap);
// Negative support_contact_z is not taken into account, it can result in false positives in cases
- if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON)
- warning_ranges.emplace_back(std::make_pair((last_extrusion_layer ? last_extrusion_layer->print_z() : 0.), layers_to_print.back().print_z()));
+ if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON) {
+ // Belt printers: a *leading* empty range (no prior extrusion layer, so the
+ // gap starts at Z=0) is not a floating object — it is just the belt lead-in.
+ // The part rests on the conveyor as it advances, so the first material can
+ // legitimately appear well above Z=0. This empty-layer check assumes a fixed
+ // bed, where material with nothing below it is unprintable; that assumption
+ // does not hold on a belt for the lead-in. Suppress only this leading case,
+ // and keep flagging genuine *internal* gaps (which on a belt may still be an
+ // over-angle overhang that would print into air).
+ const bool belt_leading_gap = object.print()->config().belt_printer.value
+ && last_extrusion_layer == nullptr;
+ if (!belt_leading_gap)
+ warning_ranges.emplace_back(std::make_pair((last_extrusion_layer ? last_extrusion_layer->print_z() : 0.), layers_to_print.back().print_z()));
+ }
}
// Remember last layer with extrusions.
if (has_extrusions)
last_extrusion_layer = &layers_to_print.back();
}
+ // ORCA-Belt: objects print at their position along the belt, so the first
+ // extrusions legitimately start far above Z=0. Drop the spurious
+ // "empty layers from the bed" range while keeping genuine mid-print gaps.
+ if (skip_empty_first_layer && !warning_ranges.empty() && warning_ranges.front().first == 0.)
+ warning_ranges.erase(warning_ranges.begin());
+
if (! warning_ranges.empty()) {
std::string warning;
size_t i = 0;
@@ -2304,6 +2370,22 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
PrintStateBase::WarningLevel::CRITICAL, warning, PrintStateBase::SlicingEmptyGcodeLayers);
}
+ // Belt printers: drop the layers that print nothing at all. An object's slicing
+ // frame starts at the belt below its leading end, so its first layers are empty,
+ // and with several objects along the belt those empty layers fall between other
+ // objects' printing layers. A layer change with no moves is noise in the file, and
+ // the preview (libvgcode) numbers its layers from the moves it sees, so a gap folds
+ // every later layer into the one before it. Both print sequences collect their
+ // layers here, so neither writes such a layer.
+ if (object.print()->config().belt_printer.value)
+ layers_to_print.erase(
+ std::remove_if(layers_to_print.begin(), layers_to_print.end(), [&object](const LayerToPrint <p) {
+ return ! ((ltp.object_layer != nullptr && belt_object_layer_prints_something(object, *ltp.object_layer)) ||
+ (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) ||
+ (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty()));
+ }),
+ layers_to_print.end());
+
return layers_to_print;
}
@@ -2325,11 +2407,14 @@ std::vector>> GCode::collec
for (size_t i = 0; i < print.objects().size(); ++i) {
try {
- per_object[i] = collect_layers_to_print(*print.objects()[i]);
+ per_object[i] = collect_layers_to_print(*print.objects()[i], print.config().belt_printer.value);
} catch (const Slic3r::SlicingError &e) {
errors.push_back(e);
continue;
}
+ // On a belt an object may be left without a layer to print at all.
+ if (per_object[i].empty())
+ continue;
OrderingItem ordering_item;
ordering_item.object_idx = i;
ordering.reserve(ordering.size() + per_object[i].size());
@@ -3055,21 +3140,71 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_fan_mover.release();
m_ordering_cache.clear();
-
+
+ // Belt printer: initialize belt-specific writer via virtual hook.
+ this->init_belt_writer(print);
m_writer.set_is_bbl_machine(is_bbl_printers);
+ // G-code axis remap. Only belt printers get one (see
+ // BeltTransformPipeline::axis_remap_enabled): a remap left in a profile must
+ // not change a non-belt print. Sync the writer's remap state to the current
+ // export UNCONDITIONALLY — even at the identity mapping (0,1,2) — so a reused
+ // writer never retains a stale non-identity mapping from a prior export.
+ // has_axis_remap() returns false at identity, so identity/default output stays
+ // unchanged.
+ {
+ const bool remap = BeltTransformPipeline::axis_remap_enabled(print.config());
+ int rx = remap ? int(print.config().gcode_remap_x.value) : int(RemapAxis::PosX);
+ int ry = remap ? int(print.config().gcode_remap_y.value) : int(RemapAxis::PosY);
+ int rz = remap ? int(print.config().gcode_remap_z.value) : int(RemapAxis::PosZ);
+ m_writer.set_axis_remap(rx, ry, rz);
+ BoundingBoxf bbox_bed(print.config().printable_area.values);
+ m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(),
+ print.config().printable_height.value));
+ }
+
+ // Belt writers only: travel-speed selection becomes per-point (see
+ // GCodeWriter::uses_pointwise_travel_speed()), which must not change for
+ // non-belt printers. The writer gets the same test the extrusions use, so a
+ // travel is judged against the belt surface (belt_height_above_floor) exactly
+ // like the path it leads to. Writer points carry the G-code origin and
+ // extruder offset that point_to_gcode() added; the belt surface is described
+ // in the object's own frame.
+ if (print.config().belt_printer.value) {
+ m_writer.set_first_layer_point_test([this](const Vec3d &point_logical) {
+ const Vec2d extruder_offset = m_writer.filament() != nullptr ? EXTRUDER_CONFIG(extruder_offset) : Vec2d::Zero();
+ // Undo what point_to_gcode() added (m_origin, minus the extruder offset) and
+ // what the writer then took off (its XY offset, the plate origin).
+ const Vec2d plate_offset = m_writer.get_xy_offset().cast();
+ return this->on_first_layer(Vec3d(point_logical.x() - (m_origin.x() - plate_offset.x()) + extruder_offset.x(),
+ point_logical.y() - (m_origin.y() - plate_offset.y()) + extruder_offset.y(),
+ point_logical.z()));
+ });
+ }
+
// How many times will be change_layer() called?
// change_layer() in turn increments the progress bar status.
m_layer_count = 0;
+ // On a belt, collect_layers_to_print() drops the layers that print nothing (an
+ // object's empty lead-in), so they must not be counted here either or the layer
+ // count in the file disagrees with its layer changes.
+ const bool belt = print.config().belt_printer.value;
if (print.config().print_sequence == PrintSequence::ByObject) {
// Add each of the object's layers separately.
for (auto object : print.objects()) {
std::vector zs;
zs.reserve(object->layers().size() + object->support_layers().size());
for (auto layer : object->layers())
- zs.push_back(layer->print_z);
+ if (! belt || belt_object_layer_prints_something(*object, *layer))
+ zs.push_back(layer->print_z);
for (auto layer : object->support_layers())
- zs.push_back(layer->print_z);
+ if (! belt || layer->has_extrusions())
+ zs.push_back(layer->print_z);
+ // Belt brim apron bands each get their own change_layer() call.
+ for (const BeltBrimBand &band : object->belt_brim_prologue())
+ zs.push_back(band.print_z);
+ if (zs.empty())
+ continue;
std::sort(zs.begin(), zs.end());
//BBS: merge numerically very close Z values.
auto end_it = std::unique(zs.begin(), zs.end());
@@ -3086,9 +3221,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
for (auto object : print.objects()) {
zs.reserve(zs.size() + object->layers().size() + object->support_layers().size());
for (auto layer : object->layers())
- zs.push_back(layer->print_z);
+ if (! belt || belt_object_layer_prints_something(*object, *layer))
+ zs.push_back(layer->print_z);
for (auto layer : object->support_layers())
- zs.push_back(layer->print_z);
+ if (! belt || layer->has_extrusions())
+ zs.push_back(layer->print_z);
+ // See the ByObject branch: apron bands are real printed layers.
+ for (const BeltBrimBand &band : object->belt_brim_prologue())
+ zs.push_back(band.print_z);
}
if (!zs.empty())
{
@@ -3721,6 +3861,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
}
}
+ // Belt printer: the tilt and transform settings the G-code viewer reads back. They
+ // are comments outside the config block, so they go after the thumbnails that a
+ // BTT TFT firmware needs first, and are written whether or not that header block is.
+ this->write_belt_header(file, print);
// Write some terse information on the slicing parameters.
const PrintObject *first_object = print.objects().front();
@@ -3934,7 +4078,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
pa_test.set_speed(fast_speed, slow_speed);
pa_test.draw_numbers() = print.calib_params().print_numbers;
+
+ // ORCA-Belt: the PA line test draws directly on the build surface in
+ // logical bed coordinates — on a belt printer that surface is the
+ // belt plane, not the slicing plane.
+ const bool belt_world_coords = print.config().belt_printer.value;
+ if (belt_world_coords)
+ install_belt_kinematics(m_writer, print.config(), /*world_coordinates=*/true);
gcode += pa_test.generate_test(params.start, params.step, std::llround(std::ceil((params.end - params.start) / params.step)) + 1);
+ if (belt_world_coords)
+ install_belt_kinematics(m_writer, print.config(), /*world_coordinates=*/false);
file.write(gcode);
} else {
@@ -3971,6 +4124,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
}
print.throw_if_canceled();
this->set_origin(unscale((*print_object_instance_sequential_active)->shift));
+ this->on_set_origin((*print_object_instance_sequential_active)->print_object,
+ (*print_object_instance_sequential_active)->shift);
// BBS: prime extruder if extruder change happens before this object instance
bool prime_extruder = false;
@@ -4011,12 +4166,15 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Reset the cooling buffer internal state (the current position, feed rate, accelerations).
m_cooling_buffer->set_current_extruder(initial_extruder_id, get_extruder_id(initial_extruder_id));
m_cooling_buffer->reset(this->writer().get_position());
+ // The belt first-layer band is tracked per object as well: if the previous
+ // object ended inside the band, this one has to open its own.
+ m_belt_in_band = false;
// Process all layers of a single object instance (sequential mode) with a parallel pipeline:
// Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser
// and export G-code into file.
tool_ordering.cal_most_used_extruder(print.config());
m_printed_objects.emplace_back(&object);
- this->process_layers(print, tool_ordering, collect_layers_to_print(object), *print_object_instance_sequential_active - object.instances().data(), file,
+ this->process_layers(print, tool_ordering, collect_layers_to_print(object, print.config().belt_printer.value), *print_object_instance_sequential_active - object.instances().data(), file,
prime_extruder);
{
// save the flush statitics stored in tool ordering by object
@@ -5319,8 +5477,43 @@ std::string GCode::generate_object_skirt_group(const Print &print,
object_skirt_tools, layer, extruder_id, m_skirt_group_done[group_idx]);
}
-std::string GCode::generate_object_brim(const Print &print, const PrintObject &object, size_t instance_id, bool first_layer)
+std::string GCode::generate_object_brim(const Print &print, const PrintObject &object, size_t instance_id, bool first_layer,
+ const Layer *object_layer)
{
+ // Belt printers lay the brim onto the tilted belt over many layers, so there is
+ // nothing special about the first one. The bands that coincide with an object
+ // layer are emitted here; those below the object's first layer are apron and go
+ // through process_belt_brim_layer() instead.
+ if (object.has_belt_brim()) {
+ if (object_layer == nullptr)
+ return {};
+ const std::vector &by_layer = object.belt_brim_by_layer();
+ const size_t layer_idx = object_layer->id();
+ if (layer_idx >= by_layer.size() || by_layer[layer_idx].empty())
+ return {};
+ std::string gcode;
+ // The band geometry is in the object's local slicing frame, exactly like its
+ // perimeters, so it needs this instance's origin. The caller does not set it
+ // until later, and the plate brim path deliberately uses (0, 0) because its
+ // geometry is already in plate coordinates.
+ m_config.apply(print.default_region_config());
+ m_config.apply(object.config(), true);
+ // m_layer is not switched to this object until after brim emission, so name
+ // the belt-floor owner explicitly or the classification borrows whichever
+ // object was visited last.
+ BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object };
+ const Point &offset = object.instances()[instance_id].shift;
+ this->set_origin(unscale(offset));
+ this->on_set_origin(&object, offset);
+ m_avoid_crossing_perimeters.use_external_mp();
+ for (const ExtrusionEntity *ee : by_layer[layer_idx].entities)
+ if (ee != nullptr)
+ gcode += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed));
+ m_avoid_crossing_perimeters.use_external_mp(false);
+ m_avoid_crossing_perimeters.disable_once();
+ return gcode;
+ }
+
if (!first_layer)
return {};
@@ -5357,6 +5550,158 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o
return {};
}
+// Belt printers: emit one brim-only apron layer. On a tilted belt the brim ahead
+// of the part lands at slicing Z below the object's first layer, because the
+// object's layer 0 IS its leading contact with the belt. Those layers carry brim
+// and nothing else.
+//
+// This is intentionally a short path rather than a variant of process_layer(): an
+// apron band has no Layer, and giving it a synthetic one would feed a fabricated
+// Layer::id() into initial-layer temperature selection, the spiral vase probe,
+// gradual interpolation and cooling. Correct first-layer treatment comes from
+// the height above the belt, which is evaluated per point.
+LayerResult GCode::process_belt_brim_layer(
+ const Print &print,
+ const std::vector &layers,
+ const LayerTools &layer_tools,
+ const bool last_layer,
+ const size_t single_object_instance_idx)
+{
+ // layer_id 0 is deliberate, not a placeholder. CoolingBuffer reads it for the
+ // initial_layer_fan_speed override and the close_fan_the_first_x_layers gate
+ // (CoolingBuffer.cpp), and every apron band is first-layer material by the only
+ // definition that means anything on a belt: it lies on the belt plane itself. Numbering
+ // the bands 1, 2, 3... would ramp the fan up while still printing on the belt.
+ // spiral_vase_enable false: spiral vase is refused alongside belt brim in
+ // Print::validate(). cooling_buffer_flush true: an apron layer is a complete layer, and
+ // the default (object_layer || raft_layer || last_layer) is false here, so fan and
+ // slowdown would otherwise never be applied to it.
+ LayerResult result { {}, 0, false, true };
+ if (layer_tools.extruders.empty())
+ // Nothing to extrude.
+ return result;
+
+ coordf_t print_z = 0.;
+ coordf_t height = 0.;
+ for (const LayerToPrint <p : layers)
+ if (ltp.belt_brim_band != nullptr) {
+ print_z = ltp.belt_brim_band->print_z;
+ height = ltp.belt_brim_band->height;
+ break;
+ }
+
+ // Apron bands precede object layer 0 and have no layer id of their own; they take the
+ // filament and nozzle assignment in effect at the first object layer.
+ m_cur_layer_idx = 0;
+
+ // Publish the band's Z for _extrude()'s first-layer-plane probe, and make sure
+ // it cannot leak past this layer even if an extrusion throws.
+ struct BeltBrimZGuard {
+ std::optional &slot;
+ ~BeltBrimZGuard() { slot.reset(); }
+ } z_guard { m_belt_brim_z };
+ m_belt_brim_z = print_z;
+ m_layer = nullptr;
+
+ std::string gcode;
+ const unsigned int extruder_id = layer_tools.extruders.front();
+ if (m_writer.filament() == nullptr || m_writer.filament()->id() != extruder_id)
+ gcode += this->set_extruder(extruder_id, print_z);
+
+ // An apron band is a real printed layer: it is counted in m_layer_count, it advances
+ // m_layer_index through change_layer(), and the G-code viewer needs its Z/height tags.
+ // Keep the same caches and hooks the ordinary path maintains, or the first object layer
+ // would compute its height against a stale pre-apron Z and layer-change templates would
+ // skip these layers entirely.
+ {
+ char buf[64];
+ gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n";
+ sprintf(buf, ";Z:%g\n", print_z);
+ gcode += buf;
+ const float band_height = float(height);
+ sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height).c_str(), band_height);
+ gcode += buf;
+ m_last_layer_z = float(print_z);
+ m_max_layer_z = std::max(m_max_layer_z, m_last_layer_z);
+ m_last_height = band_height;
+ }
+
+ if (! m_config.before_layer_change_gcode.value.empty()) {
+ DynamicConfig config;
+ config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index + 1));
+ config.set_key_value("layer_z", new ConfigOptionFloat(print_z));
+ config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z));
+ gcode += this->placeholder_parser_process("before_layer_change_gcode",
+ print.config().before_layer_change_gcode.value, m_writer.filament()->id(), &config) + "\n";
+ }
+
+ gcode += this->change_layer(print_z);
+
+ if (! m_config.layer_change_gcode.value.empty()) {
+ DynamicConfig config;
+ config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
+ config.set_key_value("layer_z", new ConfigOptionFloat(print_z));
+ config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z));
+ gcode += this->placeholder_parser_process("layer_change_gcode",
+ print.config().layer_change_gcode.value, m_writer.filament()->id(), &config) + "\n";
+ }
+
+ // Objects sharing this apron Z may use different brim filaments; print each in its own tool.
+ for (const unsigned int brim_extruder : layer_tools.extruders) {
+ if (m_writer.filament() == nullptr || m_writer.filament()->id() != brim_extruder)
+ gcode += this->set_extruder(brim_extruder, print_z);
+ gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx, brim_extruder);
+ }
+
+ result.gcode = std::move(gcode);
+ return result;
+}
+
+// Emit every apron band carried by this set of layers.
+//
+// Shared by the brim-only branch above and the ordinary process_layer() path. Both need
+// it: an apron band prints below its OWN object's first layer, but on a multi-object belt
+// another object can already be printing at that print_z, in which case the layer has an
+// object layer, takes the ordinary path, and the band would be silently dropped.
+std::string GCode::emit_belt_brim_bands(const Print &print,
+ const std::vector &layers,
+ const size_t single_object_instance_idx,
+ const unsigned int extruder_id)
+{
+ std::string gcode;
+ for (const LayerToPrint <p : layers) {
+ const BeltBrimBand *band = ltp.belt_brim_band;
+ if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr)
+ continue;
+ const PrintObject &object = *ltp.original_object;
+ // belt_brim_filament() is 1-based.
+ if (! object.has_belt_brim() || static_cast(object.belt_brim_filament() - 1) != extruder_id)
+ continue;
+ // Speeds, flow and retraction all read m_config.
+ m_config.apply(print.default_region_config());
+ m_config.apply(object.config(), true);
+ // Apron bands have no Layer at all (m_layer is null here), so the belt
+ // floor owner has to be named the same way the object brim names it.
+ BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object };
+ const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx;
+ const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size()
+ : single_object_instance_idx + 1;
+ for (size_t i = i_begin; i < i_end && i < object.instances().size(); ++ i) {
+ // Band geometry is object-local, like the object's own extrusions.
+ const Point &offset = object.instances()[i].shift;
+ this->set_origin(unscale(offset));
+ this->on_set_origin(&object, offset);
+ m_avoid_crossing_perimeters.use_external_mp();
+ for (const ExtrusionEntity *ee : band->fills.entities)
+ if (ee != nullptr)
+ gcode += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed));
+ m_avoid_crossing_perimeters.use_external_mp(false);
+ m_avoid_crossing_perimeters.disable_once();
+ }
+ }
+ return gcode;
+}
+
// Bedslinger model. The heavier the bed load, the lower the achievable Y acceleration for a given
// drive force (a = F / (bed_mass + printed_mass)). Reads machine_max_force_Y / machine_bed_mass_Y (both
// default 0, i.e. absent on every existing printer), in which case it just returns the min configured Y
@@ -5679,6 +6024,13 @@ LayerResult GCode::process_layer(
}
}
+ // Belt printers: a brim-only apron layer has neither an object nor a support
+ // layer, so it must be handled before layer_ptr is dereferenced below.
+ if (object_layer == nullptr && support_layer == nullptr &&
+ std::any_of(layers.begin(), layers.end(),
+ [](const LayerToPrint &l) { return l.belt_brim_band != nullptr; }))
+ return this->process_belt_brim_layer(print, layers, layer_tools, last_layer, single_object_instance_idx);
+
const Layer* layer_ptr = nullptr;
if (object_layer != nullptr)
layer_ptr = object_layer;
@@ -5842,7 +6194,29 @@ LayerResult GCode::process_layer(
//BBS: set layer time fan speed after layer change gcode
gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n";
+ // Belt printers: ordinary-layer apron bands (a band whose print_z coincides with an
+ // object/support layer, so it takes this path rather than the brim-only branch) are
+ // NOT emitted here anymore. They used to be laid down with whatever tool happened to
+ // be active; instead they are now emitted inside the extruder loop below, in their
+ // own brim-filament pass and before that pass's object extrusion, so the brim goes
+ // down first with the correct tool. See the emit_belt_brim_for_extruder call.
+
//Calibration Layer-specific GCode
+ // ORCA-Belt: on belt printers the calibration object is counter-rotated to
+ // stand upright in slicing space on top of a support wedge, so its first
+ // layer starts above Z=0 (at its position along the belt) with support-only
+ // layers below it. Reference the per-height calibration bands to the bottom
+ // of the object so they keep their designed meaning; on regular printers
+ // the object base is at Z=0 and calib_z == print_z.
+ double calib_z = print_z;
+ if (m_config.belt_printer.value && print.calib_mode() != CalibMode::Calib_None) {
+ // Skip empty ghost layers the grid may produce below the object.
+ for (const Layer* l : layer.object()->layers())
+ if (!l->lslices.empty()) {
+ calib_z = print_z - (l->print_z - l->height);
+ break;
+ }
+ }
switch (print.calib_mode()) {
case CalibMode::Calib_PA_Tower: {
gcode += writer().set_pressure_advance(this->interpolate_value_across_layers(static_cast(print.calib_params().start),
@@ -5851,7 +6225,18 @@ LayerResult GCode::process_layer(
break;
}
case CalibMode::Calib_Temp_Tower: {
- gcode += writer().set_temperature(this->interpolate_value_across_layers(static_cast(print.calib_params().start), static_cast(print.calib_params().end), 5.0f));
+ // ORCA-Belt: the sectioned variant prints each temperature as its
+ // own object in native belt orientation, with the temperature
+ // encoded in the object name ("temp_230") — step per object
+ // instead of ramping per layer band.
+ int sectioned_temp = 0;
+ if (m_config.belt_printer.value &&
+ sscanf(layer.object()->model_object()->name.c_str(), "temp_%d", §ioned_temp) == 1 &&
+ sectioned_temp > 0) {
+ gcode += writer().set_temperature(static_cast(sectioned_temp));
+ } else {
+ gcode += writer().set_temperature(this->interpolate_value_across_layers(static_cast(print.calib_params().start), static_cast(print.calib_params().end), 5.0f));
+ }
break;
}
case CalibMode::Calib_VFA_Tower: {
@@ -5865,16 +6250,16 @@ LayerResult GCode::process_layer(
break;
}
case CalibMode::Calib_Vol_speed_Tower: {
- auto _speed = print.calib_params().start + print_z * print.calib_params().step;
+ auto _speed = print.calib_params().start + std::max(0.0, calib_z) * print.calib_params().step;
m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloatsNullable({std::round(_speed)}));
break;
}
case CalibMode::Calib_Retraction_tower: {
- auto _length = print.calib_params().start + std::floor(std::max(0.0,print_z-0.4)) * print.calib_params().step;
+ auto _length = print.calib_params().start + std::floor(std::max(0.0,calib_z-0.4)) * print.calib_params().step;
DynamicConfig _cfg;
_cfg.set_key_value("retraction_length", new ConfigOptionFloats{_length});
writer().config.apply(_cfg);
- sprintf(buf, "; Calib_Retraction_tower: Z_HEIGHT: %g, length:%g\n", print_z, _length);
+ sprintf(buf, "; Calib_Retraction_tower: Z_HEIGHT: %g, length:%g\n", calib_z, _length);
gcode += buf;
break;
}
@@ -5936,7 +6321,15 @@ LayerResult GCode::process_layer(
}
}
- if (!first_layer && !m_second_layer_things_done) {
+ // Belt printers: defer the temperature/PLR transition until the entire layer
+ // is past the first-layer band above the belt. Elsewhere (non-belt printers,
+ // support-only layers) the legacy `!first_layer` predicate applies, so
+ // behavior is bit-identical to the pre-feature path.
+ bool past_first_layer_band = !first_layer;
+ if (int past = this->belt_layer_past_first_layer_band(object_layer); past >= 0)
+ past_first_layer_band = past > 0;
+
+ if (past_first_layer_band && !m_second_layer_things_done) {
// Orca: set power loss recovery
const auto plr_mode = print.config().enable_power_loss_recovery.value;
gcode += m_writer.enable_power_loss_recovery(plr_mode);
@@ -6329,6 +6722,7 @@ LayerResult GCode::process_layer(
std::vector &objects_by_extruder = objects_by_extruder_it->second;
std::vector &instances = filament_plan.first;
std::vector nodes;
+ std::vector> layout; // Per instance, see IslandOrderCacheEntry
std::vector node_instances;
auto quantize_to_mm = [](const Point &pt) -> Point {
const coord_t grid = coord_t(scale_(1.));
@@ -6353,6 +6747,7 @@ LayerResult GCode::process_layer(
const size_t instance_idx = instances.size();
instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id,
print_object->instances()[instance_id].model_instance->get_labeled_id());
+ layout.emplace_back(islands.size(), ! islands.empty() && ! islands.back().by_region.empty());
const Point &shift = print_object->instances()[instance_id].shift;
const size_t first_node = nodes.size();
if (islands_chainable)
@@ -6372,8 +6767,9 @@ LayerResult GCode::process_layer(
// Reuse the cached tour while this filament's island layout is unchanged.
auto &cache_entry = m_ordering_cache[filament_id];
- if (!(cache_entry.first == nodes)) {
- cache_entry.first = nodes;
+ if (! (cache_entry.nodes == nodes && cache_entry.layout == layout)) {
+ cache_entry.nodes = nodes;
+ cache_entry.layout = layout;
Points node_points;
node_points.reserve(nodes.size());
for (const IslandOrderNode &node : nodes)
@@ -6406,12 +6802,12 @@ LayerResult GCode::process_layer(
// A visit without explicit islands already prints everything.
continue;
std::vector &islands = instances[i].object_by_extruder.islands;
- if (!islands.back().by_region.empty())
+ if (! islands.empty() && ! islands.back().by_region.empty())
last_visit.islands.emplace_back(islands.size() - 1);
}
- cache_entry.second = std::move(visits);
+ cache_entry.visits = std::move(visits);
}
- filament_plan.second = cache_entry.second;
+ filament_plan.second = cache_entry.visits;
}
}
@@ -6471,6 +6867,21 @@ LayerResult GCode::process_layer(
// Extrude the skirt, brim, support, perimeters, infill ordered by the extruders.
m_skirt_group_done.resize(print.skirt_brim_groups().size());
+
+ // Belt brim bookkeeping. A coincident belt_brim_by_layer band must be emitted
+ // exactly once, in its object's brim-filament pass; this records which have gone
+ // down so the in-visit emit and the end-of-layer orphan sweep never double it.
+ // Key = (LayerToPrint index, instance_id).
+ std::set> belt_brim_emitted;
+
+ // Emit every ORDINARY-layer apron band (belt_brim_prologue band coinciding with an
+ // object/support layer) whose brim filament is this pass's extruder, so each band
+ // prints in the correct tool's pass (Finding B). extruder_id is 0-based (the
+ // reindexed tool domain).
+ auto emit_belt_brim_for_extruder = [this, &print, &layers, single_object_instance_idx](unsigned int extruder_id) -> std::string {
+ return this->emit_belt_brim_bands(print, layers, single_object_instance_idx, extruder_id);
+ };
+
for (unsigned int extruder_id : layer_tools.extruders)
{
if (print.config().skirt_type == stCombined && !print.skirt_brim_groups().empty()) {
@@ -6587,6 +6998,16 @@ LayerResult GCode::process_layer(
if (layer_tools.has_wipe_tower && m_wipe_tower)
m_last_processor_extrusion_role = erWipeTower;
+ // Belt printers: now that this pass's tool is selected, lay down any ordinary-layer
+ // apron band whose brim filament is this extruder, before the object extrusion at
+ // this Z (brim goes down first, with the correct tool). Restore the origin so the
+ // object-setup code below is unaffected.
+ if (print.has_belt_brim()) {
+ const Vec2d saved_origin = m_origin;
+ gcode += emit_belt_brim_for_extruder(extruder_id);
+ this->set_origin(saved_origin);
+ }
+
auto &filament_plan = filament_to_print_instances[extruder_id];
std::vector &instances_to_print = filament_plan.first;
const std::vector &instance_visits = filament_plan.second;
@@ -6603,7 +7024,20 @@ LayerResult GCode::process_layer(
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id);
- gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer);
+ const PrintObject &vobj = instance_to_print.print_object;
+ if (vobj.has_belt_brim()) {
+ // Coincident belt brim: emit once, only in this object's brim-filament
+ // pass (extruder_id and belt_brim_filament()-1 are both 0-based here),
+ // and dedup on the LayerToPrint index (not Layer::id()) so the orphan
+ // sweep below never re-emits it.
+ if (extruder_id == (unsigned int)(vobj.belt_brim_filament() - 1) &&
+ belt_brim_emitted.insert({ instance_to_print.layer_id, instance_to_print.instance_id }).second)
+ gcode += generate_object_brim(print, vobj, instance_to_print.instance_id, first_layer,
+ layer_to_print.object_layer);
+ } else {
+ gcode += generate_object_brim(print, vobj, instance_to_print.instance_id, first_layer,
+ layer_to_print.object_layer);
+ }
}
// To control print speed of the 1st object layer printed over raft interface.
@@ -6654,6 +7088,7 @@ LayerResult GCode::process_layer(
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
+ this->on_set_origin(&instance_to_print.print_object, offset);
if (visit.first_visit && instance_to_print.object_by_extruder.support != nullptr) {
m_layer = layers[instance_to_print.layer_id].support_layer;
m_object_layer_over_raft = false;
@@ -6665,6 +7100,7 @@ LayerResult GCode::process_layer(
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
+ this->on_set_origin(&instance_to_print.print_object, offset);
ExtrusionEntityCollection support_eec;
// BBS
@@ -6694,7 +7130,13 @@ LayerResult GCode::process_layer(
// in this instance's frame after set_origin() above). Empty islands are skipped;
// the trailing catch-all island has no centroid to chain by and always goes last.
std::vector &islands = instance_to_print.object_by_extruder.islands;
- std::vector island_order = visit.islands;
+ std::vector island_order;
+ island_order.reserve(visit.islands.size());
+ for (size_t idx : visit.islands) // Never index past the islands (see IslandOrderCacheEntry)
+ if (idx < islands.size())
+ island_order.emplace_back(idx);
+ else
+ BOOST_LOG_TRIVIAL(error) << "island tour refers to island " << idx << " of " << islands.size() << ", skipped";
if (island_order.empty()) {
island_order.reserve(islands.size());
if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) {
@@ -6732,6 +7174,7 @@ LayerResult GCode::process_layer(
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
+ this->on_set_origin(&instance_to_print.print_object, offset);
//FIXME the following code prints regions in the order they are defined, the path is not optimized in any way.
auto has_infill = [](const std::vector &by_region) {
@@ -6871,6 +7314,9 @@ LayerResult GCode::process_layer(
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
+ // Same as the main instance loop: a belt printer rotates the origin through
+ // the belt transform (BeltGCode::on_set_origin).
+ this->on_set_origin(&instance_to_print.print_object, offset);
// --- Build emission plan ---
// Each entry represents one travel_to_z + extrude pass. Per-object mode produces
@@ -7103,6 +7549,37 @@ LayerResult GCode::process_layer(
}
}
+
+ // Belt brim orphan sweep (Finding C). A coincident belt_brim_by_layer band lives on
+ // an object layer, but that layer can yield no InstanceVisit above - a zero-extrusion
+ // lead-in slice with no coinciding support - so the in-visit emit never fired and the
+ // band would be dropped. Emit any such band exactly once here, keyed the same way as
+ // the in-visit emit so already-printed bands are skipped. These orphan layers carry
+ // no object material, so ending on the brim's position is harmless; we still save and
+ // restore m_origin, and only toolchange when the brim filament differs from the active
+ // one - a no-op on single-extruder prints, keeping their output unchanged.
+ if (print.has_belt_brim()) {
+ const Vec2d saved_origin = m_origin;
+ for (const LayerToPrint <p : layers) {
+ const PrintObject *obj = ltp.original_object;
+ if (obj == nullptr || ! obj->has_belt_brim() || ltp.object_layer == nullptr)
+ continue;
+ const size_t ltp_idx = size_t(<p - layers.data());
+ const unsigned int brim0 = (unsigned int)(obj->belt_brim_filament() - 1);
+ const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx;
+ const size_t i_end = single_object_instance_idx == size_t(-1) ? obj->instances().size()
+ : single_object_instance_idx + 1;
+ for (size_t instance_id = i_begin; instance_id < i_end && instance_id < obj->instances().size(); ++ instance_id) {
+ if (! belt_brim_emitted.insert({ ltp_idx, instance_id }).second)
+ continue;
+ if (m_writer.filament() == nullptr || m_writer.filament()->id() != brim0)
+ gcode += this->set_extruder(brim0, print_z);
+ gcode += generate_object_brim(print, *obj, instance_id, first_layer, ltp.object_layer);
+ }
+ }
+ this->set_origin(saved_origin);
+ }
+
if (first_layer) {
for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) {
if (!iter->second.empty())
@@ -7431,8 +7908,13 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_
loop.split_at(last_pos, false);
const auto seam_scarf_type = m_config.seam_slope_type.value;
+ // Belt printers never get a scarf joint. The scarf starts one layer height
+ // below the layer, which on a tilted belt is a step backwards along the belt
+ // axis into the previous layer's wall at the seam (0.28 mm at 45 degrees per
+ // 0.2 mm layer); with an aligned seam that ram repeats at the same spot on
+ // every layer and knocks the part loose.
bool enable_seam_slope = ((seam_scarf_type == SeamScarfType::External && !is_hole) || seam_scarf_type == SeamScarfType::All) &&
- !m_config.spiral_mode &&
+ !m_config.spiral_mode && !m_config.belt_printer.value &&
(loop.role() == erExternalPerimeter || (loop.role() == erPerimeter && m_config.seam_slope_inner_walls)) &&
layer_id() > 0;
const auto nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter);
@@ -8081,6 +8563,21 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
const std::string bridge_description = is_bridge(path.role()) ? path_description + " (bridge)" : std::string();
const std::string &description = bridge_description.empty() ? path_description : bridge_description;
+ // First-layer plane evaluation: compute the path's slicing-frame point
+ // once and reuse for every per-path call site below. When the plane
+ // evaluator is inactive (non-belt printers, or belt printers without
+ // a Z-axis shear) `path_on_first_layer` falls back to the legacy
+ // layer-id check, so behavior is bit-identical to the pre-feature path.
+ // A belt brim apron band has no Layer of its own, so it publishes its Z
+ // through m_belt_brim_z instead; without that the plane would be probed at
+ // Z=0 and the apron mis-classified for fan and speed.
+ const Vec3d path_point_mm{
+ unscale(path.first_point().x()),
+ unscale(path.first_point().y()),
+ m_layer ? m_layer->print_z : (m_belt_brim_z ? *m_belt_brim_z : 0.0)
+ };
+ const bool path_on_first_layer = this->on_first_layer(path_point_mm);
+
const ExtrusionPathSloped* sloped = dynamic_cast(&path);
const auto get_sloped_z = [&sloped, this](double z_ratio) {
@@ -8155,7 +8652,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
const double internal_solid_infill_acceleration = role == erSolidInfill ?
m_config.internal_solid_infill_acceleration.get_at(nozzle).get_abs_value(m_config.default_acceleration.get_at(nozzle)) : 0.;
double acceleration;
- if (this->on_first_layer() && m_config.initial_layer_acceleration.get_at(nozzle) > 0) {
+ if (path_on_first_layer && m_config.initial_layer_acceleration.get_at(nozzle) > 0) {
acceleration = m_config.initial_layer_acceleration.get_at(nozzle);
#if 0
} else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) {
@@ -8181,7 +8678,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
// adjust X Y jerk
if (NOZZLE_CONFIG(default_jerk) > 0) {
- if (this->on_first_layer() && NOZZLE_CONFIG(initial_layer_jerk) > 0) {
+ if (path_on_first_layer && NOZZLE_CONFIG(initial_layer_jerk) > 0) {
jerk = NOZZLE_CONFIG(initial_layer_jerk);
} else if (NOZZLE_CONFIG(outer_wall_jerk) > 0 && is_external_perimeter(path.role())) {
jerk = NOZZLE_CONFIG(outer_wall_jerk);
@@ -8243,7 +8740,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
}
// Additionally, adjust the value if we are on the first layer (except for brims and skirts)
- if (this->on_first_layer() && (path.role() != erBrim && path.role() != erSkirt)) {
+ if (path_on_first_layer && (path.role() != erBrim && path.role() != erSkirt)) {
_mm3_per_mm *= m_config.first_layer_flow_ratio;
}
}
@@ -8310,9 +8807,25 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
if (speed == 0)
speed = filament_max_volumetric_speed / _mm3_per_mm;
-
- const auto _layer = layer_id();
- if (this->on_first_layer() || object_layer_over_raft()) {
+ // Use the belt-aware effective layer index when on a belt printer so
+ // the speed fade tracks perpendicular distance from the plane on
+ // belt printers; otherwise this falls back to the slicing layer id.
+ const int _layer = this->effective_layer_index_for_point(path_point_mm);
+ // Belt printers: a tilted layer runs from the belt to the top of the part, so the
+ // "first layers" the fan stays off for are a band along the belt. Mark where the
+ // extrusion enters and leaves it, per segment, for the cooling buffer.
+ const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value;
+ const int belt_band_layers = belt_band_tags ? m_config.close_fan_the_first_x_layers.get_at(m_writer.filament()->id()) : 0;
+ auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_layers, z = path_point_mm.z()](coord_t x, coord_t y) {
+ if (! belt_band_tags)
+ return;
+ const bool in_band = this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)) < belt_band_layers;
+ if (in_band != m_belt_in_band) {
+ gcode += in_band ? ";_BELT_BAND_START\n" : ";_BELT_BAND_END\n";
+ m_belt_in_band = in_band;
+ }
+ };
+ if (path_on_first_layer || object_layer_over_raft()) {
//BBS: for solid infill of first layer, speed can be higher as long as
//wall lines have be attached
if (path.role() != erBottomSurface) {
@@ -8321,7 +8834,6 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
NOZZLE_CONFIG(initial_layer_infill_speed);
}
} else if (m_config.slow_down_layers > 1 && m_config.raft_layers == 0) {
-
if (_layer > 0 && _layer < m_config.slow_down_layers) {
const auto first_layer_speed =
is_perimeter(path.role())
@@ -8408,7 +8920,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
const bool need_overhang_detection = NOZZLE_CONFIG(enable_overhang_speed) ||
(FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers);
- if (need_overhang_detection && !this->on_first_layer() && !object_layer_over_raft() &&
+ if (need_overhang_detection && !path_on_first_layer && !object_layer_over_raft() &&
(is_bridge(path.role()) || is_perimeter(path.role()))) {
bool is_external = is_external_perimeter(path.role());
double ref_speed = is_external ? NOZZLE_CONFIG(outer_wall_speed) : NOZZLE_CONFIG(inner_wall_speed);
@@ -8768,7 +9280,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
}
// BBS: use G1 if not enable arc fitting or has no arc fitting result or in spiral_mode mode or we are doing sloped extrusion
// Attention: G2 and G3 is not supported in spiral_mode mode
- if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || path.z_contoured) {
+ if (!m_config.enable_arc_fitting || path.polyline.fitting_result.empty() || m_config.spiral_mode || sloped != nullptr || path.z_contoured || this->should_disable_arc_fitting()) {
double path_length = 0.;
double total_length = sloped == nullptr ? 0. : path.polyline.length() * SCALING_FACTOR;
double saved_z = m_writer.get_position().z();
@@ -8788,6 +9300,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
flow_description = description + Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length);
}
}
+ tag_belt_band((line.a.x() + line.b.x()) / 2, (line.a.y() + line.b.y()) / 2);
if (path.z_contoured) {
// ZAA: Z anti-aliased extrusion with variable Z per point
Vec2d dest2d = this->point_to_gcode(line.b.to_point());
@@ -8918,6 +9431,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
const ProcessedPoint &processed_point = new_points[i];
const ProcessedPoint &pre_processed_point = new_points[i-1];
Vec3d p = this->point_to_gcode_quantized(processed_point.p);
+ tag_belt_band((pre_processed_point.p.x() + processed_point.p.x()) / 2, (pre_processed_point.p.y() + processed_point.p.y()) / 2);
if (m_enable_cooling_markers) {
if (enable_overhang_bridge_fan) {
cur_fan_enabled = check_overhang_fan(processed_point.overlap, path.role());
@@ -9077,10 +9591,26 @@ std::string GCode::extrusion_role_to_string_for_parser(const ExtrusionRole & rol
// Step = 0 means gradual interpolation finishing at last value.
float GCode::interpolate_value_across_layers(float start_value, float end_value, float step) const
{
- if (m_layer_index <= 1) {
+ float ratio;
+ // ORCA-Belt: counter-rotated calibration objects stand on a support wedge,
+ // so support-only layers below the object would stretch a layer-index
+ // interpolation. Use the object's own Z span instead, so the value ramps
+ // across the test geometry only.
+ if (m_config.belt_printer.value && m_layer != nullptr && !m_layer->object()->layers().empty()) {
+ const auto& layers = m_layer->object()->layers();
+ // Skip empty ghost layers the grid may produce below the object.
+ double z_min = layers.front()->print_z;
+ for (const Layer* l : layers)
+ if (!l->lslices.empty()) { z_min = l->print_z; break; }
+ const double z_max = layers.back()->print_z;
+ if (m_layer->print_z <= z_min + EPSILON || z_max - z_min <= EPSILON)
+ return start_value;
+ ratio = float(std::min(1.0, (m_layer->print_z - z_min) / (z_max - z_min)));
+ } else if (m_layer_index <= 1) {
return start_value;
+ } else {
+ ratio = m_layer_index / (m_layer_count - 1.f);
}
- const float ratio = m_layer_index / (m_layer_count - 1.f);
if (step > 0.f) {
// Discrete equal-width bands. band is clamped to the last band so the result can't overshoot the range:
// at the top layer ratio * n_bands == n_bands, which would otherwise index one band past the end.
@@ -9193,6 +9723,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string
// multi-hop travel path inside the configuration space
if (m_config.reduce_crossing_wall
&& !m_avoid_crossing_perimeters.disabled_once()
+ && m_layer != nullptr // A brim apron layer has no Layer to avoid crossing
&& m_writer.is_current_position_clear())
//BBS: don't generate detour travel paths when current position is unclea
{
@@ -9217,7 +9748,8 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string
// When "Wipe while retracting" is enabled, then extruder moves to another position, and travel from this position can cross perimeters.
// Because of it, it is necessary to call avoid crossing perimeters again with new starting point after calling retraction()
// FIXME Lukas H.: Try to predict if this second calling of avoid crossing perimeters will be needed or not. It could save computations.
- if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall) {
+ if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall
+ && m_layer != nullptr) { // A brim apron layer has no Layer to avoid crossing
// If in the previous call of m_avoid_crossing_perimeters.travel_to was use_external_mp_once set to true restore this value for next call.
if (used_external_mp_once)
m_avoid_crossing_perimeters.use_external_mp_once();
@@ -10072,6 +10604,10 @@ std::string GCode::set_object_info(Print *print) {
for (PrintInstance& inst : object->instances()) {
inst.unique_id = unique_id++;
inst.id = inst_id++;
+ // Outlines are in plate coordinates. On a belt printer that is the frame after
+ // the slicing rotation has been undone and before the G-code axis remap and
+ // machine-frame shear: where the object stands on the belt, which is what an
+ // object picker shows. Klipper cancels by name, so nothing depends on more.
auto bbox = inst.get_bounding_box();
auto center = print->translate_to_print_space(Vec2d(bbox.center().x(), bbox.center().y()));
const std::string &inst_name = instance_name(inst);
@@ -10094,6 +10630,64 @@ std::string GCode::set_object_info(Print *print) {
return gcode.str();
}
+// Whether an object layer lies entirely past the first-layer band above the belt:
+// 1 when its lowest point is at least one band thickness above the belt, 0 when
+// any of it is inside the band, -1 when the belt surface is not known for this
+// layer (not a belt print, or no object layer), in which case the caller falls
+// back to the slicing layer index.
+int GCode::belt_layer_past_first_layer_band(const Layer *object_layer) const
+{
+ if (object_layer == nullptr)
+ return -1;
+ // The belt surface is linear in the sliced XY, so a bbox's lowest point above
+ // it is at one of its corners.
+ double min_height = std::numeric_limits::max();
+ bool known = false;
+ for (const BoundingBox &bb : object_layer->lslices_bboxes) {
+ const double xs[2] = { unscale(bb.min.x()), unscale(bb.max.x()) };
+ const double ys[2] = { unscale(bb.min.y()), unscale(bb.max.y()) };
+ for (double x : xs)
+ for (double y : ys) {
+ double h;
+ if (! this->belt_height_above_floor(Vec3d(x, y, object_layer->print_z), h))
+ return -1;
+ known = true;
+ min_height = std::min(min_height, h);
+ }
+ }
+ if (! known)
+ return -1;
+ return min_height >= this->first_layer_band_mm() - EPSILON ? 1 : 0;
+}
+
+bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const
+{
+ // The owning object, which is what carries the belt description. During
+ // object-brim and coincident-apron emission m_layer still points at whichever
+ // object was visited last (or at nothing at all), so those paths publish the
+ // owner explicitly -- otherwise a brim's speed would depend on plate order.
+ const PrintObject *object = m_belt_floor_object != nullptr ? m_belt_floor_object
+ : (m_layer != nullptr ? m_layer->object() : nullptr);
+ if (object == nullptr)
+ return false;
+ const SlicingParameters &sp = object->slicing_parameters();
+ // Deliberately NOT BeltFloorContext: its init() folds in
+ // belt_support_floor_offset, a support-generator diagnostic. Letting that
+ // option move the model's first-layer speed band would be a surprising
+ // coupling -- a negative value would switch the slowdown off entirely.
+ // The belt surface itself is just shear * u + z_shift.
+ if (std::abs(sp.belt_floor_shear_factor) < EPSILON)
+ return false;
+ const double u = sp.belt_floor_from_axis == 0 ? point_slicing_mm.x() : point_slicing_mm.y();
+ const double floor_z = sp.belt_floor_shear_factor * u + sp.belt_floor_z_shift;
+ // Measured along the slicing Z, not perpendicular to the belt: layers are
+ // horizontal slabs in the sliced frame, so the slab holding the material that
+ // rests on the belt at this point is the one within one layer height of it.
+ // A perpendicular measure would shrink the band by 1/cos(tilt).
+ height_mm = point_slicing_mm.z() - floor_z;
+ return true;
+}
+
// convert a model-space scaled point into G-code coordinates
Vec2d GCode::point_to_gcode(const Point &point) const
{
diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp
index ca89341f33..da78aa0a35 100644
--- a/src/libslic3r/GCode.hpp
+++ b/src/libslic3r/GCode.hpp
@@ -7,6 +7,7 @@
#include "Print.hpp"
#include "libslic3r.h"
#include "GCodeWriter.hpp"
+#include "GCode/BeltKinematics.hpp"
#include "Layer.hpp"
#include "Point.hpp"
#include "PlaceholderParser.hpp"
@@ -40,13 +41,16 @@
#include
#include