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 #include +#include #include #include #include #include #include +#include #include "BoundingBox.hpp" #include "Polyline.hpp" +#include "BeltBrim.hpp" namespace Slic3r { class ExtrusionEntityCollection; } @@ -249,8 +253,9 @@ public: m_toolchange_count(0), m_nominal_z(0.) {} - ~GCode() = default; + virtual ~GCode() = default; +public: // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); @@ -342,6 +347,13 @@ public: const Layer* object_layer; const SupportLayer* support_layer; const PrintObject* original_object; //BBS: used for shared object logic + // Belt printers only: an apron band that prints BELOW the object's first + // layer, so it has no object or support layer of its own. Deliberately + // not a Layer, so it cannot leak Layer::id() semantics into initial-layer + // temperature, spiral vase, cooling or interpolation logic. When this is + // the only thing set, layer() is null and process_layer() takes its + // dedicated brim-only branch. + const BeltBrimBand* belt_brim_band { nullptr }; const Layer* layer() const { if (object_layer != nullptr) @@ -371,11 +383,17 @@ public: count++; } + // A brim-only apron band contributes no object/support layer, and + // averaging zero terms would yield NaN. Never folded into the + // average, so the non-belt result is bit-identical. + if (count == 0 && belt_brim_band != nullptr) + return belt_brim_band->print_z; + return sum_z / count; } }; -private: +protected: class GCodeOutputStream { public: GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {} @@ -403,9 +421,21 @@ private: FILE *f = nullptr; GCodeProcessor &m_processor; }; + + // Virtual hooks for belt printer subclass (BeltGCode). + // No-ops in base GCode; overridden in BeltGCode. + virtual void init_belt_writer(Print &print) {} + virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} + virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} + // Arc fitting is suppressed whenever the writer's machine mapping cannot + // represent a G2/G3 arc. Belt printers get this through BeltKinematics + // rather than through an override of their own. + virtual bool should_disable_arc_fitting() const + { return ! m_writer.kinematics().supports_arc_moves(); } + void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); - static std::vector collect_layers_to_print(const PrintObject &object); + static std::vector collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false); static std::vector>> collect_layers_to_print(const Print &print); std::string generate_skirt(const Print &print, @@ -425,7 +455,29 @@ private: std::string generate_object_brim(const Print &print, const PrintObject &object, size_t instance_id, - bool first_layer); + bool first_layer, + const Layer *object_layer); + + // Belt printers: emit one brim-only apron layer. These print below the + // object's first layer, so there is no object or support layer for the normal + // process_layer() machinery to work from. Kept to the minimum a layer needs - + // tool, Z move, extrusions - so that nothing here can perturb the + // Layer::id()-based logic the ordinary path relies on. + LayerResult 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); + + // Emit the apron bands carried by these layers whose brim filament is extruder_id + // (0-based). Called from both the brim-only branch and the ordinary path, since a + // band's print_z can coincide with another object's layer on a multi-object belt. + std::string emit_belt_brim_bands( + const Print &print, + const std::vector &layers, + const size_t single_object_instance_idx, + const unsigned int extruder_id); LayerResult process_layer( const Print &print, @@ -629,9 +681,21 @@ private: }; // Cache the per-filament island tour to avoid recomputing while the layer's island layout is - // unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}. - std::map, std::vector>> - m_ordering_cache; + // unchanged. Key: filament_id. Value: the nodes the tour was computed from, the per-instance + // island layout (count and whether the trailing catch-all island has anything to print), and + // the resulting visits. + // The layout is part of the key. Nodes only cover the chainable islands, so two + // layers with the same centroids but a different number of islands (thin walls, negative + // volumes come and go) matched the cache and the visit's catch-all index -- islands.size() - 1 + // of the OLD layer -- ran past the new layer's islands (found by fuzzing: segfault in + // extrude_perimeters on multi-part objects). + struct IslandOrderCacheEntry + { + std::vector nodes; + std::vector> layout; + std::vector visits; + }; + std::map m_ordering_cache; ExtrusionQualityEstimator m_extrusion_quality_estimator; @@ -768,7 +832,6 @@ private: std::unique_ptr m_cooling_buffer; std::unique_ptr m_spiral_vase; - std::unique_ptr m_pressure_equalizer; std::unique_ptr m_pa_processor; @@ -823,6 +886,25 @@ private: mutable ConfigIndexCache m_filament_index_cache; mutable ConfigIndexCache m_nozzle_index_cache; + // Belt brim apron layers only. They have no Layer, so the print_z that + // _extrude() needs for the first-layer-plane probe is published here instead. + // Scoped by BeltBrimZGuard in process_belt_brim_layer(), never left set. + std::optional m_belt_brim_z; + // Belt brim only. Brim and coincident apron bands are emitted before m_layer + // is switched to their object, so belt_height_above_floor() would otherwise + // read the previously visited object's belt description -- making a brim's + // classification depend on plate visiting order. Those paths publish the + // owner here for the duration of the emission. Never left set. + const PrintObject *m_belt_floor_object{nullptr}; + struct BeltFloorObjectGuard { + const PrintObject *&slot; + BeltFloorObjectGuard(const PrintObject *&s, const PrintObject *o) : slot(s) { slot = o; } + ~BeltFloorObjectGuard() { slot = nullptr; } + }; + + // The last extrusion segment was inside the belt's first-layer fan band (see _extrude()). + bool m_belt_in_band{false}; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS @@ -840,6 +922,46 @@ private: // On the first printing layer. This flag triggers first layer speeds. //BBS bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0 && abs(m_layer->bottom_z()) < EPSILON; } + // Per-point first-layer test. On a belt printer the result depends on the + // supplied slicing-frame point (its height above the belt); otherwise we + // delegate to the legacy per-layer test. This is the entry point used by + // per-path call sites in _extrude. + bool on_first_layer(const Vec3d &point_slicing_mm) const { + double h; + if (this->belt_height_above_floor(point_slicing_mm, h)) + return h <= m_config.initial_layer_print_height.value + EPSILON; + return on_first_layer(); + } + // "Effective layer index" used to drive layer-count thresholds like + // slow_down_layers. On a belt printer this is the height above the belt in + // first_layer_band_mm() units; otherwise it is the legacy slicing layer index. + int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const { + double h; + if (this->belt_height_above_floor(point_slicing_mm, h)) { + const double lh = this->first_layer_band_mm(); + return h <= 0. ? 0 : int(std::floor(h / lh)); + } + return on_first_layer() ? 0 : layer_id(); + } + + // Band thickness for the *effective layer index*: one first layer height, so + // "the first N layers" means the same height above the belt as on a flat bed. + double first_layer_band_mm() const { + const double band = m_config.initial_layer_print_height.value; + return band > 0. ? band : 0.2; + } + + // Height of a slicing-frame point above the belt surface, or false when this + // is not a belt print. + // + // The belt surface is known exactly in the slicing frame from the slicing + // parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same + // description the support generator uses, independent of every remap and + // back-transform. + bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const; + // 1 / 0 / -1: the object layer is entirely past the first-layer band above the + // belt / reaches into it / the belt surface is not known for it. + int belt_layer_past_first_layer_band(const Layer *object_layer) const; int layer_id() const { if (m_layer == nullptr) return -1; diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp new file mode 100644 index 0000000000..3d2cabc718 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -0,0 +1,33 @@ +#include "BeltBackTransform.hpp" +#include "../BeltTransform.hpp" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +bool BeltBackTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_inverse = Transform3d::Identity(); + + if (!config.belt_printer.value) + return false; + + // Build the forward pipeline (the rotation) and store its inverse. + Transform3d forward = BeltTransformPipeline::build_forward_transform(config); + if (forward.isApprox(Transform3d::Identity())) + return false; + + m_inverse = forward.inverse(); + m_active = true; + return true; +} + +Vec3d BeltBackTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_inverse * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp new file mode 100644 index 0000000000..236ad84f51 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -0,0 +1,38 @@ +#ifndef slic3r_BeltBackTransform_hpp_ +#define slic3r_BeltBackTransform_hpp_ + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +// Reverses the pre-slice rotation that PrintObjectSlice.cpp applies to belt +// printer geometry, converting G-code coordinates from the sliced (rotated) +// frame back to the machine's real coordinate space. +// +// Initialized once from PrintConfig, then applied per-point in +// BeltKinematics::to_machine() before axis remapping. +// +// Active on belt printers with a non-identity pre-slice rotation. +class BeltBackTransform +{ +public: + BeltBackTransform() = default; + + // Initialize from belt printer config. Rebuilds the same pre-slice rotation + // as PrintObjectSlice.cpp and precomputes the affine inverse. Returns true if a non-identity back-transform was computed. + bool init_from_config(const PrintConfig &config); + + // Apply the inverse transform to a point. Returns pos unchanged if + // no back-transform is active. + Vec3d apply(const Vec3d &pos) const; + +private: + bool m_active = false; + Transform3d m_inverse = Transform3d::Identity(); +}; + +} // namespace Slic3r + +#endif // slic3r_BeltBackTransform_hpp_ diff --git a/src/libslic3r/GCode/BeltKinematics.cpp b/src/libslic3r/GCode/BeltKinematics.cpp new file mode 100644 index 0000000000..27d5dad105 --- /dev/null +++ b/src/libslic3r/GCode/BeltKinematics.cpp @@ -0,0 +1,29 @@ +#include "BeltKinematics.hpp" +#include "../BeltTransform.hpp" +#include "../PrintConfig.hpp" +#include "../GCodeWriter.hpp" +#include "../Point.hpp" +#include + +namespace Slic3r { + +BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates) + : m_world_coordinates(world_coordinates) +{ + m_back_transform.init_from_config(config); + m_machine_frame.init_from_config(config); +} + +Vec3d BeltKinematics::to_machine(const Vec3d &p) const +{ + const Vec3d after_back = m_world_coordinates ? p : m_back_transform.apply(p); + const Vec3d after_remap = this->apply_axis_remap(after_back); + return m_machine_frame.apply(after_remap); +} + +void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates) +{ + writer.set_kinematics(std::make_unique(config, world_coordinates)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp new file mode 100644 index 0000000000..7dc08635ab --- /dev/null +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -0,0 +1,59 @@ +#ifndef slic3r_BeltKinematics_hpp_ +#define slic3r_BeltKinematics_hpp_ + +#include "MachineKinematics.hpp" +#include "BeltBackTransform.hpp" +#include "MachineFrameTransform.hpp" +#include "../Point.hpp" + +namespace Slic3r { + +class PrintConfig; +class GCodeWriter; + +// Belt-printer machine frame. +// +// Forward order, as applied per emitted point: +// machine = MachineFrameTransform( axis_remap( BeltBackTransform( logical ) ) ) +// +// i.e. the slicer->world back-transform runs FIRST and the machine-frame +// shear/scale LAST, so the latter acts as a global linear transform on the +// already-placed coordinates. +// +// world_coordinates mode (the PA line / PA pattern calibration generators) +// treats the incoming point as already relative to the belt surface -- X across, +// Y along the belt, Z above it -- and therefore skips the back-transform while +// keeping the remap and the machine frame. It is a different coordinate map, not +// a writer mode, which is why it is fixed at construction. +class BeltKinematics : public CartesianKinematics +{ +public: + explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false); + + Vec3d to_machine(const Vec3d &p) const override; + + // A belt writer has always emitted full XYZ on every move, whether or not any + // individual stage reports itself active. Making this conditional would change + // emitted G-code for an identity-transform belt configuration. + bool must_emit_all_axes() const override { return true; } + bool suppress_lift_at_unknown_position() const override { return true; } + // The machine frame shears and scales, so a circle is an ellipse in machine + // coordinates and G2/G3 cannot describe it. + bool supports_arc_moves() const override { return false; } + +private: + BeltBackTransform m_back_transform; + MachineFrameTransform m_machine_frame; + bool m_world_coordinates { false }; +}; + +// Install a belt machine frame on any GCodeWriter. Any axis remap and build +// volume already configured on the writer are carried over, so this may be +// called before or after those setters. Re-calling it with a different +// world_coordinates value swaps the map (used around the PA line generator). +void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, + bool world_coordinates = false); + +} // namespace Slic3r + +#endif // slic3r_BeltKinematics_hpp_ diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 9fe7bbbd20..03ef4b3441 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -46,10 +47,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_g m_num_extruders = std::max(ex.id() + 1, m_num_extruders); m_extruder_ids.emplace_back(ex.id()); } + } void CoolingBuffer::reset(const Vec3d &position) { + m_belt_band_active = false; // BBS: add I and J axis to store center of arc m_current_pos.assign(7, 0.f); m_current_pos[0] = float(position.x()); @@ -89,6 +92,9 @@ struct CoolingLine // ORCA: Add support for ironing fan speed control TYPE_IRONING_FAN_START = 1 << 19, TYPE_IRONING_FAN_END = 1 << 20, + // Belt printers: extrusions within the first-layer band above the belt. + TYPE_BELT_BAND_START = 1 << 21, + TYPE_BELT_BAND_END = 1 << 22, }; CoolingLine(unsigned int type, size_t line_start, size_t line_end) : @@ -549,6 +555,10 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: line.type = CoolingLine::TYPE_IRONING_FAN_START; } else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control line.type = CoolingLine::TYPE_IRONING_FAN_END; + } else if (boost::starts_with(sline, ";_BELT_BAND_START")) { + line.type = CoolingLine::TYPE_BELT_BAND_START; + } else if (boost::starts_with(sline, ";_BELT_BAND_END")) { + line.type = CoolingLine::TYPE_BELT_BAND_END; } else if (boost::starts_with(sline, "G4 ")) { // Parse the wait time. line.type = CoolingLine::TYPE_G4; @@ -891,7 +901,9 @@ std::string CoolingBuffer::apply_layer_cooldown( {CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false}, {CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control {CoolingLine::TYPE_FORCE_RESUME_FAN, false}}; - bool need_set_fan = false; + // Belt printers: a band still open from the previous layer has to take the fan back from + // the layer-level speed issued just above. + bool need_set_fan = m_belt_band_active; for (const CoolingLine *line : lines) { const char *line_start = gcode.c_str() + line->line_start; @@ -905,6 +917,8 @@ std::string CoolingBuffer::apply_layer_cooldown( if (new_extruder != m_current_extruder) { m_current_extruder = new_extruder; change_extruder_set_fan(true); + if (m_belt_band_active) + need_set_fan = true; } } new_gcode.append(line_start, line_end - line_start); @@ -957,6 +971,13 @@ std::string CoolingBuffer::apply_layer_cooldown( if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value) new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed); } + else if (line->type & CoolingLine::TYPE_BELT_BAND_START) { + m_belt_band_active = true; + need_set_fan = true; + } else if (line->type & CoolingLine::TYPE_BELT_BAND_END) { + m_belt_band_active = false; + need_set_fan = true; + } else if (line->type & CoolingLine::TYPE_EXTRUDE_END) { // Just remove this comment. } else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) { @@ -1049,7 +1070,15 @@ std::string CoolingBuffer::apply_layer_cooldown( m_current_fan_speed = speed; } }; - if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ + if (m_belt_band_active) { + // Belt printers: a tilted layer runs from the belt to the top of the part, so + // "the first layers" are a band along the belt rather than the first slicing + // layers. Extrusions GCode::_extrude() marks as inside that band print with the + // fan off, whatever overhang, bridge or resume request is pending, as the first + // layers of a flat bed do. Leaving the band falls through to the branches below. + set_fan(0); + fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false; + } else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ set_fan(overhang_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control set_fan(internal_bridge_fan_speed); diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index c2323dfc01..7c54d2f5ec 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -21,7 +21,7 @@ struct PerExtruderAdjustments; // // The simple it sounds, the actual implementation is significantly more complex. // Namely, for a multi-extruder print, each material may require a different cooling logic. -// For example, some materials may not like to print too slowly, while with some materials +// For example, some materials may not like to print too slowly, while with some materials // we may slow down significantly. // class CoolingBuffer { @@ -63,6 +63,9 @@ private: unsigned int m_current_nozzle; //BBS: current fan speed int m_current_fan_speed; + // Belt printers: the extrusion being processed lies in the first-layer band above the + // belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers. + bool m_belt_band_active = false; }; } diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 17f55905f9..ac5a435bcd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2616,6 +2616,9 @@ void GCodeProcessorResult::reset() { long_retraction_when_cut = false; timelapse_warning_code = 0; printable_height = 0.0f; + machine_frame_transform_active = false; + belt_tilt_angle = 0.f; + belt_z_origin = 0.f; settings_ids.reset(); filaments_count = 0; backtrace_enabled = false; @@ -2852,6 +2855,32 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int return ps; }; + // Belt-printer post-gcode shear/scale/post_remap is applied as the final + // step of BeltKinematics::to_machine, so MoveVertex.position is + // in the printer's machine frame. Undo it here so the XY area check + // operates in the build-volume frame that printable_area is defined in + // (the height checks below are skipped on belt printers). For non-belt printers + // (is_active() == false) apply_inverse is identity and behaviour is + // unchanged from before. + const bool machine_frame_active = m_machine_frame_transform.is_active(); + auto compare_pos = [&](const GCodeProcessorResult::MoveVertex &move) -> Vec3d { + Vec3d pos = move.position.cast(); + if (!machine_frame_active) + return pos; + Vec3d extruder_off = Vec3d::Zero(); + if (size_t(move.extruder_id) < m_extruder_offsets.size()) + extruder_off = m_extruder_offsets[move.extruder_id].cast(); + // Strip plate + extruder offsets to recover the raw machine-frame + // coordinate that was emitted into the G-code (see store_move_vertex). + Vec3d machine(pos.x() - m_x_offset - extruder_off.x(), + pos.y() - m_y_offset - extruder_off.y(), + pos.z() - extruder_off.z() + m_z_offset); + Vec3d build = m_machine_frame_transform.apply_inverse(machine); + // Re-apply plate offset so the result matches plate_printable_poly, + // which is translated by plate_offset below. + return Vec3d(build.x() + m_x_offset, build.y() + m_y_offset, build.z()); + }; + struct GCodePosInfo { Points pos; @@ -2863,26 +2892,20 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) { // sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) { + const Vec3d cp = compare_pos(move); + // For belt printers we read Z from the inverse-transformed position + // (post-origin-snap, pre-machine-frame). Otherwise keep the + // original print_z source (the slicer's layer-Z comment) so + // non-belt behaviour is bit-for-bit unchanged. + const float z_for_height = machine_frame_active ? float(cp.z()) : move.print_z; if (move.extrusion_role == ExtrusionRole::erCustom) { - /*if (move.is_arc_move_with_interpolation_points()) { - for (int i = 0; i < move.interpolation_points.size(); i++) { - gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.interpolation_points[i].cast())); - } - } else {*/ - gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.position.cast())); - //} + gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(cp)); gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom = - std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, move.print_z); + std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, z_for_height); } else { - /*if (move.is_arc_move_with_interpolation_points()) { - for (int i = 0; i < move.interpolation_points.size(); i++) { - gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.interpolation_points[i].cast())); - } - } else {*/ - gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.position.cast())); - //} + gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(cp)); gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z = std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z, - move.print_z); + z_for_height); } } } @@ -2917,7 +2940,12 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int valid = false; } } - if ( iter->second.max_print_z > plate_printable_height ) { //over height + // Belt printers: the Z recorded here grows with belt travel (machine Z with the + // frame transform, the slicing-frame Z without it), while printable_height is the + // clearance above the belt; the two are not comparable, so the over-height check + // is skipped, as the preview's ToolHeightOutside warning already is. + // Print::validate() checks the object's height against the clearance. + if ( !m_belt_printer && iter->second.max_print_z > plate_printable_height ) { //over height m_result.gcode_check_result.error_code |= (1 << 3); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; @@ -2958,7 +2986,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int } // check printable height - if ((extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { + if (!m_belt_printer && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { m_result.gcode_check_result.error_code |= (1 << 1); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; @@ -3124,6 +3152,13 @@ void GCodeProcessor::apply_config(const PrintConfig& config) m_result.printable_height = config.printable_height; + // Belt printer: cache the post-gcode machine-frame transform so the + // multi-extruder validator can undo it and compare against build-volume + // bounds rather than machine-frame positions. + m_machine_frame_transform.init_from_config(config); + m_result.machine_frame_transform_active = m_machine_frame_transform.is_active(); + m_belt_printer = config.belt_printer.value; + auto filament_maps = config.option("filament_map"); if (filament_maps != nullptr) { m_filament_maps = filament_maps->values; @@ -3154,6 +3189,32 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) { m_parser.apply_config(config); + // Belt printer: remember the file's belt keys for export_config_for_render(). The + // config block lists belt_printer for every printer, so a non-belt file loaded while + // a belt printer is selected switches the preview's belt view off, and a belt file + // loaded on another printer brings its own tilt, remaps and bed along. + m_belt_render_config.clear(); + { + const auto *belt = config.option("belt_printer"); + if (belt != nullptr) { + static const char *belt_keys[] = { + "belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", + "belt_frame_tilt_decouple", "belt_frame_tilt_angle", + }; + for (const char *key : belt_keys) + if (const ConfigOption *opt = config.option(key); opt != nullptr) + m_belt_render_config.set_key_value(key, opt->clone()); + // The Rev remaps mirror inside the build volume, so the designed view needs + // the bed the file was sliced for. Only a belt file may override it. + static const char *bed_keys[] = { "printable_area", "printable_height" }; + if (belt->value) + for (const char *key : bed_keys) + if (const ConfigOption *opt = config.option(key); opt != nullptr) + m_belt_render_config.set_key_value(key, opt->clone()); + } + } + //BBS const ConfigOptionFloatsNullable* nozzle_volume = config.option("nozzle_volume"); if (nozzle_volume != nullptr) { @@ -3637,6 +3698,7 @@ void GCodeProcessor::reset() m_zero_layer_height = 0.0f; m_first_layer_height = 0.0f; m_processing_start_custom_gcode = false; + m_in_config_block = false; m_g1_line_id = 0; m_layer_id = 0; m_cp_color.reset(); @@ -3677,6 +3739,7 @@ DynamicConfig GCodeProcessor::export_config_for_render() const config.set_key_value("filament_is_support", new ConfigOptionBools(m_parser.get_config().filament_is_support.values)); config.set_key_value("filament_type", new ConfigOptionStrings(m_parser.get_config().filament_type.values)); config.set_key_value("filament_map", new ConfigOptionInts(m_parser.get_config().filament_map.values)); + config.apply(m_belt_render_config); return config; } @@ -4242,6 +4305,34 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + // ;Z: -- the layer Z tag non-BBL printers write. Only read on a belt printer, + // where the preview labels its layers with it (GCodeViewer::load_as_gcode); + // elsewhere print_z stays unset, as it always was, so nothing downstream of + // it changes for other printers. + if (m_belt_printer && boost::starts_with(comment, "Z:")) { + m_print_z = get_z_height(comment); + return; + } + + if (boost::starts_with(comment, " CONFIG_BLOCK_START")) { + m_in_config_block = true; + return; + } + if (boost::starts_with(comment, " CONFIG_BLOCK_END")) { + m_in_config_block = false; + return; + } + + // Belt printer: derive the physical tilt magnitude from the slicing-rotation + // angle header comment (used to enable the preview's belt view). Only the belt + // header carries it outside the config block; the config block lists the key + // for every printer, belt or not. + if (!m_in_config_block && boost::starts_with(comment, " belt_slice_rotation_angle = ")) { + try { + m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29)))); + } catch (...) {} + return; + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; @@ -6138,6 +6229,13 @@ void GCodeProcessor::process_G92(const GCodeReader::GCodeLine& line) if (line.has_z()) { m_origin[Z] = m_end_position[Z] - line.z() * lengths_scale_factor; any_found = true; + // Belt only: the start G-code's purge-blob advance + G92 Z0 resets leave a constant + // machine-Z origin offset here; the designed-view back-transform subtracts it so + // toolpaths map to the model's belt coordinate (gcode Z). Gated on belt_tilt_angle + // (set from the belt header, parsed before the body) so non-belt G-code processing + // is byte-identical — no unconditional work on the shared path. + if (m_result.belt_tilt_angle != 0.f) + m_result.belt_z_origin = m_origin[Z]; } if (line.has_e()) { @@ -7116,6 +7214,22 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, m_result.print_statistics.total_travel_distance += m_travel_dist; } + // During the start G-code "prepare" stage the toolhead Z is not yet a real + // print height on a normal printer, so it is pinned to the first-layer height + // to keep the preview tidy. Belt printers are the exception: there the Z is + // written explicitly by the belt kinematics and the designed-view back-transform + // couples machine Z into the rendered model Y (the belt tilt mixes the height + // and belt-feed axes). Overriding Z therefore back-transforms the last + // prepare-stage move (the unretract before the first extrusion) to model + // Y ~= 0, and the libvgcode path builder then draws a phantom extrusion + // segment from Y ~= 0 to the first real toolpath. Keep the real Z for belt + // printers so prepare-stage moves map correctly. Gated on belt_tilt_angle (set + // from the G-code header before the body is processed) so non-belt processing + // is byte-identical. + const float store_z = (m_processing_start_custom_gcode && m_result.belt_tilt_angle == 0.f) + ? m_first_layer_height + : m_end_position[Z] - m_z_offset; + m_result.moves.push_back({ m_last_line_id, type, @@ -7123,7 +7237,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, static_cast(filament_id), m_cp_color.current, //BBS: add plate's offset to the rendering vertices - Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, m_processing_start_custom_gcode ? m_first_layer_height : m_end_position[Z]- m_z_offset) + m_extruder_offsets[filament_id], + Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, store_z) + m_extruder_offsets[filament_id], static_cast(m_end_position[E] - m_start_position[E]), m_feedrate, 0.0f, // actual feedrate diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 919315d00c..9b3571223c 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -12,6 +12,7 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/CustomGCode.hpp" #include "libslic3r/MultiNozzleUtils.hpp" +#include "libslic3r/GCode/MachineFrameTransform.hpp" #include #include @@ -290,6 +291,19 @@ class Print; bool support_traditional_timelapse{true}; float printable_height; float z_offset; + // Belt printer: physical tilt magnitude (deg) parsed from the slicing-rotation + // header comment; used to enable the preview's belt view. + float belt_tilt_angle{ 0.f }; + // Belt printer: machine-Z origin offset (mm) left in m_origin[Z] by the start + // G-code (purge-blob belt advance + G92 Z0 resets). Move positions are stored + // as gcode_Z + this offset, so the designed-view back-transform must subtract it + // to recover the model's belt coordinate. + float belt_z_origin{ 0.f }; + // Belt printer: post-gcode shear/scale/post_remap is configured and + // non-identity. When set, the layer Z values in `moves` are in the + // machine frame and should not be compared against `printable_height` + // (which lives in the build-volume frame). + bool machine_frame_transform_active{ false }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -385,6 +399,9 @@ class Print; // Keep the SKIPPABLE per-type time on a copied result. skippable_part_time = std::forward(other).skippable_part_time; initial_layer_time = std::forward(other).initial_layer_time; + belt_tilt_angle = std::forward(other).belt_tilt_angle; + belt_z_origin = std::forward(other).belt_z_origin; + machine_frame_transform_active = std::forward(other).machine_frame_transform_active; #if ENABLE_GCODE_VIEWER_STATISTICS time = std::forward(other).time; #endif @@ -1085,6 +1102,10 @@ class Print; private: CommandProcessor m_command_processor; GCodeReader m_parser; + // Belt printer: the belt keys of the loaded file's config block (plus the bed they + // are relative to), handed to the preview through export_config_for_render() so the + // belt view and its back-transform follow the file, not the selected printer. + DynamicConfig m_belt_render_config; EUnits m_units; EPositioningType m_global_positioning_type; EPositioningType m_e_local_positioning_type; @@ -1160,6 +1181,13 @@ class Print; double m_x_offset{ 0 }; double m_y_offset{ 0 }; + // Belt-printer post-gcode shear/scale/post_remap. Used by + // check_multi_extruder_gcode_valid to undo the machine-frame + // transform on move positions so bounds checks operate in the + // pre-machine-frame (build-volume) frame. + MachineFrameTransform m_machine_frame_transform; + bool m_belt_printer{ false }; + unsigned int m_line_id; unsigned int m_last_line_id; float m_feedrate; // mm/s @@ -1189,6 +1217,7 @@ class Print; float m_first_layer_height; // mm float m_zero_layer_height; // mm bool m_processing_start_custom_gcode; + bool m_in_config_block; unsigned int m_g1_line_id; unsigned int m_layer_id; CpColor m_cp_color; diff --git a/src/libslic3r/GCode/MachineFrameTransform.cpp b/src/libslic3r/GCode/MachineFrameTransform.cpp new file mode 100644 index 0000000000..9159dbca67 --- /dev/null +++ b/src/libslic3r/GCode/MachineFrameTransform.cpp @@ -0,0 +1,89 @@ +#include "MachineFrameTransform.hpp" +#include "../Geometry.hpp" +#include "../Point.hpp" +#include "../PrintConfig.hpp" +#include "../libslic3r.h" + +#include + +namespace Slic3r { + +bool MachineFrameTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_transform = Transform3d::Identity(); + m_transform_inverse = Transform3d::Identity(); + + if (!config.belt_printer.value) + return false; + + // The machine-frame transform is derived from the single belt tilt (axis + + // angle) that also drives the pre-slice mesh rotation. Expert decouple lets + // the machine-frame angle differ from the slicing rotation; otherwise both + // use belt_slice_rotation_angle. + const BeltRotationAxis axis = config.belt_slice_rotation.value; + if (axis == BeltRotationAxis::None || axis == BeltRotationAxis::Z) + return false; // Z is an in-plane spin: no machine-frame tilt. + + const double angle_deg = config.belt_frame_tilt_decouple.value + ? config.belt_frame_tilt_angle.value + : config.belt_slice_rotation_angle.value; + if (std::abs(angle_deg) <= EPSILON) + return false; + + const double angle_rad = Geometry::deg2rad(angle_deg); + const double sin_a = std::sin(angle_rad); + if (std::abs(sin_a) <= EPSILON) + return false; + const double cot_a = std::cos(angle_rad) / sin_a; + const double inv_sin = 1.0 / std::abs(sin_a); + + // This stage runs after the conventional belt axis swap. For an X-axis + // slicing rotation, remapped Y is model height and remapped Z is travel + // along the belt. Convert those Cartesian coordinates to machine axes with + // the established belt-printer convention: + // machine gantry = model height / sin(a) + // machine belt = model belt + model height * cot(a) + // The Y-rotation case is the same mapping on X/Z, with the rotation sign. + // At 45 degrees tan/cot and sin/cos are equal, which previously hid the + // incorrect complementary-angle formulas used by this unified transform. + Matrix3d shear = Matrix3d::Identity(); + Matrix3d scale = Matrix3d::Identity(); + if (axis == BeltRotationAxis::X) { + shear(2, 1) = cot_a; // Z from Y + scale(1, 1) = inv_sin; // Y + } else { // BeltRotationAxis::Y + shear(2, 0) = -cot_a; // Z from X + scale(0, 0) = inv_sin; // X + } + + // Apply shear first, then scale (the historical default ShearThenScale order: + // result = scale * shear * p). For the canonical 45°/X belt this maps + // (x,y,z) -> (x, y/sin, y + z), matching the previous per-axis config. + Transform3d combined = Transform3d::Identity(); + combined.linear() = scale * shear; + + if (combined.isApprox(Transform3d::Identity())) + return false; + + m_transform = combined; + m_transform_inverse = combined.inverse(); + m_active = true; + return true; +} + +Vec3d MachineFrameTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_transform * pos; +} + +Vec3d MachineFrameTransform::apply_inverse(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_transform_inverse * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp new file mode 100644 index 0000000000..cdae28ca1a --- /dev/null +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -0,0 +1,54 @@ +#ifndef slic3r_MachineFrameTransform_hpp_ +#define slic3r_MachineFrameTransform_hpp_ + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +// Post-stage machine-frame transform for belt printers. +// +// Applied in BeltKinematics::to_machine AFTER the back-transform and +// the gcode_remap_* axis remap. Maps Cartesian (axis-permuted) G-code +// coordinates into the printer's physical machine frame. +// +// Derived entirely from the single belt tilt (belt_slice_rotation axis + +// belt_slice_rotation_angle): a shear coupling the height axis to the belt-feed +// axis (factor cot a) plus a 1/sin a scale on the gantry-height axis. The expert +// belt_frame_tilt_decouple flag lets the machine-frame angle differ from the +// pre-slice rotation angle via belt_frame_tilt_angle. +class MachineFrameTransform +{ +public: + MachineFrameTransform() = default; + + // Initialize from belt printer config. Returns true if a non-identity + // transform was computed. Inactive when belt_printer is disabled or + // both shear and scale are identity. + bool init_from_config(const PrintConfig &config); + + // Apply the transform to a point. Returns pos unchanged if not active. + Vec3d apply(const Vec3d &pos) const; + + // Apply the inverse transform. Returns pos unchanged if not active. + // Used by validators that need to compare emitted machine-frame + // coordinates against build-volume bounds. + Vec3d apply_inverse(const Vec3d &pos) const; + + bool is_active() const { return m_active; } + + // The composed shear*scale transform (identity when inactive). Exposed so the + // G-code viewer can build the machine->model back-transform for the upright + // ("designed") belt preview. + const Transform3d& transform() const { return m_transform; } + +private: + bool m_active = false; + Transform3d m_transform = Transform3d::Identity(); + Transform3d m_transform_inverse = Transform3d::Identity(); +}; + +} // namespace Slic3r + +#endif // slic3r_MachineFrameTransform_hpp_ diff --git a/src/libslic3r/GCode/MachineKinematics.cpp b/src/libslic3r/GCode/MachineKinematics.cpp new file mode 100644 index 0000000000..d9b4b65513 --- /dev/null +++ b/src/libslic3r/GCode/MachineKinematics.cpp @@ -0,0 +1,25 @@ +#include "MachineKinematics.hpp" +#include "../Point.hpp" + +namespace Slic3r { + +// Moved verbatim from GCodeWriter::apply_axis_remap(). +Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const +{ + if (!has_axis_remap()) + return pos; + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; +} + +Vec3d CartesianKinematics::to_machine(const Vec3d &p) const +{ + return this->apply_axis_remap(p); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp new file mode 100644 index 0000000000..fc13282998 --- /dev/null +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -0,0 +1,92 @@ +#ifndef slic3r_MachineKinematics_hpp_ +#define slic3r_MachineKinematics_hpp_ + +#include "../Point.hpp" + +namespace Slic3r { + +// The frame contract for emitted movement. +// +// GCodeWriter produces points in the *logical placed* frame: plate offsets have +// already been subtracted, but no machine-specific mapping has been applied. +// A MachineKinematics turns that into the coordinates actually written to +// G-code, and answers the two structural questions the writer needs in order to +// decide which axis words it may omit. +// +// This is a seam for writer-generated movement only. Start/end/custom G-code, +// classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass +// through it: they write machine coordinates directly. +class MachineKinematics +{ +public: + virtual ~MachineKinematics() = default; + + // Logical placed point -> emitted machine point. + virtual Vec3d to_machine(const Vec3d &p) const = 0; + + // True when a move must emit X, Y and Z because omitting a word would be + // wrong under this mapping. Deliberately not called "couples_axes": a pure + // axis permutation forces full emission without physically coupling axes. + virtual bool must_emit_all_axes() const = 0; + + // True when a lift must be suppressed while the current position is unknown, + // because _travel_to_z() re-emits the logical X/Y through this mapping and an + // uninitialised position would map to a bogus machine point -- for a reverse + // mapping, the far corner of the bed. + virtual bool suppress_lift_at_unknown_position() const = 0; + + // True when a G2/G3 arc in the logical XY plane is still the same arc in the + // machine frame. Arc moves emit only X, Y, I and J, so this asks a narrower + // question than must_emit_all_axes(): whether logical X and Y reach the + // machine unchanged. A mapping that only negates or reverses Z keeps its + // arcs; one that permutes X or Y moves the arc out of the plane that I/J + // describes, and a shear turns the circle into an ellipse G2/G3 cannot + // express at all. + virtual bool supports_arc_moves() const = 0; + + // Configuration. GCodeWriter forwards its setters here so that the state + // lives with the strategy and a strategy installed before the setters run + // still receives it. + virtual void set_axis_remap(int rx, int ry, int rz) = 0; + virtual void set_build_volume_max(const Vec3d &max) = 0; +}; + +// Axis remap only -- the historical GCodeWriter behaviour, moved verbatim. +// +// The remap encodes, per output axis, which source axis feeds it and how: +// r < 3 : source axis r, unchanged +// r < 6 : source axis r-3, negated +// else : source axis r-6, reversed within the build volume +class CartesianKinematics : public MachineKinematics +{ +public: + Vec3d to_machine(const Vec3d &p) const override; + + bool must_emit_all_axes() const override { return this->has_axis_remap(); } + bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); } + + // X and Y must reach the machine untouched. Because the remap is a + // permutation, pinning those two also pins Z to Z, so a mapping that only + // negates or reverses Z still supports arcs -- every word a G2/G3 emits is + // unchanged by it. + bool supports_arc_moves() const override { return m_remap_x == 0 && m_remap_y == 1; } + + void set_axis_remap(int rx, int ry, int rz) override + { m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; } + void set_build_volume_max(const Vec3d &max) override { m_build_vol_max = max; } + + bool has_axis_remap() const + { return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; } + +protected: + Vec3d apply_axis_remap(const Vec3d &pos) const; + + int m_remap_x { 0 }; + int m_remap_y { 1 }; + int m_remap_z { 2 }; + Vec3d m_build_vol_max { Vec3d::Zero() }; +}; + +} // namespace Slic3r + +#endif // slic3r_MachineKinematics_hpp_ diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index c591dd1c6a..24fed0493c 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -706,7 +706,7 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po, SeamPosition seam_position = spAligned) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: gather occlusion meshes: start"; - auto obj_transform = po->trafo_centered(); + auto obj_transform = po->trafo_sliced(); indexed_triangle_set triangle_set; indexed_triangle_set negative_volumes_set; //add all parts @@ -796,7 +796,7 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start"; - auto obj_transform = po->trafo_centered(); + auto obj_transform = po->trafo_sliced(); for (const ModelVolume *mv : po->model_object()->volumes) { // Collect painting only from model parts (what the gizmo edits) and negative volumes (the only way diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index c0f43f1888..f61587d562 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -18,6 +18,7 @@ #include "Utils.hpp" #include "format.hpp" #include "I18N.hpp" +#include "../BeltBrim.hpp" #include #include @@ -419,6 +420,10 @@ bool ToolOrdering::insert_wipe_tower_extruder() { if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower) return false; + // Belt mode has no classic wipe tower; the dedicated wipe tower filament + // must not inject extra toolchanges into the purge prism planning. + if (m_print_config_ptr->belt_printer) + return false; if (m_print_config_ptr->wipe_tower_filament == 0) return false; @@ -516,6 +521,11 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude zs.emplace_back(layer->print_z); for (auto layer : object.support_layers()) zs.emplace_back(layer->print_z); + // Belt brim apron bands sit below the object's first layer and have no + // layer of their own, but tools_for_layer() asserts an exact Z match, so + // their print_z must be part of the ordering. + for (const BeltBrimBand &band : object.belt_brim_prologue()) + zs.emplace_back(band.print_z); this->initialize_layers(zs); } @@ -560,6 +570,10 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool zs.emplace_back(layer->print_z); for (auto layer : object->support_layers()) zs.emplace_back(layer->print_z); + // See the single-object ctor: belt brim apron bands need their own + // ordering entries or tools_for_layer() will assert. + for (const BeltBrimBand &band : object->belt_brim_prologue()) + zs.emplace_back(band.print_z); max_layer_height = std::max(max_layer_height, object->config().layer_height.value); } @@ -994,6 +1008,42 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto } } + // Belt brim apron bands own their layers outright: they print below the + // object's first layer, so no object or support layer claims an extruder there + // and process_layer() would bail out at "Nothing to extrude". Claim the + // object's outer wall filament, in the same raw 1-based domain the loops above + // push. Deliberately not layer_tools.has_object, which drives skirt marking + // and wiping overrides. + if (! object.belt_brim_prologue().empty()) { + // 1-based, same domain the object/support pushes above use; reindexed to 0-based + // with the rest of the list later. + const unsigned int brim_filament = object.belt_brim_filament(); + for (const BeltBrimBand &band : object.belt_brim_prologue()) { + if (band.fills.empty()) + continue; + LayerTools &layer_tools = this->tools_for_layer(band.print_z); + layer_tools.extruders.push_back(brim_filament); + } + } + + // Coincident brim bands (belt_brim_by_layer) print ON an object layer rather than + // below it, but that layer can produce no InstanceVisit in process_layer - a + // zero-extrusion lead-in slice with no coinciding support - and the band would then + // be silently dropped. Register the brim filament on every layer that carries a + // coincident band, in the same 1-based domain as the prologue push above, so a brim + // pass always exists there. + if (object.has_belt_brim()) { + const unsigned int brim_filament = object.belt_brim_filament(); + const auto &by_layer = object.belt_brim_by_layer(); + const size_t n = std::min(by_layer.size(), object.layers().size()); + for (size_t i = 0; i < n; ++ i) { + if (by_layer[i].empty()) + continue; + LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z); + layer_tools.extruders.push_back(brim_filament); + } + } + for (auto& layer : m_layer_tools) { // Sort and remove duplicates sort_remove_duplicates(layer.extruders); @@ -1036,12 +1086,28 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ } //FIXME this is a hack to get the ball rolling. + // The `print_z < object_bottom_z` clause reads "below the object" as "raft + // gap". On a belt printer that is wrong: the brim apron legitimately prints + // below the object's first layer, and treating those layers as raft would put a + // wipe tower at negative Z. A belt printer never prints the classic + // prime tower (Print::has_wipe_tower()), so simply drop the clause there. + // + // Gate on config.belt_printer, NOT on has_belt_brim: every layer below the + // object bottom on a belt printer is legitimately a sub-object stream - brim + // apron, belt support printed below Z0, or the object's own lead-in - and none of + // them is ever raft, because Print::validate() rejects raft_layers>0 on a belt + // printer outright. Narrowing this to has_belt_brim would reclassify + // belt-support-below-floor layers as raft on brim-less belt prints and reintroduce + // the negative-Z wipe tower, so the broad belt_printer gate is correct. + const bool belt_no_raft_gap = config.belt_printer.value; for (LayerTools < : m_layer_tools) lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0)) - || lt.print_z < object_bottom_z + EPSILON; + || (! belt_no_raft_gap && lt.print_z < object_bottom_z + EPSILON); // Test for a raft, insert additional wipe tower layer to fill in the raft separation gap. - for (size_t i = 0; i + 1 < m_layer_tools.size(); ++ i) { + // Skipped on belt printers for the same reason as the clause above: layers + // below the object are brim apron, not raft. + for (size_t i = 0; ! belt_no_raft_gap && i + 1 < m_layer_tools.size(); ++ i) { const LayerTools < = m_layer_tools[i]; const LayerTools <_next = m_layer_tools[i + 1]; if (lt.print_z < object_bottom_z + EPSILON && lt_next.print_z >= object_bottom_z + EPSILON) { diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 5cb3d4e977..5c11f482cd 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -84,7 +84,17 @@ public: void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; } + // Returns true if entity is not printed with its usual extruder for a given + // copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the + // belt purge prism uses it to tell which of its fills actually carry purge + // from the ones that are unclaimed waste (Print::_plan_belt_purge()). + bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { + auto it = entity_map.find(std::make_tuple(entity, object)); + return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1; + } + private: + int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; @@ -94,12 +104,6 @@ private: void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies); void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies); - // Returns true in case that entity is not printed with its usual extruder for a given copy: - bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { - auto it = entity_map.find(std::make_tuple(entity, object)); - return it == entity_map.end() ? false : it->second[copy_id] != -1; - } - std::map, ExtruderPerCopy> entity_map; // to keep track of who prints what // BBS std::map support_map; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 72c1812da5..6176b6693c 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,6 +1,7 @@ #include "GCodeWriter.hpp" #include "Config.hpp" #include "Extruder.hpp" +#include "Geometry.hpp" #include "I18N.hpp" #include "Point.hpp" #include "Polygon.hpp" @@ -31,6 +32,7 @@ #include #include #include +#include #ifdef __APPLE__ #include @@ -43,6 +45,57 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +// A lift emitted through _travel_to_z() re-emits the stored logical X/Y under a +// mapping that must emit every axis. While the position is unknown that X/Y is +// the uninitialised origin, which maps to a real but wrong machine point, so the +// lift has to be skipped rather than commanded. +bool GCodeWriter::must_skip_lift_now() const +{ + return m_kinematics->suppress_lift_at_unknown_position() && ! this->is_current_position_clear(); +} + +bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const +{ + if (m_first_layer_point_test) + return m_first_layer_point_test(point_logical); + return m_is_first_layer; +} + +void GCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; + m_kinematics->set_axis_remap(rx, ry, rz); +} + +void GCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; + m_kinematics->set_build_volume_max(max); +} + +void GCodeWriter::set_kinematics(std::unique_ptr kinematics) +{ + assert(kinematics); + m_kinematics = std::move(kinematics); + // Replay whatever was configured on the previous strategy so callers may + // install the kinematics before or after set_axis_remap/set_build_volume_max. + m_kinematics->set_axis_remap(m_remap_x, m_remap_y, m_remap_z); + m_kinematics->set_build_volume_max(m_build_vol_max); +} + +// Kept as the writer-facing name for "this move must emit every axis word". +bool GCodeWriter::has_axis_remap() const +{ + return m_kinematics->must_emit_all_axes(); +} + +Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const +{ + return m_kinematics->to_machine(pos); +} + bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -796,8 +849,14 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); - auto speed = m_is_first_layer + if (has_axis_remap()) { + // Axis remap may couple XY with Z; emit full XYZ in machine coordinates. + Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } + auto speed = this->point_on_first_layer(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); w.emit_f(speed * 60.0); //BBS @@ -810,6 +869,8 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { + if (m_force_normal_lift) + lift_type = LiftType::NormalLift; // check whether the above/below conditions are met double target_lift = 0; { @@ -824,6 +885,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS if (m_lifted == 0 && m_to_lift == 0 && target_lift > 0) { if (spiral_vase) { + if (this->must_skip_lift_now()) + // Record no lift, so a later unlift() does not descend from a + // height that was never commanded. + return ""; m_lifted = target_lift; return this->_travel_to_z(m_pos(2) + target_lift, "lift Z"); } @@ -836,8 +901,9 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) } // BBS: immediately execute an undelayed lift move with a spiral lift pattern -// designed specifically for subsequent gcode injection (e.g. timelapse) +// designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { + const LiftType effective_type = m_force_normal_lift ? LiftType::NormalLift : type; std::string lift_move; double target_lift = 0; { @@ -851,7 +917,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) { } // BBS: spiral lift only safe with known position - if (type == LiftType::SpiralLift && this->is_current_position_clear()) { + if (effective_type == LiftType::SpiralLift && this->is_current_position_clear()) { double radius = target_lift / (2 * PI * atan(filament()->travel_slope())); // static spiral alignment when no move in x,y plane. // spiral centra is a radius distance to the right (y=0) @@ -868,7 +934,12 @@ std::string GCodeWriter::eager_lift(const LiftType type) { } //BBS: if position is unknown use normal lift else if (target_lift > 0) { - lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z"); + if (this->must_skip_lift_now()) + // Skipped, not deferred: leave m_lifted at zero below so unlift() + // does not descend from a height that was never commanded. + target_lift = 0.; + else + lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z"); } m_lifted = target_lift; m_to_lift = 0; @@ -889,7 +960,12 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // BBS Vec3d dest_point = point; auto travel_speed = - m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); + this->point_on_first_layer(Vec3d(point.x() - m_x_offset, point.y() - m_y_offset, point.z())) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); + // See uses_pointwise_travel_speed(): the historical path deliberately emits the + // raw configured speed in the final branch below, ignoring travel_speed. + const double final_travel_speed = this->uses_pointwise_travel_speed() + ? travel_speed + : this->config.travel_speed.get_at(m_cached_extruder_idx); //BBS: a z_hop need to be handle when travel if (std::abs(m_to_lift) > EPSILON) { assert(std::abs(m_lifted) < EPSILON); @@ -938,13 +1014,23 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; GCodeG1Formatter w0; - w0.emit_xyz(slope_top_point); + // A slope lift is a straight (linear) diagonal move, so remapping its + // endpoint is exact. Route the destination through apply_axis_remap() + // when a remap is active (no-op at identity). + w0.emit_xyz(has_axis_remap() ? apply_axis_remap(slope_top_point) : slope_top_point); w0.emit_f(travel_speed * 60.0); //BBS w0.emit_comment(GCodeWriter::full_gcode_comment, comment); slop_move = w0.string(); } - else if (m_to_lift_type == LiftType::NormalLift) { + else if (m_to_lift_type == LiftType::NormalLift && ! this->must_skip_lift_now()) { + // Only lift in place when the current position is known, for a mapping + // that makes _travel_to_z re-emit logical X/Y: at print start (and after + // custom gcode) m_pos.xy is still the uninitialised origin, which would + // map to a bogus machine point. The xy_z_move below then travels straight + // to the destination with full XYZ and establishes the correct position. + // Mappings that do not need this (the historical Cartesian behaviour) + // report false and keep lifting unconditionally. slop_move = _travel_to_z(target.z(), "normal lift Z"); } } @@ -952,7 +1038,14 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co std::string xy_z_move; { GCodeG1Formatter w0; - if (this->is_current_position_clear()) { + if (has_axis_remap()) { + // Remap may couple XY with Z; emit full XYZ in machine coordinates. + w0.emit_xyz(apply_axis_remap(target)); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } + else if (this->is_current_position_clear()) { w0.emit_xyz(target); w0.emit_f(travel_speed * 60.0); w0.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -990,17 +1083,23 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; std::string out_string; GCodeG1Formatter w; - if (!this->is_current_position_clear()) + if (has_axis_remap()) { + // Remap may couple XY with Z; emit full XYZ in machine coordinates. + w.emit_xyz(apply_axis_remap(point_on_plate)); + w.emit_f(final_travel_speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + out_string = w.string(); + } else if (!this->is_current_position_clear()) { //force to move xy first then z after filament change w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_f(final_travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_f(final_travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); out_string = w.string(); } @@ -1035,12 +1134,19 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); if (speed == 0.) { - speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); + speed = this->point_on_first_layer(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)) + ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) + : this->config.travel_speed.get_at(m_cached_extruder_idx); } GCodeG1Formatter w; - w.emit_z(z); + if (has_axis_remap()) { + // Remap may couple Z with other axes; emit full XYZ. + Vec3d machine = apply_axis_remap(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + w.emit_xyz(machine); + } else { + w.emit_z(z); + } w.emit_f(speed * 60.0); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, comment); @@ -1049,6 +1155,14 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment) { + // A circular XY arc / spiral lift cannot be correctly axis-remapped by + // transforming only its endpoint: the arc plane (G17/XY) and the I-J center + // would change under the remap. When an axis remap is active, fall back to a + // plain linear lift instead of emitting a possibly-wrong spiral/arc. This + // single guard covers every spiral call site (lazy/eager lift and travel_to_xyz). + if (has_axis_remap()) + return _travel_to_z(z, comment); + std::string output; double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); @@ -1155,7 +1269,12 @@ void GCodeWriter::extrude_to_xy(std::string &out, const Vec2d &point, double dE, Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; GCodeG1Formatter w; - w.emit_xy(point_on_plate); + if (has_axis_remap()) { + Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + w.emit_xyz(machine); + } else { + w.emit_xy(point_on_plate); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS @@ -1163,6 +1282,42 @@ void GCodeWriter::extrude_to_xy(std::string &out, const Vec2d &point, double dE, w.append_to(out); } +// Approximate an arc with linear extrusions, for machine mappings that cannot +// express a G2/G3 (see extrude_arc_to_xy). center_offset is I/J: the centre +// relative to the CURRENT position, which is why this must run before m_pos is +// updated. +void GCodeWriter::extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d ¢er_offset, + double dE, const bool is_ccw, + const std::string &comment, bool force_no_extrusion) +{ + const Vec2d start = Vec2d(m_pos.x(), m_pos.y()); + const Vec2d centre = start + center_offset; + const double r = (start - centre).norm(); + if (r < EPSILON) { + // Degenerate: no arc to speak of, so a single move is exact. + this->extrude_to_xy(out, point, dE, comment, force_no_extrusion); + return; + } + + double a0 = std::atan2(start.y() - centre.y(), start.x() - centre.x()); + double a1 = std::atan2(point.y() - centre.y(), point.x() - centre.x()); + double sweep = a1 - a0; + if (is_ccw) { while (sweep <= 0.) sweep += 2. * PI; } + else { while (sweep >= 0.) sweep -= 2. * PI; } + + // Segment count from a chord-deviation bound: r*(1-cos(dtheta/2)) <= tol. + const double tol = 0.005; // mm + const double dmax = (tol >= r) ? PI : 2. * std::acos(1. - tol / r); + const int n = std::max(2, int(std::ceil(std::abs(sweep) / std::max(dmax, EPSILON)))); + + for (int i = 1; i <= n; ++ i) { + const double a = a0 + sweep * (double(i) / double(n)); + const Vec2d p = (i == n) ? point + : Vec2d(centre.x() + r * std::cos(a), centre.y() + r * std::sin(a)); + this->extrude_to_xy(out, p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion); + } +} + //BBS: generate G2 or G3 extrude which moves by arc //point is end point which means X and Y axis //center_offset is I and J axis @@ -1175,6 +1330,23 @@ std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& cent void GCodeWriter::extrude_arc_to_xy(std::string &out, const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion) { + // Arcs emit only X/Y/I/J, so a mapping that moves logical X or Y cannot be + // expressed as a G2/G3. GCode::should_disable_arc_fitting() normally stops + // arcs being generated at all for such a mapping, but this is public API, so + // define the behaviour rather than asserting. + // + // This check MUST precede every state mutation below: falling through to + // extrude_to_xy() after filament()->extrude(dE) would advance E twice. + // + // A single chord is not a safe substitute either -- a semicircle would become + // its diameter and a full circle a stationary blob -- so approximate the arc + // with linear segments bounded by a chord tolerance, splitting dE between + // them in proportion to arc length. + if (! m_kinematics->supports_arc_moves()) { + this->extrude_arc_as_polyline(out, point, center_offset, dE, is_ccw, comment, force_no_extrusion); + return; + } + m_pos(0) = point(0); m_pos(1) = point(1); if (!force_no_extrusion) @@ -1215,10 +1387,18 @@ void GCodeWriter::extrude_to_xyz(std::string &out, const Vec3d &point, double dE Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; GCodeG1Formatter w; - if (z_changed) + if (has_axis_remap()) { + // z_changed was computed from the ORIGINAL slicing Z, but an axis remap can + // make machine-Z depend on slicing X/Y. An X/Y-only move (slicing-Z + // unchanged) would then drop the required machine-Z word, so always emit + // full XYZ whenever a remap is active. + point_on_plate = apply_axis_remap(point_on_plate); w.emit_xyz(point_on_plate); - else + } else if (z_changed) { + w.emit_xyz(point_on_plate); + } else { w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); + } if (!force_no_extrusion) w.emit_e(filament()->E()); //BBS diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index abeca9bc03..12a56dacdc 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -10,31 +10,38 @@ #include #include #include +#include +#include #include "Extruder.hpp" #include "Point.hpp" #include "Polygon.hpp" #include "PrintConfig.hpp" #include "Config.hpp" +#include "GCode/MachineKinematics.hpp" +#include namespace Slic3r { + class GCodeWriter { public: GCodeConfig config; bool multiple_extruders; GCodeWriter() : - multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr), - m_curr_extruder_id (-1), - m_cached_extruder_idx(0), - m_single_extruder_multi_material(false), - m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0), - m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0), - m_last_bed_temperature(0), m_last_bed_temperature_reached(true), + multiple_extruders(false), m_lifted(0), m_to_lift(0), m_to_lift_type(LiftType::NormalLift), - m_current_speed(3600), m_is_first_layer(true) + m_is_first_layer(true), m_current_speed(3600), + m_kinematics(std::make_unique()), + m_cached_extruder_idx(0), + m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr), + m_curr_extruder_id (-1), + m_single_extruder_multi_material(false), + m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0), + m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0), + m_last_bed_temperature(0), m_last_bed_temperature_reached(true) {} Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } @@ -91,6 +98,10 @@ public: std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); + // Linear approximation of an arc, used when the machine mapping cannot + // express a G2/G3. Must be called before m_pos is updated: center_offset is + // relative to the current position. + void extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); // Each appends its line to `out`. void set_speed(std::string &out, double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()); @@ -147,16 +158,94 @@ public: void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; } void invalidate_jerk() { m_last_jerk = 0; } + // Axis remap: permute/negate/reverse axes in G-code output. + // Works standalone (without belt mode) for printers with non-standard axis conventions. + void set_axis_remap(int rx, int ry, int rz); + void set_build_volume_max(const Vec3d &max); + bool has_axis_remap() const; + + // Install the machine frame mapping. Any axis remap / build volume already + // configured is carried over, so install order does not matter. + void set_kinematics(std::unique_ptr kinematics); + const MachineKinematics& kinematics() const { return *m_kinematics; } + + // Per-point first-layer test. When set, travel speed selection asks it per + // destination point (in the writer's logical placed frame) instead of using + // the layer-coarse m_is_first_layer flag. GCode installs it on belt printers + // with the same test its extrusions use (GCode::on_first_layer(point)), so a + // travel is judged against the belt surface exactly as the path it leads to. + using FirstLayerPointTest = std::function; + void set_first_layer_point_test(FirstLayerPointTest test) { m_first_layer_point_test = std::move(test); } + + // Force every lift to a plain vertical lift. Spiral and slope lifts compute + // their slope in the logical frame and do not account for a machine mapping + // that couples axes. + void set_force_normal_lift(bool force) { m_force_normal_lift = force; } + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); - private: +protected: + // Position/lift/offset state. + Vec3d m_pos = Vec3d::Zero(); + double m_x_offset{ 0 }; + double m_y_offset{ 0 }; + double m_lifted; + double m_to_lift; + LiftType m_to_lift_type; + bool m_is_first_layer = true; + bool m_is_current_pos_clear = false; + double m_current_speed; + + std::string _travel_to_z(double z, const std::string &comment); + + // Whether a destination gets first-layer treatment. With a point test + // installed it decides; otherwise the layer-coarse m_is_first_layer flag does. + bool point_on_first_layer(const Vec3d &point_logical) const; + + // True when a lift must be skipped because this mapping would emit the + // stored logical X/Y and that position is not yet known. + bool must_skip_lift_now() const; + + // True when travel speed is selected per destination point rather than per + // layer. Set for writers that install a first-layer point test. The + // historical path emits the raw configured travel speed in the final branch + // of travel_to_xyz(), ignoring the first-layer selection computed at the top + // of that function; a point-test-driven writer uses the first-layer-aware + // value throughout. Both are preserved exactly -- unifying them would change + // emitted feedrates and belongs in its own commit. + bool uses_pointwise_travel_speed() const { return bool(m_first_layer_point_test); } + + FirstLayerPointTest m_first_layer_point_test; + bool m_force_normal_lift = false; + + // The machine frame mapping. Owns the axis-remap state that used to live + // here as m_remap_* / m_build_vol_max; the setters above forward to it. + // Never null: a CartesianKinematics at the identity remap reproduces the + // historical behaviour exactly. + std::unique_ptr m_kinematics; + + // Last configured remap / build volume, replayed onto a newly installed + // kinematics so set_kinematics() and the setters are order-independent. + int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc. + int m_remap_y = 1; + int m_remap_z = 2; + Vec3d m_build_vol_max = Vec3d::Zero(); + + // Apply the machine frame mapping to a point. Returns pos unchanged when the + // mapping is the identity. + Vec3d apply_axis_remap(const Vec3d &pos) const; + + // Motion uses the global/base process variant until a filament becomes active. + // Indexes the per-extruder speed options (travel_speed, travel_speed_z, + // initial_layer_travel_speed). + size_t m_cached_extruder_idx; + +private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; bool m_single_extruder_multi_material; std::vector m_curr_filament_extruder; int m_curr_extruder_id; - // Motion uses the global/base process variant until a filament becomes active. - size_t m_cached_extruder_idx; unsigned int m_last_acceleration; unsigned int m_last_travel_acceleration; std::vector m_max_travel_acceleration; @@ -178,19 +267,6 @@ public: //BBS int m_last_bed_temperature; bool m_last_bed_temperature_reached; - double m_lifted; - - // BBS - double m_to_lift; - LiftType m_to_lift_type; - Vec3d m_pos = Vec3d::Zero(); - //BBS: this flag is used to indicate whether the m_pos is real. - //A example that of the first move, the m_pos is zero, but the real position of extruder doesn't - //Pos must be clear after the first xyz travel move - bool m_is_current_pos_clear = false; - //BBS: x, y offset for gcode generated - double m_x_offset{ 0 }; - double m_y_offset{ 0 }; // Orca: slicing resolution in mm double m_resolution = 0.01; @@ -202,21 +278,18 @@ public: // non-rectangular beds such as delta/circular printers. Polygon m_bed_printable_area; std::vector m_extruder_printable_areas; - + std::string m_gcode_label_objects_start; std::string m_gcode_label_objects_end; //SoftFever bool m_is_bbl_printers = false; - double m_current_speed; - bool m_is_first_layer = true; enum class Acceleration { Travel, Print }; - std::string _travel_to_z(double z, const std::string &comment); std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment); // Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown. const Polygon *active_printable_area() const; diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index d2f76e7e66..19dbee88dc 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1260,7 +1260,7 @@ static inline std::vector> segmentation_top_and_bottom_l // project downards pointing painted triangles over bottom surfaces. std::vector> top_raw(num_facets_states), bottom_raw(num_facets_states); std::vector zs = zs_from_layers(layers); - Transform3d object_trafo = print_object.trafo_centered(); + Transform3d object_trafo = print_object.trafo_sliced(); #ifdef MM_SEGMENTATION_DEBUG_TOP_BOTTOM static int iRun = 0; @@ -1289,10 +1289,16 @@ static inline std::vector> segmentation_top_and_bottom_l slicing_params.trafo = volume_trafo; Polygons bottom_slice = slice_mesh(painted, zs[0], slicing_params); - top.erase(top.begin()); - bottom.erase(bottom.begin()); - - bottom[0] = union_(bottom[0], bottom_slice); + // Only the requested projections exist: with + // top_shell_layers = 0 `top` is empty and erasing its begin() was + // undefined (found by fuzzing: a sunk, painted object crashed here). + if (! top.empty()) + top.erase(top.begin()); + if (! bottom.empty()) { + bottom.erase(bottom.begin()); + if (! bottom.empty()) + bottom[0] = union_(bottom[0], bottom_slice); + } } else slice_mesh_slabs(painted, zs, volume_trafo, max_top_layers > 0 ? &top : nullptr, max_bottom_layers > 0 ? &bottom : nullptr, nullptr, throw_on_cancel_callback); auto merge = [](std::vector &&src, std::vector &dst) { @@ -2088,17 +2094,19 @@ std::vector> segmentation_by_painting(const PrintObject } BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Projection of painted triangles - Begin"; + // The layers were sliced in this frame (belt rotation, remap and Z lift included), and it already centers the object. + const Transform3d object_trafo = print_object.trafo_sliced(); for (const ModelVolume *mv : print_object.model_object()->volumes) { const ModelVolumeFacetsInfo facets_info = extract_facets_info(*mv); - tbb::parallel_for(tbb::blocked_range(1, num_facets_states), [&mv, &print_object, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(1, num_facets_states), [&mv, &object_trafo, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { for (size_t extruder_idx = range.begin(); extruder_idx < range.end(); ++extruder_idx) { throw_on_cancel_callback(); const indexed_triangle_set custom_facets = facets_info.facets_annotation.get_facets(*mv, EnforcerBlockerType(extruder_idx)); if (!mv->is_model_part() || custom_facets.indices.empty()) continue; - const Transform3f tr = print_object.trafo().cast() * mv->get_matrix().cast(); - tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &print_object, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { + const Transform3f tr = (object_trafo * mv->get_matrix()).cast(); + tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { for (size_t facet_idx = range.begin(); facet_idx < range.end(); ++facet_idx) { float min_z = std::numeric_limits::max(); float max_z = std::numeric_limits::lowest(); @@ -2151,7 +2159,6 @@ std::vector> segmentation_by_painting(const PrintObject Line line_to_test(Point(scale_(line_start_f.x()), scale_(line_start_f.y())), Point(scale_(line_end_f.x()), scale_(line_end_f.y()))); - line_to_test.translate(-print_object.center_offset()); // BoundingBoxes for EdgeGrids are computed from printable regions. It is possible that the painted line (line_to_test) could // be outside EdgeGrid's BoundingBox, for example, when the negative volume is used on the painted area (GH #7618). diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 41db0ed9db..b1c6ed21fa 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -1839,8 +1839,10 @@ void PerimeterGenerator::process_classic() bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner; if (is_outer_wall_first || //BBS: always print outer wall first when there indeed has brim. + // btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact). (this->layer_id == 0 && - this->object_config->brim_type == BrimType::btOuterOnly && + (this->object_config->brim_type == BrimType::btOuterOnly || + this->object_config->brim_type == BrimType::btLeadingEdgeOnly) && this->object_config->brim_width.value > 0)) entities.reverse(); // Orca: sandwich mode. Apply after 1st layer. diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index dc4056dda1..2e9577e9c8 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1238,7 +1238,7 @@ static std::vector s_Preset_print_options{ "top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed", "bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed", "outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield", - "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", + "brim_width", "leading_brim_length", "extra_brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", "raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion", "support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style", // BBS @@ -1308,6 +1308,8 @@ static std::vector s_Preset_print_options{ "prime_volume", "prime_tower_infill_gap", "prime_tower_flat_ironing", + "belt_purge_tower_width", + "belt_purge_tower_object", "enable_tower_interface_features", "enable_tower_interface_cooldown_during_tower", "wipe_tower_no_sparse_layers", @@ -1558,8 +1560,14 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", - "gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", + "printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_infinite_y", + "belt_slice_rotation", "belt_slice_rotation_angle", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", + "belt_frame_tilt_decouple", "belt_frame_tilt_angle", + "belt_support_floor_offset", + "enable_belt_purge_tower", + "gcode_flavor", "gcode_skip_config_block", + "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", "printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "extruder_clearance_dist_to_rod", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 46de2715eb..6d810a922d 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -69,6 +69,8 @@ #include "Thread.hpp" #include "Time.hpp" #include "GCode.hpp" +#include "BeltGCode.hpp" +#include "BeltTransform.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "GCode/WipeTowerEstimate.hpp" @@ -77,6 +79,7 @@ #include "MaterialType.hpp" #include "Model.hpp" #include "format.hpp" +#include "LocalesUtils.hpp" #include #include @@ -103,6 +106,7 @@ #include "Format/STEP.hpp" #include "PlaceholderParser.hpp" #include "SurfaceCollection.hpp" +#include "BeltBrim.hpp" namespace fs = boost::filesystem; @@ -166,6 +170,14 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Cache the plenty of parameters, which influence the G-code generator only, // or they are only notes not influencing the generated G-code. static std::unordered_set steps_gcode = { + // Belt printer G-code axis remap (only affects G-code output, not slicing). + "gcode_remap_x", + "gcode_remap_y", + "gcode_remap_z", + // Machine-frame transform (derived from belt tilt; only affects G-code output). + "belt_frame_tilt_decouple", "belt_frame_tilt_angle", + // Only inflates the GUI bed volume, like printable_area. + "belt_printer_infinite_y", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -367,8 +379,18 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. - || opt_key == "spiral_mode") { + || opt_key == "spiral_mode" + // Build plate tilt changes slicing plane orientation. + || opt_key == "build_plate_tilt_x" + || opt_key == "build_plate_tilt_y" + // Belt printer transform options change the mesh geometry before slicing. + || opt_key == "belt_printer" + || opt_key == "belt_slice_rotation" + || opt_key == "belt_slice_rotation_angle") { osteps.emplace_back(posSlice); + } else if ( + opt_key == "belt_support_floor_offset") { + osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" || opt_key == "filament_type" @@ -403,6 +425,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "hot_plate_temp" || opt_key == "textured_plate_temp" || opt_key == "enable_prime_tower" + || opt_key == "enable_belt_purge_tower" || opt_key == "enable_wrapping_detection" || opt_key == "prime_tower_enable_framework" || opt_key == "prime_tower_width" @@ -437,6 +460,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "prime_volume" || opt_key == "flush_into_infill" || opt_key == "flush_into_support" + || opt_key == "belt_purge_tower_width" || opt_key == "initial_layer_infill_speed" || opt_key == "travel_speed" || opt_key == "travel_speed_z" @@ -668,6 +692,9 @@ std::vector Print::print_object_ids() const bool Print::has_infinite_skirt() const { + // Belt printer: no skirt support. + if (m_config.belt_printer.value) + return false; // Orca: unclear why (m_config.ooze_prevention && this->extruders().size() > 1) logic is here, removed. // return (m_config.draft_shield == dsEnabled && m_config.skirt_loops > 0) || (m_config.ooze_prevention && this->extruders().size() > 1); @@ -676,6 +703,9 @@ bool Print::has_infinite_skirt() const bool Print::has_skirt() const { + // Belt printer: no skirt support. + if (m_config.belt_printer.value) + return false; return (m_config.skirt_height > 0); } @@ -684,6 +714,24 @@ bool Print::has_brim() const return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_brim(); }); } +bool Print::has_tilted_belt() const +{ + if (! m_config.belt_printer.value) + return false; + // A Z rotation leaves the belt floor flat (BeltTransform forces shear = 0) and no + // rotation at all means the machine is geometrically a flat bed. + const BeltRotationAxis axis = m_config.belt_slice_rotation.value; + if (axis != BeltRotationAxis::X && axis != BeltRotationAxis::Y) + return false; + const double tilt = std::abs(m_config.belt_slice_rotation_angle.value); + return tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG; +} + +bool Print::has_belt_brim() const +{ + return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_belt_brim(); }); +} + //BBS std::vector Print::layers_sorted_for_object(float start, float end, std::vector &layers_of_objects, std::vector &boundingBox_for_objects, VecOfPoints &objects_instances_shift) { @@ -1803,6 +1851,75 @@ StringObjectException Print::validate(std::vector *warnin if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Belt printer validation: incompatible features. + if (m_config.belt_printer.value) { + for (const PrintObject *object : m_objects) { + if (object->config().raft_layers > 0) + return { L("Raft is not compatible with belt printer mode.") }; + } + if (m_config.draft_shield != dsDisabled) + return { L("Draft shield is not compatible with belt printer mode.") }; + + // Belt brim spans many layers and owns the layers below the object, which + // spiral vase cannot share. The prime tower setting is no obstacle: belt + // printers never print the classic tower, and the belt purge prism is an + // ordinary object that never takes a brim. + if (this->has_belt_brim()) { + if (m_config.spiral_mode.value) + return { L("Brim is not compatible with spiral vase mode on a belt printer. " + "Disable one of them.") }; + } + + for (const PrintObject *object : m_objects) { + const PrintObjectConfig &ocfg = object->config(); + // Mirror PrintObject::has_belt_brim(): an inner-only brim needs a positive + // brim_width (leading/extra widen only the outer ring), so keep this + // predicate in step or the belt-brim warnings below would fire for a brim + // that has_belt_brim() rejects. + const bool wants_brim = ocfg.brim_type != btNoBrim + && (ocfg.brim_type == btInnerOnly + ? ocfg.brim_width.value > 0. + : (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0. + || ocfg.extra_brim_width.value > 0.)); + if (! wants_brim) + continue; + + if (! this->has_tilted_belt()) { + if (std::abs(m_config.belt_slice_rotation_angle.value) > BELT_BRIM_MAX_TILT_DEG) + warn(L("The belt is too steep for a brim, so no brim will be generated."), + "brim_width", object->model_object()); + else + warn(L("A brim is only generated when the belt is tilted. Set a belt tilt angle, " + "or remove the brim setting."), + "brim_type", object->model_object()); + } + + if (ocfg.brim_type == btAutoBrim || ocfg.brim_type == btEar || ocfg.brim_type == btPainted) + warn(L("Belt printers support outer and inner brim only. Auto, Mouse ear and Painted " + "brim are printed as Outer brim only, using Brim width."), + "brim_type", object->model_object()); + + if (ocfg.leading_brim_length.value > 0. && ocfg.brim_object_gap.value > 0.) + warn(L("Brim-object gap separates the leading brim from the object's leading edge, " + "which is the edge it is meant to anchor. Set the gap to 0 when using leading " + "brim length."), + "brim_object_gap", object->model_object()); + } + if (this->has_belt_brim() && m_objects.size() > 1) + warn(L("Leading brim length extends ahead of each object along the belt, and Arrange does " + "not reserve that space. Leave room between objects."), + "leading_brim_length"); + } else { + // "Leading edge only" describes where a part meets a moving belt, so it has no + // meaning on a fixed bed. Brim.cpp prints it as an ordinary outer brim rather + // than silently producing nothing; say so. + for (const PrintObject *object : m_objects) + if (object->config().brim_type == btLeadingEdgeOnly) + warn(L("\"Leading edge only\" brim applies to belt printers. On this printer it is " + "printed as an ordinary outer brim."), + "brim_type", object->model_object()); + } + // Orca: a gradient mixed filament only renders its gradient with "Mixed color sublayer" on; // without it ToolOrdering::resolve_mixed_filaments prints one whole component per layer and // the gradient is dropped silently. extruders() already covers painting, height ranges, @@ -1868,6 +1985,40 @@ StringObjectException Print::validate(std::vector *warnin } } + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value + && m_config.print_sequence == PrintSequence::ByObject + && extruders.size() > 1) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " + "filament changes will not be purged."); + warningtemp.opt_key = "enable_belt_purge_tower"; + warningtemp.is_warning = true; + add_warning(warningtemp); + } + + // The purge tower is a model object the GUI creates and sizes; libslic3r only purges + // into one that exists. A project sliced without it (the CLI on a project saved before + // the tower was generated) changes filament with nowhere to purge. + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value + && m_config.print_sequence != PrintSequence::ByObject + && ! m_config.spiral_mode.value && this->object_extruders().size() > 1 && ! this->has_belt_purge_tower()) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; " + "filament changes will not be purged. Open the project in the application " + "to generate the tower."); + warningtemp.opt_key = "enable_belt_purge_tower"; + warningtemp.is_warning = true; + add_warning(warningtemp); + } + + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) { + const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { + return object->config().belt_purge_tower_object.value; + }); + if (prism_count > 1) + return {L("The project contains multiple managed belt purge towers. Reload the plate or toggle the belt purge tower off and on to regenerate it.")}; + } + if (m_config.enable_prime_tower) { for (const PrintObject* object : m_objects) { if (object->config().precise_z_height.value) { @@ -1921,35 +2072,57 @@ StringObjectException Print::validate(std::vector *warnin return profile; }; - // Checks that the print does not exceed the max print height + // Checks that the print does not exceed the max print height. + // For belt printers the slicing-frame Z spans the sheared X-length and + // is not comparable to printable_height (which is gantry clearance in the + // build-volume frame). Compare against the model's pre-shear Z instead, + // mirroring the bbox computed in PrintObject::update_slicing_parameters. + // The machine-frame transform only changes how that height is written to + // G-code, not how much room there is under the gantry. + const bool belt_printer = this->config().belt_printer.value; + const double shrinkage_compensation_z = this->shrinkage_compensation().z(); for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) { const PrintObject &print_object = *m_objects[print_object_idx]; - //FIXME It is quite expensive to generate object layers just to get the print height! - if (auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value); - !layers.empty()) { - Vec3d test =this->shrinkage_compensation(); - const double shrinkage_compensation_z = this->shrinkage_compensation().z(); - - if (shrinkage_compensation_z != 1. && layers.back() > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) { - // The object exceeds the maximum build volume height because of shrinkage compensation. - return StringObjectException{ - Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name), - print_object.model_object(), - "" - }; - } else if (layers.back() > this->config().printable_height + EPSILON) { - // Test whether the last slicing plane is below or above the print volume. - return StringObjectException{ - 0.5 * (layers[layers.size() - 2] + layers.back()) > this->config().printable_height + EPSILON ? - Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name) : - Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) + - " " + _u8L("You might want to reduce the size of your model or change current print settings and retry."), - print_object.model_object(), - "" - }; + double effective_max_z = 0; + bool last_layer_below_max = false; + bool have_height = false; + + if (belt_printer) { + const double raw_z = print_object.model_object()->max_z(); + effective_max_z = raw_z; + have_height = raw_z > 0; + } else { + //FIXME It is quite expensive to generate object layers just to get the print height! + auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value); + if (!layers.empty()) { + effective_max_z = layers.back(); + last_layer_below_max = layers.size() >= 2 && + 0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON; + have_height = true; } } + + if (!have_height) + continue; + + if (shrinkage_compensation_z != 1. && effective_max_z > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) { + // The object exceeds the maximum build volume height because of shrinkage compensation. + return StringObjectException{ + Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name), + print_object.model_object(), + "" + }; + } else if (effective_max_z > this->config().printable_height + EPSILON) { + return StringObjectException{ + last_layer_below_max ? + Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) + + " " + _u8L("You might want to reduce the size of your model or change current print settings and retry.") : + Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name), + print_object.model_object(), + "" + }; + } } // Some of the objects has variable layer height applied by painting or by a table. @@ -1969,12 +2142,12 @@ StringObjectException Print::validate(std::vector *warnin return {_u8L("Variable layer height is not supported with Organic supports.") }; } - if (this->has_wipe_tower() && ! m_objects.empty()) { + if ((this->has_wipe_tower() || this->has_belt_purge_tower()) && ! m_objects.empty()) { // Orca: wipe_tower_filament (issue #10971) is inserted into the tool order after // resolve_mixed_filaments has expanded every mixed (virtual) slot, so a mixed slot here // would reach the G-code as a tool change to a slot no nozzle carries. The GUI hides // mixed slots from the option; this guards loaded projects and the CLI. - if (m_config.wipe_tower_filament > 0) { + if (this->has_wipe_tower() && m_config.wipe_tower_filament > 0) { const auto &is_mixed = m_config.filament_is_mixed.values; const size_t wipe_idx = size_t(m_config.wipe_tower_filament - 1); if (wipe_idx < is_mixed.size() && is_mixed[wipe_idx]) @@ -1996,12 +2169,17 @@ StringObjectException Print::validate(std::vector *warnin } } - if (! m_config.use_relative_e_distances) - return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") }; + // The following two constraints come from the classic wipe tower G-code + // generator; purging into the belt purge prism uses normal object + // extrusions and does not need them. + if (this->has_wipe_tower()) { + if (! m_config.use_relative_e_distances) + return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") }; + + if (m_config.ooze_prevention && m_config.single_extruder_multi_material) + return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")}; + } - if (m_config.ooze_prevention && m_config.single_extruder_multi_material) - return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")}; - #if 0 if (m_config.gcode_flavor != gcfRepRapSprinter && m_config.gcode_flavor != gcfRepRapFirmware && m_config.gcode_flavor != gcfRepetier && m_config.gcode_flavor != gcfMarlinLegacy && m_config.gcode_flavor != gcfMarlinFirmware) @@ -2728,8 +2906,28 @@ BoundingBox PrintObject::get_first_layer_bbox(float& a, float& layer_height, std a += area(slice); } } - if (has_brim()) + // Guard on `defined`: make_brim() can return before assigning this (it does on + // belt printers, where has_brim() is still true but the plate brim is skipped), + // and overwriting a valid bbox with an undefined one corrupted the first-layer + // centre and the GUI's first-layer area readout. + if (has_brim() && firstLayerObjectBrimBoundingBox.defined) bbox = firstLayerObjectBrimBoundingBox; + // Belt brim: the apron reaches ahead of the object along the belt. + if (has_belt_brim()) { + const Point shift = instances().empty() ? Point(0, 0) : instances()[0].shift_without_plate_offset(); + for (const ExPolygons &areas : m_belt_brim_areas_by_layer) + for (const ExPolygon &ex : areas) { + BoundingBox bb = get_extents(ex.contour); + bb.translate(shift.x(), shift.y()); + bbox.merge(bb); + } + for (const BeltBrimBand &band : m_belt_brim_prologue) + for (const ExPolygon &ex : band.areas) { + BoundingBox bb = get_extents(ex.contour); + bb.translate(shift.x(), shift.y()); + bbox.merge(bb); + } + } return bbox; } @@ -2776,6 +2974,19 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) if (m_objects.empty()) return; + // Belt purge prism: _plan_belt_purge() (psWipeTower) truncates the prism's + // layers and drops its unclaimed fills, stashing both so a replan can undo + // them. The object steps below regenerate per-layer content over m_layers + // ONLY, so if any of them is about to rerun the stashes must go back first; + // otherwise truncated layers keep stale perimeters/fills and dropped fills + // are re-inserted next to freshly generated ones. Every object-step + // invalidation also invalidates psWipeTower, so "psWipeTower not done" is + // exactly "some object step may rerun" -- and when it IS done nothing below + // regenerates, and the plan's edits have to stay. + if (!this->is_step_done(psWipeTower)) + for (PrintObject *obj : m_objects) + obj->belt_undo_purge_plan(); + { LifecycleEventContext ctx; ctx.id = std::to_string(m_model.id().id); @@ -2837,15 +3048,20 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) int object_count = m_objects.size(); std::set need_slicing_objects; std::set re_slicing_objects; + // Belt global modes couple each object's bed position into its layer Z values, + // so sharing layers between "identical" objects is wrong. + bool belt_no_share = m_config.belt_printer.value; if (!use_cache) { for (int index = 0; index < object_count; index++) { PrintObject *obj = m_objects[index]; - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + break; + } } } if (!obj->get_shared_object()) @@ -2864,12 +3080,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) PrintObject *obj = m_objects[index]; bool found_shared = false; if (need_slicing_objects.find(obj) == need_slicing_objects.end()) { - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - found_shared = true; - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + found_shared = true; + break; + } } } if (!found_shared) { @@ -2992,7 +3210,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) for (int i = range.begin(); i < range.end(); i++) { PrintObject* obj = m_objects[i]; if (need_slicing_objects.count(obj) != 0) { - obj->generate_support_material(); + // The belt brim follows sequentially below. + obj->generate_support_material(false); } else { if (obj->set_started(posSupportMaterial)) @@ -3001,6 +3220,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } } ); + // The belt brim keeps clear of every object's layers and support layers, + // so it runs once no support step is rebuilding them any more. + for (PrintObject *obj : m_objects) + obj->generate_belt_brim(); if (m_pipeline_plugin_active) for (size_t i = 0; i < m_objects.size(); ++i) @@ -3077,7 +3300,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) m_wipe_tower_data.clear(); m_tool_ordering.clear(); - if (this->has_wipe_tower()) { + if (this->has_belt_purge_tower() && this->config().print_sequence != PrintSequence::ByObject) { + this->_plan_belt_purge(); + } + else if (this->has_wipe_tower()) { this->_make_wipe_tower(); } else if (this->config().print_sequence != PrintSequence::ByObject) { @@ -3331,6 +3557,26 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } + // Belt brim: bound the first-layer convex hull by the lowest apron band, so + // bed levelling and the initial purge line account for brim that reaches + // ahead of every object. + if (this->has_belt_brim()) { + for (PrintObject *object : m_objects) { + if (! object->has_belt_brim() || object->belt_brim_prologue().empty()) + continue; + const BeltBrimBand &lowest = object->belt_brim_prologue().front(); + for (const PrintInstance &instance : object->instances()) + for (const ExPolygon &ex : lowest.areas) { + Polygon poly = ex.contour; + poly.translate(instance.shift); + append(m_first_layer_convex_hull.points, std::move(poly.points)); + } + } + } + + // Unchanged for belt printers: _make_skirt() already returns early for them, and + // the belt brim does not populate m_brimMapByInstance, which is what the + // skirt/brim grouping reads. if (has_skirt() || has_infinite_skirt() || has_brim()) { // Generate skirt/brim groups after brim so per-object and draft-shield footprints // include brims when grouping and offsetting skirt loops. @@ -3434,12 +3680,17 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor this->set_status(80, message); // The following line may die for multiple reasons. - GCode gcode; + // Factory: use BeltGCode for belt printers, plain GCode otherwise. + std::unique_ptr gcode; + if (m_config.belt_printer.value) + gcode = std::make_unique(); + else + gcode = std::make_unique(); //BBS: compute plate offset for gcode-generator const Vec3d origin = this->get_plate_origin(); - gcode.set_gcode_offset(origin(0), origin(1)); - gcode.do_export(this, path.c_str(), result, thumbnail_cb); - gcode.export_layer_filaments(result); + gcode->set_gcode_offset(origin(0), origin(1)); + gcode->do_export(this, path.c_str(), result, thumbnail_cb); + gcode->export_layer_filaments(result); //BBS if (result != nullptr) { result->conflict_result = m_conflict_result; @@ -3454,6 +3705,10 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor void Print::_make_skirt() { + // Belt printer: skirt is not compatible. + if (m_config.belt_printer.value) + return; + const bool generate_skirt = this->has_skirt() || this->has_infinite_skirt(); // First off we need to decide how tall the skirt must be. @@ -4502,6 +4757,13 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector 2) return true; @@ -4514,6 +4776,7 @@ bool Print::has_wipe_tower() const return false; } + const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const { // Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so @@ -4543,6 +4806,7 @@ bool Print::enable_timelapse_print() const return m_config.timelapse_type.value == TimelapseType::tlSmooth; } + void Print::_make_wipe_tower() { m_wipe_tower_data.clear(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 5e85bb040c..062a8eb3a3 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -27,6 +27,8 @@ #include "GCode/ThumbnailData.hpp" #include "GCode/GCodeProcessor.hpp" #include "MultiMaterialSegmentation.hpp" +#include "BeltBrim.hpp" +#include "BeltTransform.hpp" #include "ObjectID.hpp" #include "TriangleSelector.hpp" #include "libslic3r.h" @@ -374,6 +376,9 @@ public: // Trafo with the center_offset() applied after the transformation, to center the object in XY before slicing. Transform3d trafo_centered() const { Transform3d t = this->trafo(); t.pretranslate(Vec3d(- unscale(m_center_offset.x()), - unscale(m_center_offset.y()), 0)); return t; } + // trafo_centered() with the belt pre-slice transforms applied: the frame the layers were sliced in (Layer::slice_z). + // Equal to trafo_centered() unless a belt rotation or pre-slice remap is active. + Transform3d trafo_sliced() const; const PrintInstances& instances() const { return m_instances; } // Orca: Bounding box of each connected body, indexed by Layer::lslices_separated_component_ids. const std::vector& separated_body_bboxes() const { return m_separated_body_bboxes; } @@ -415,6 +420,26 @@ public: && ! this->has_raft(); } + // Belt brim. A tilted belt needs its brim laid onto the belt plane over many + // layers instead of as one flat first-layer ring, so it is generated by + // BeltBrim.cpp and stored per layer here. Deliberately separate from + // has_brim(): that predicate feeds PrintRegion extruder collection, support + // trimming and the spiral vase probe, and widening it would perturb belt + // support output. + bool has_belt_brim() const; + // Brim filament for this object's belt brim, returned in the 1-based domain of + // PrintRegion::outer_wall_filament_id and the raw values pushed into + // LayerTools::extruders in ToolOrdering::collect_extruders (ToolOrdering reindexes + // the whole list to 0-based afterwards). Lowest positive outer_wall_filament_id + // over the printing regions, 1 if none is explicitly set. + unsigned int belt_brim_filament() const; + const std::vector& belt_brim_by_layer() const { return m_belt_brim_by_layer; } + const std::vector& belt_brim_prologue() const { return m_belt_brim_prologue; } + void clear_belt_brim(); + void set_belt_brim(std::vector &&by_layer, + std::vector &&areas, + std::vector &&prologue); + // BBS const ExtrusionEntityCollection& object_skirt() const { return m_skirt; @@ -550,7 +575,13 @@ private: void ironing(); bool need_z_contouring() const; void contour_z(); - void generate_support_material(); + // with_belt_brim = false leaves the belt brim to generate_belt_brim(), for a + // caller that runs the support step of several objects in parallel. + void generate_support_material(bool with_belt_brim = true); + // The belt brim keeps clear of every object's layers and support layers, so + // it has to run after all support steps finished. A no-op unless a support + // step left it pending. + void generate_belt_brim(); void estimate_curled_extrusions(); void simplify_extrusion_path(); @@ -566,6 +597,39 @@ private: std::vector> detect_extruder_geometric_unprintables() const; void slice_volumes(); + // Belt mode: shift the sliced layer grid (layer print_z and the belt floor + // clipping shift) by delta, used by Print::_align_belt_purge_layers() to + // snap the purge prism's layers onto the printed objects' layer grid. + void belt_shift_layer_grid(double delta); + // Belt mode: remove layers above z (cancel the purge prism past the last + // toolchange). Returns how many layers were dropped. + size_t belt_truncate_layers_above(coordf_t z); + // Restore layers removed by belt_truncate_layers_above() before replanning. + // Wipe-tower-only invalidations do not necessarily reslice the object, so + // truncation must be reversible when later toolchanges move upward. + void belt_restore_truncated_layers(); + // Belt purge prism, plastic saving: drop the fills on one layer that no + // toolchange claimed. `claimed` reports whether an entity was overridden as + // purge; everything else on that layer would otherwise print as solid infill + // in the prism's own filament for nothing. Perimeters are never touched, so + // the bar keeps a continuous wall along the belt. + // + // Entities are STASHED, not deleted, with their original positions -- the + // same reversibility contract belt_truncate_layers_above() has, and the + // reason the original version of this had to be removed: psWipeTower can + // rerun without regenerating infill, and a later tool ordering may claim what + // this one did not. Returns the number of entities dropped. + size_t belt_drop_unclaimed_fills(Layer *layer, const std::function &claimed); + // Put every stashed fill back at its original index. Must run before a replan. + void belt_restore_dropped_fills(); + // Undo every edit _plan_belt_purge() made to this object's layers, leaving + // m_layers exactly as the object steps produced it. Fills first: they point + // into layers that are still live, and truncated layers were stashed whole + // with their own fills untouched, so the two stashes never share an entity. + // Print::process() calls this before any object step may rerun (those steps + // regenerate per-layer content over m_layers only, so a stale stash would + // otherwise be restored on top of fresh content); the plan calls it too. + void belt_undo_purge_plan() { belt_restore_dropped_fills(); belt_restore_truncated_layers(); } //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS @@ -608,7 +672,24 @@ private: SlicingParameters m_slicing_params; LayerPtrs m_layers; + LayerPtrs m_belt_truncated_layers; + // Fills removed by belt_drop_unclaimed_fills(), owned by this vector until + // restored or until clear_layers() deletes them. An entity is in exactly one + // of the live collection or this stash, never both. + struct BeltDroppedFill { + Layer *layer { nullptr }; + size_t region_idx { 0 }; + size_t index { 0 }; // position in the original fills.entities + ExtrusionEntity *entity { nullptr }; + }; + std::vector m_belt_dropped_fills; SupportLayerPtrs m_support_layers; + // Belt brim, generated in posSupportMaterial by BeltBrim.cpp. Object-local + // slicing frame, one entry per object layer plus a prologue of brim-only + // bands that print below the object's first layer. + std::vector m_belt_brim_by_layer; + std::vector m_belt_brim_areas_by_layer; + std::vector m_belt_brim_prologue; // BBS std::shared_ptr m_tree_support_preview_cache; @@ -628,7 +709,25 @@ private: PrintObject* m_shared_object{ nullptr }; - + // Belt printer: global Z offset applied to this object's layers for shear positioning. + double m_belt_global_z_offset { 0.0 }; + // generate_support_material(false) finished and generate_belt_brim() has not run yet. + bool m_belt_brim_pending { false }; + // Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc. + double m_belt_min_z { 0.0 }; + // Belt printer: exact belt_floor_z_shift computed during posSlice from a + // vertex-level scan of the post-transform mesh. Cached separately from + // m_slicing_params so that rebuilding m_slicing_params on a non-belt-affecting + // invalidation (e.g. support config change) doesn't replace the exact value + // with the bbox approximation seeded by create_from_config(). Cleared when + // posSlice is invalidated. + double m_belt_floor_z_shift_cached { 0.0 }; + bool m_belt_floor_z_shift_cache_valid { false }; +public: + double belt_global_z_offset() const { return m_belt_global_z_offset; } +private: + + // SoftFever // // object id @@ -991,6 +1090,15 @@ public: bool has_infinite_skirt() const; bool has_skirt() const; bool has_brim() const; + // True when the belt is tilted enough that the BELT plane, not the bed plane, is + // the adhesion surface. The flat plate brim is geometrically meaningless then + // and must not run, whatever the per-object brim settings say - in particular + // brim_type "Auto", which has_brim() reports as enabled even at width 0. + bool has_tilted_belt() const; + // True when at least one object actually gets a tilted-belt brim generated. + // Implies has_tilted_belt(), but additionally requires the object's own brim + // settings to ask for one. + bool has_belt_brim() const; //BBS bool has_auto_brim() const { return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject* object) { return object->config().brim_type == btAutoBrim; }); @@ -1067,6 +1175,8 @@ public: // Wipe tower support. bool has_wipe_tower() const; + // Belt purge prism (belt-mode replacement for the wipe tower). + bool has_belt_purge_tower() const; const WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const; const ToolOrdering& tool_ordering() const { return m_tool_ordering; } @@ -1325,6 +1435,12 @@ private: void _make_skirt(); void _make_wipe_tower(); + // Belt mode: route filament-change purging into the belt purge prism + // (flush-into-objects) without generating a classic wipe tower. + void _plan_belt_purge(); + // Belt mode: shift the purge prism's layer grid to match the printed + // objects' grid so toolchange layers find prism layers to purge into. + void _align_belt_purge_layers(); void finalize_first_layer_convex_hull(); void update_filament_self_index_cache(); // Deduplicates, per filament, the (extruder type x volume type) variants the grouping diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 1a7bf41aed..096f505542 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -6,6 +6,7 @@ #include "Point.hpp" #include "Polygon.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" #include "FilamentMixer.hpp" #include "Slicing.hpp" #include "libslic3r.h" @@ -186,22 +187,29 @@ struct PrintObjectTrafoAndInstances // Generate a list of trafos and XY offsets for instances of a ModelObject // Orca: Updated to include XYZ filament shrinkage compensation -static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation) +static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation, bool force_separate_instances = false) { std::set trafos; PrintObjectTrafoAndInstances trafo; //BBS: add useful logs for debug int index = 0; + int unique_counter = 0; for (ModelInstance *model_instance : model_object.instances) { if (model_instance->is_printable()) { // Orca: Updated with XYZ filament shrinkage compensation Geometry::Transformation model_instance_transformation = model_instance->get_transformation(); trafo.trafo = model_instance_transformation.get_matrix_with_applied_shrinkage_compensation(shrinkage_compensation); - + auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]); // Reset the XY axes of the transformation. trafo.trafo.data()[12] = 0; trafo.trafo.data()[13] = 0; + // Belt printer global mode: prevent instance grouping so each + // copy gets its own PrintObject with independent layer Z values. + // Add a tiny unique perturbation to the existing Z (don't replace + // it — the Z translation from ensure_on_bed must be preserved). + if (force_separate_instances) + trafo.trafo.data()[14] += 1e-10 * (++unique_counter); // Search or insert a trafo. auto it = trafos.emplace(trafo).first; const_cast(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift }); @@ -1286,6 +1294,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", i=%1%, key=%2%")%i %changed_keys[i]; } } + // On belt printers the support tilt follows the slicing rotation. The GUI keeps the two in + // sync, but a CLI or 3MF edit of the rotation alone would otherwise leave supports on a stale tilt. + if (const auto *belt_opt = new_full_config.option("belt_printer"); belt_opt && belt_opt->value) { + const auto *axis_opt = new_full_config.option>("belt_slice_rotation"); + const auto *angle_opt = new_full_config.option("belt_slice_rotation_angle"); + if (axis_opt && angle_opt) { + const auto tilt = BeltTransformPipeline::physical_tilt(axis_opt->value, angle_opt->value); + new_full_config.set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg)); + new_full_config.set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg)); + } + } const ConfigOption* enable_support_option = new_full_config.option("enable_support"); if (enable_support_option && enable_support_option->getBool()) m_support_used = true; @@ -1818,11 +1837,15 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ PrintObjectPtrs print_objects_new; print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size())); bool new_objects = false; + bool belt_instances_shifted = false; // Walk over all new model objects and check, whether there are matching PrintObjects. for (ModelObject *model_object : m_model.objects) { ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(*model_object)); // Orca: Updated for XYZ filament shrink compensation - model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation()); + // Belt printers: force each instance into its own PrintObject so each + // gets independent layer Z values (its bed position is folded into them). + bool belt_force_separate = m_config.belt_printer.value; + model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); std::vector old; old.reserve(print_object_status_db.count(*model_object)); for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(*model_object)) @@ -1870,6 +1893,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ if (status != PrintBase::APPLY_STATUS_UNCHANGED) { size_t extruder_num = new_full_config.option("nozzle_diameter")->size(); update_apply_status(status == PrintBase::APPLY_STATUS_INVALIDATED); + belt_instances_shifted = true; } print_objects_new.emplace_back((*it_old)->print_object); const_cast(*it_old)->status = PrintObjectStatus::Reused; @@ -1892,6 +1916,13 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } if (new_objects || deleted_objects) update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport })); + // A belt brim is clipped against the other objects on the plate (BeltBrim.cpp, + // belt_brim_obstacles), and it is rebuilt with its object's support step: an + // object that arrived or left changes every other brim owner's brim. + if ((new_objects || deleted_objects) && m_config.belt_printer.value) + for (PrintObject *object : m_objects) + if (object->has_belt_brim()) + update_apply_status(object->invalidate_step(posSupportMaterial)); if (new_objects) update_apply_status(false); print_regions_reshuffled = true; @@ -1905,6 +1936,14 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(object->invalidate_step(posSlice)); } } + + // Belt printer: when any object's instances shifted, re-slice every object. + // The global Z offset follows each object's position along the belt, and the + // belt brims are clipped against the other objects. + if (belt_instances_shifted && m_config.belt_printer.value) { + for (PrintObject *object : m_objects) + update_apply_status(object->invalidate_step(posSlice)); + } } //BBS: check the config again diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c8e364cc69..ce27b0eb93 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3,8 +3,10 @@ #include "Point.hpp" #include "Polygon.hpp" #include "PrintConfigConstants.hpp" +#include "BeltTransform.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" +#include "Geometry.hpp" #include "FilamentMixer.hpp" #include "MaterialType.hpp" #include "I18N.hpp" @@ -384,6 +386,27 @@ static t_config_enum_values s_keys_map_SlicingMode { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SlicingMode) +static t_config_enum_values s_keys_map_BeltRotationAxis { + { "none", int(BeltRotationAxis::None) }, + { "x", int(BeltRotationAxis::X) }, + { "y", int(BeltRotationAxis::Y) }, + { "z", int(BeltRotationAxis::Z) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRotationAxis) + +static t_config_enum_values s_keys_map_RemapAxis { + { "pos_x", int(RemapAxis::PosX) }, + { "pos_y", int(RemapAxis::PosY) }, + { "pos_z", int(RemapAxis::PosZ) }, + { "neg_x", int(RemapAxis::NegX) }, + { "neg_y", int(RemapAxis::NegY) }, + { "neg_z", int(RemapAxis::NegZ) }, + { "rev_x", int(RemapAxis::RevX) }, + { "rev_y", int(RemapAxis::RevY) }, + { "rev_z", int(RemapAxis::RevZ) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -500,6 +523,7 @@ static const t_config_enum_values s_keys_map_BrimType = { {"auto_brim", btAutoBrim}, // BBS {"brim_ears", btEar}, // Orca {"painted", btPainted}, // BBS + {"leading_edge_only", btLeadingEdgeOnly}, // belt printers }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BrimType) @@ -1945,6 +1969,45 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("leading_brim_length", coFloat); + def->label = L("Leading brim length"); + def->category = L("Support"); + def->tooltip = L("Belt printers only. Extends the brim AHEAD of the object along the belt, on " + "every downhill-facing edge of its contact area - both the object's first " + "contact with the belt and any island that lands later. This apron is laid " + "onto the belt before the object reaches it, so the leading edge has " + "something already stuck down to hold on to.\n\n" + "Measured on the belt surface, and added on top of Brim width: the brim " + "reaches Brim-object gap + Leading brim length + Brim width ahead of the " + "object. Set Brim-object gap to 0, or the apron will not touch the object it " + "is meant to anchor.\n\n" + "On a tilted belt each layer lays one strip of the brim, so the thickness of " + "the resulting brim sheet is set by flow rather than by layer height. Use " + "Brim flow ratio to tune it.\n\n" + "Set to 0 to disable."); + def->sidetext = L("mm"); // millimeters, CIS languages need translation + def->min = 0; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("extra_brim_width", coFloat); + def->label = L("Extra brim width"); + def->category = L("Support"); + def->tooltip = L("Belt printers only. Widens the brim SIDEWAYS, across the belt, without " + "extending it further ahead of or behind the object. Use it when a part needs " + "more grip along its length than Brim width alone gives.\n\n" + "Measured on the belt surface, and added on top of Brim width: the brim " + "reaches Brim-object gap + Brim width + Extra brim width to either side of " + "the object. To extend the brim ahead of the object instead, use Leading brim " + "length.\n\n" + "Set to 0 to disable."); + def->sidetext = L("mm"); // millimeters, CIS languages need translation + def->min = 0; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.)); + def = this->add("brim_type", coEnum); def->label = L("Brim type"); def->category = L("Support"); @@ -1958,6 +2021,7 @@ void PrintConfigDef::init_fff_params() def->enum_values.emplace_back("inner_only"); def->enum_values.emplace_back("outer_and_inner"); def->enum_values.emplace_back("no_brim"); + def->enum_values.emplace_back("leading_edge_only"); def->enum_labels.emplace_back(L("Auto")); def->enum_labels.emplace_back(L("Mouse ear")); def->enum_labels.emplace_back(L("Painted")); @@ -1965,6 +2029,7 @@ void PrintConfigDef::init_fff_params() def->enum_labels.emplace_back(L("Inner brim only")); def->enum_labels.emplace_back(L("Outer and inner brim")); def->enum_labels.emplace_back(L("No-brim")); + def->enum_labels.emplace_back(L("Leading edge only")); def->mode = comSimple; def->set_default_value(new ConfigOptionEnum(btAutoBrim)); @@ -7271,6 +7336,166 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionFloatOrPercent(50., true)); + def = this->add("build_plate_tilt_x", coFloat); + def->label = L("Build plate tilt X"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the X axis. " + "A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. " + "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt. " + "In belt printer mode, this is automatically synced to the belt angle."); + def->sidetext = u8"\u00B0"; + def->min = -89; + def->max = 89; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("build_plate_tilt_y", coFloat); + def->label = L("Build plate tilt Y"); + def->category = L("Support"); + def->tooltip = L("Tilt angle of the build plate along the Y axis. " + "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " + "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); + def->sidetext = u8"\u00B0"; + def->min = -89; + def->max = 89; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("belt_printer", coBool); + def->label = L("Enable belt printing"); + def->category = L("Printable space"); + def->tooltip = L("Enable belt printer mode. Belt printers use a conveyor belt as the build surface, " + "tilted at an angle (typically 45 degrees). The slicer will rotate the slicing plane " + "and transform G-code coordinates for the tilted build surface."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("belt_printer_infinite_y", coBool); + def->label = L("Infinite Y axis"); + def->category = L("Printable space"); + def->tooltip = L("Enable infinite Y axis for belt printers. " + "When enabled, the Y axis build volume limit is effectively removed, " + "allowing objects of any length to be printed along the belt direction."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); + + // Mesh rotation applied before slicing — the sole mesh-side belt transform AND + // the single source of truth for the physical belt tilt (bed rendering, support + // gravity tilt and bed-exclusion projection all derive their angle from this). + def = this->add("belt_slice_rotation", coEnum); + def->label = L("Belt tilt axis"); + def->category = L("Printable space"); + def->tooltip = L("Axis the mesh is rotated about before slicing. This is the belt " + "printer's tilt: an isometric (no distortion) rotation that also " + "drives bed rendering and support gravity tilt, and that the g-code " + "back-transform inverts before the machine-frame shear/scale and remap. " + "X is the typical gantry tilt (belt travels along Y)."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "x", "y", "z"}; + def->enum_labels = {L("None"), L("X"), L("Y"), L("Z")}; + def->mode = comDevelop; + def->set_default_value(new ConfigOptionEnum(BeltRotationAxis::X)); + + def = this->add("belt_slice_rotation_angle", coFloat); + def->label = L("Belt tilt angle"); + def->category = L("Printable space"); + def->tooltip = L("Tilt angle of the belt surface, in degrees. Most belt printers use " + "45°. Positive values rotate counter-clockwise looking down the " + "positive tilt axis; the magnitude is also the physical belt tilt " + "used for bed rendering and support gravity."); + def->sidetext = L("°"); + def->min = -180.; + def->max = 180.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(45.)); + + def = this->add("belt_frame_tilt_decouple", coBool); + def->label = L("Decouple machine-frame tilt"); + def->category = L("Printable space"); + def->tooltip = L("Expert override: set the machine-frame (g-code shear/scale) tilt angle " + "independently of the pre-slice rotation angle. When disabled, the " + "machine-frame transform is derived from the belt tilt angle, so a single " + "angle drives both stages. Enable only to compensate for a machine whose " + "physical gantry tilt differs from the slicing rotation."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("belt_frame_tilt_angle", coFloat); + def->label = L("Machine-frame tilt angle"); + def->category = L("Printable space"); + def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (cot) and " + "scale (1/|sin|) applied to G-code. Only used when 'Decouple machine-frame " + "tilt' is enabled; otherwise the belt tilt angle is used."); + def->sidetext = L("°"); + def->min = -89.9; + def->max = 89.9; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(45.)); + + // G-code axis remap with sign. Each field is its own row in the settings tab. The + // labels and tooltips are literals in L() so they are extracted for translation. + auto add_belt_remap = [this](const char *key, const std::string &label, const std::string &tooltip, + RemapAxis default_axis, ConfigOptionMode mode) { + auto def = this->add(key, coEnum); + def->label = label; + def->category = L("Printable space"); + def->tooltip = tooltip; + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; + def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; + def->mode = mode; // Visibility may also be gated by toggle_line in Tab.cpp + def->set_default_value(new ConfigOptionEnum(default_axis)); + }; + add_belt_remap("gcode_remap_x", L("G-code remap X"), + L("Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation."), + RemapAxis::PosX, comDevelop); + add_belt_remap("gcode_remap_y", L("G-code remap Y"), + L("Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation."), + RemapAxis::PosY, comDevelop); + add_belt_remap("gcode_remap_z", L("G-code remap Z"), + L("Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation."), + RemapAxis::PosZ, comDevelop); + + // The machine-frame G-code transform (shear + scale) is no longer configured + // by per-axis keys: it is derived from the belt tilt (belt_slice_rotation axis + // + angle, or belt_frame_tilt_angle when decoupled) in MachineFrameTransform. + + // Belt support floor debug controls + def = this->add("belt_support_floor_offset", coFloat); + def->label = L("Support Floor Z offset"); + def->category = L("Printable space"); + def->tooltip = L("Shifts the computed belt floor up or down (mm). Negative values lower the floor, allowing more supports to survive. Use this to diagnose belt floor formula issues."); + def->sidetext = L("mm"); + def->min = -500; + def->max = 500; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + + def = this->add("enable_belt_purge_tower", coBool); + def->label = L("Enable belt purge tower"); + def->category = L("Multimaterial"); + def->tooltip = L("Belt-printer replacement for the wipe/prime tower. When enabled on a belt " + "printer, a purge prism is automatically generated next to the printed parts " + "and filament-change purging is routed into it (the classic wipe tower cannot " + "be used on belt printers because its G-code bypasses the belt transform). " + "Only available on belt printers."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("belt_purge_tower_width", coFloat); + def->label = L("Belt purge tower width"); + def->category = L("Printable space"); + def->tooltip = L("Width (machine X, across the belt) of the purge prism that is automatically " + "generated on belt printers when the belt purge tower is enabled and multiple " + "filaments are used. Filament-change purging is routed into this prism's " + "extrusions instead of a classic wipe tower. Its height is computed " + "automatically from the worst-case purge volume per layer: a wider prism " + "results in a shorter one."); + def->sidetext = L("mm"); + def->min = 1.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(35.)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); @@ -7924,6 +8149,16 @@ void PrintConfigDef::init_fff_params() "It will not take effect unless the prime tower is enabled."); def->set_default_value(new ConfigOptionBool(false)); + // Internal marker (not shown in any settings tab): identifies the auto-generated + // belt purge prism so it can be updated/removed by the auto-manager and aligned + // to the object layer grid by the backend. Persisted to 3mf like any per-object key. + def = this->add("belt_purge_tower_object", coBool); + def->category = L("Flush options"); + def->label = L("Belt purge tower object"); + def->tooltip = L("Marks the auto-generated belt purge prism. Managed automatically; do not set manually."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wipe_tower_bridging", coFloat); def->label = L("Maximal bridging distance"); def->tooltip = L("Maximal distance between supports on sparse infill sections."); @@ -9408,6 +9643,13 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va "smooth_coefficient", "overhang_totally_speed", "silent_mode", "overhang_speed_classic", "anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order + // Belt printer keys retired before the first release: the global-mode and + // back-transform switches are presumed on, and the pre-slice axis remap, the + // support Z offset mode, the support floor mode (always on) and the first-layer + // plane evaluator were removed. + "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", }; if (ignore.find(opt_key) != ignore.end()) { @@ -13135,10 +13377,22 @@ Polygons get_bed_excluded_area(const PrintConfig& cfg) { const Pointfs exclude_area_points = cfg.bed_exclude_area.values; + // Belt printer: project exclusion zone points from the belt surface to machine-frame XY. + // On the belt surface Z=0, so the in-plane axis foreshortens by cos(tilt). The tilt + // axis decides which bed axis foreshortens: tilt about X (belt along Y) scales Y, + // tilt about Y (belt along X) scales X. Derived from belt_slice_rotation. + const bool is_belt = cfg.belt_printer.value; + const auto tilt = BeltTransformPipeline::physical_tilt( + cfg.belt_slice_rotation.value, cfg.belt_slice_rotation_angle.value); + const double cos_x = is_belt ? std::cos(Geometry::deg2rad(tilt.tilt_x_deg)) : 1.0; // foreshortens Y + const double cos_y = is_belt ? std::cos(Geometry::deg2rad(tilt.tilt_y_deg)) : 1.0; // foreshortens X + Polygon exclude_poly; for (int i = 0; i < exclude_area_points.size(); i++) { auto pt = exclude_area_points[i]; - exclude_poly.points.emplace_back(scale_(pt.x()), scale_(pt.y())); + double x = is_belt ? pt.x() * cos_y : pt.x(); + double y = is_belt ? pt.y() * cos_x : pt.y(); + exclude_poly.points.emplace_back(scale_(x), scale_(y)); } exclude_poly.make_counter_clockwise(); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 43c3358cc9..7c9633ff78 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -284,6 +284,25 @@ enum class SlicingMode CloseHoles, }; +// Axis around which the mesh is rotated before slicing, when +// `belt_slice_rotation` is set. None disables the rotation stage. This is the +// single "belt tilt" axis: it drives both the pre-slice mesh rotation and the +// post-slice machine-frame transform (shear + scale derived from the tilt angle). +enum class BeltRotationAxis +{ + None = 0, + X = 1, + Y = 2, + Z = 3, +}; + +enum class RemapAxis +{ + PosX = 0, PosY = 1, PosZ = 2, + NegX = 3, NegY = 4, NegZ = 5, + RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -391,6 +410,10 @@ enum BrimType { btInnerOnly, btOuterAndInner, btNoBrim, + // Belt printers: brim only where the part first touches the belt, nothing after + // that. Appended last so no existing value shifts. On a non-belt printer this + // has no meaning and behaves as btOuterOnly. + btLeadingEdgeOnly, }; enum TimelapseType : int { @@ -724,6 +747,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRotationAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1195,6 +1220,8 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, brim_use_efc_outline)) ((ConfigOptionEnum, brim_type)) ((ConfigOptionFloat, brim_width)) + ((ConfigOptionFloat, leading_brim_length)) + ((ConfigOptionFloat, extra_brim_width)) ((ConfigOptionFloat, brim_ears_detection_length)) ((ConfigOptionFloat, brim_ears_max_angle)) ((ConfigOptionBool, brim_ears_outer_only)) @@ -1278,6 +1305,9 @@ PRINT_CONFIG_CLASS_DEFINE( // BBS ((ConfigOptionBool, flush_into_infill)) ((ConfigOptionBool, flush_into_support)) + // Marker for the auto-generated belt purge prism; identifies the object to + // the auto-manager (GUI) and the layer-grid alignment step (backend). + ((ConfigOptionBool, belt_purge_tower_object)) // BBS ((ConfigOptionFloat, tree_support_branch_distance)) ((ConfigOptionFloat, tree_support_tip_diameter)) @@ -1835,6 +1865,33 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( PrintConfig, (MachineEnvelopeConfig, GCodeConfig), + // Build plate tilt for off-axis gravity support generation (printer-level setting). + ((ConfigOptionFloat, build_plate_tilt_x)) + ((ConfigOptionFloat, build_plate_tilt_y)) + // Belt printer settings (printer-level). + ((ConfigOptionBool, belt_printer)) + ((ConfigOptionBool, belt_printer_infinite_y)) + // Mesh rotation applied before slicing — the single source of truth for the + // physical belt tilt. Its angle + axis drive bed rendering, support gravity + // tilt, the bed-exclusion projection, AND the post-slice machine-frame + // transform (shear + scale, derived from the tilt angle; see + // MachineFrameTransform). Isometric (no distortion) on the mesh side; the + // g-code back-transform inverts the rotation before the machine-frame stage. + ((ConfigOptionEnum, belt_slice_rotation)) + ((ConfigOptionFloat, belt_slice_rotation_angle)) + // Expert override: decouple the machine-frame tilt angle from the pre-slice + // rotation angle. When disabled, the machine frame uses belt_slice_rotation_angle. + ((ConfigOptionBool, belt_frame_tilt_decouple)) + ((ConfigOptionFloat, belt_frame_tilt_angle)) + ((ConfigOptionEnum, gcode_remap_x)) + ((ConfigOptionEnum, gcode_remap_y)) + ((ConfigOptionEnum, gcode_remap_z)) + ((ConfigOptionFloat, belt_support_floor_offset)) + // Width (machine X, across the belt) of the auto-generated belt purge prism. + ((ConfigOptionFloat, belt_purge_tower_width)) + // Belt-printer-only "type" of purge tower: enables the auto-generated belt + // purge prism (the belt replacement for the classic wipe/prime tower). + ((ConfigOptionBool, enable_belt_purge_tower)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionInts, close_additional_fan_first_x_layers)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 4f4d823951..3387d22ba8 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -8,6 +8,8 @@ #include "Polygon.hpp" #include "Polyline.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" + #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "Geometry.hpp" @@ -18,6 +20,7 @@ #include "PrintConfig.hpp" #include "SLA/IndexedMesh.hpp" #include "Support/SupportMaterial.hpp" +#include "Support/SupportCommon.hpp" #include "Support/SupportSpotsGenerator.hpp" #include "Support/TreeSupport.hpp" #include "Surface.hpp" @@ -81,6 +84,7 @@ #include "Fill/Lightning/Generator.hpp" #include "SurfaceCollection.hpp" #include "TriangleMesh.hpp" +#include "BeltBrim.hpp" namespace Slic3r { enum class EnforcerBlockerType : int8_t; } @@ -983,7 +987,7 @@ void PrintObject::detect_overhangs_for_lift() } } -void PrintObject::generate_support_material() +void PrintObject::generate_support_material(bool with_belt_brim) { if (this->set_started(posSupportMaterial)) { this->clear_support_layers(); @@ -1026,10 +1030,30 @@ void PrintObject::generate_support_material() this->_generate_support_material(); m_print->throw_if_canceled(); } + // Belt brim rides here rather than in the brim step because its apron + // prologue introduces print_z values below the object's first layer, and + // those must exist before ToolOrdering is built at psWipeTower - one step + // ahead of psSkirtBrim. The brim options already invalidate + // posSupportMaterial, so this needs no extra invalidation edges. + m_belt_brim_pending = true; + if (with_belt_brim) + this->generate_belt_brim(); this->set_done(posSupportMaterial); } } +void PrintObject::generate_belt_brim() +{ + if (! m_belt_brim_pending) + return; + // belt_brim_obstacles() looks up the layers and support layers of every object + // on the plate by print_z. Another object's support step rebuilds those (and + // temporarily shifts its layer Z values), so this must not overlap with it. + make_belt_brim(*this); + m_print->throw_if_canceled(); + m_belt_brim_pending = false; +} + void PrintObject::estimate_curled_extrusions() { if (this->set_started(posEstimateCurledExtrusions)) { @@ -1193,7 +1217,10 @@ FillAdaptive::RegionOctrees PrintObject::prepare_adaptive_infill_data( indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set(); // Rotate mesh and build octree on it with axis-aligned (standart base) cubes. auto to_octree = transform_to_octree().toRotationMatrix(); - its_transform(mesh, to_octree * this->trafo_centered(), true); + // Overhangs below are placed at Layer::bottom_z(), which includes the belt global Z offset. + Transform3d object_trafo = this->trafo_sliced(); + object_trafo.translation().z() += m_belt_global_z_offset; + its_transform(mesh, to_octree * object_trafo, true); // Triangulate internal bridging surfaces. std::vector> overhangs(std::max(surfaces_w_layer.size(), size_t(1))); @@ -1276,6 +1303,16 @@ void PrintObject::clear_layers() for (Layer *l : m_layers) delete l; m_layers.clear(); + for (Layer *l : m_belt_truncated_layers) + delete l; + m_belt_truncated_layers.clear(); + // Fills dropped for plastic saving are owned by the stash while they sit + // outside their layer's collection, so they are freed here too. Order + // matters only in that these point at layers deleted just above, and we + // never dereference the layer -- just the entity. + for (const BeltDroppedFill &d : m_belt_dropped_fills) + delete d.entity; + m_belt_dropped_fills.clear(); } } @@ -1309,6 +1346,75 @@ void PrintObject::clear_support_layers() l->cantilevers.clear(); } } + // Belt brim is owned by the same step, so it must die with it or an + // invalidate-without-rerun would leave stale bands (and stale prologue Zs) + // behind. Unconditional: unlike support layers it is never shared. + this->clear_belt_brim(); +} + +// Belt brim ------------------------------------------------------------------ +// +// The tilt test is answered from the print CONFIG, not from SlicingParameters: +// invalidating posSupportMaterial clears m_slicing_params.valid, and this is +// queried from Print::process() dispatch, Brim.cpp and the G-code emitter, where +// a stale zero shear factor would silently drop the brim. BeltBrim.cpp itself +// reads the real belt floor through BeltFloorContext, where the parameters are +// guaranteed current. +bool PrintObject::has_belt_brim() const +{ + if (! m_print->has_tilted_belt()) + return false; + // The purge prism is sacrificial and sits at the plate's edge; its generator sets no_brim, and + // this keeps it brimless whatever its config says, so a brim on the parts never blocks purging. + if (m_config.belt_purge_tower_object.value) + return false; + if (m_config.brim_type == btNoBrim) + return false; + // An inner-only brim has no leading/extra geometry: leading_brim_length and + // extra_brim_width both widen the OUTER ring, which btInnerOnly never emits, so it + // produces nothing unless brim_width itself is positive. Every other brim type is + // satisfied by any one of the three widths. Requiring the width here (instead of + // "any width") stops has_belt_brim() - and therefore Print::validate() - from + // rejecting the prime tower / spiral vase for a brim that would never be drawn. + if (m_config.brim_type == btInnerOnly) { + if (m_config.brim_width.value <= 0.) + return false; + } else if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0. + && m_config.extra_brim_width.value <= 0.) { + return false; + } + return ! this->has_raft(); +} + +unsigned int PrintObject::belt_brim_filament() const +{ + // 1-based, matching PrintRegion::outer_wall_filament_id and the raw values pushed + // into LayerTools::extruders in ToolOrdering::collect_extruders (the whole list is + // reindexed to 0-based later). Lowest positive outer-wall filament over the + // printing regions; 1 when none is explicitly set. + unsigned int brim_filament = 0; + for (size_t i = 0; i < this->num_printing_regions(); ++ i) { + const unsigned int f = this->printing_region(i).config().outer_wall_filament_id.value; + if (f > 0 && (brim_filament == 0 || f < brim_filament)) + brim_filament = f; + } + return brim_filament == 0 ? 1u : brim_filament; +} + +void PrintObject::clear_belt_brim() +{ + m_belt_brim_by_layer.clear(); + m_belt_brim_areas_by_layer.clear(); + m_belt_brim_prologue.clear(); +} + +void PrintObject::set_belt_brim(std::vector &&by_layer, + std::vector &&areas, + std::vector &&prologue) +{ + m_belt_brim_by_layer = std::move(by_layer); + m_belt_brim_areas_by_layer = std::move(areas); + m_belt_brim_prologue = std::move(prologue); } std::shared_ptr PrintObject::alloc_tree_support_preview_cache() @@ -1351,6 +1457,8 @@ bool PrintObject::invalidate_state_by_config_options( bool invalidated = false; for (const t_config_option_key &opt_key : opt_keys) { if ( opt_key == "brim_width" + || opt_key == "leading_brim_length" + || opt_key == "extra_brim_width" || opt_key == "brim_object_gap" || opt_key == "brim_use_efc_outline" || opt_key == "brim_type" @@ -1375,7 +1483,10 @@ bool PrintObject::invalidate_state_by_config_options( const auto* new_brim_type = new_config.option>(opt_key); //BBS: When switch to manual brim, the object must have brim, then re-generate perimeter //to make the wall order of first layer to be outer-first - if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly) + // btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it + // takes part in the same first-layer wall order rule. + if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly || + old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly) steps.emplace_back(posPerimeters); } } else if ( @@ -1735,7 +1846,8 @@ bool PrintObject::invalidate_state_by_config_options( } else if ( opt_key == "flush_into_infill" || opt_key == "flush_into_objects" - || opt_key == "flush_into_support") { + || opt_key == "flush_into_support" + || opt_key == "belt_purge_tower_object") { invalidated |= m_print->invalidate_step(psWipeTower); invalidated |= m_print->invalidate_step(psGCodeExport); } else { @@ -1765,12 +1877,25 @@ bool PrintObject::invalidate_step(PrintObjectStep step) invalidated |= this->invalidate_steps({ posIroning, posContouring, posSimplifyInfill }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); } else if (step == posSlice) { - invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill }); + // posSimplifySupportPath is listed with posSupportMaterial: invalidate_steps() does not + // propagate, so without it a re-slice regenerated the supports but kept the step done, + // and the new support paths were exported unsimplified, unlike a fresh slice. + // posDetectOverhangsForLift reads the layers' overhang regions, which a re-slice + // starts over empty: without it here the step stayed done and the lift logic in + // GCode::needs_retraction() had no overhangs to test against until something else + // invalidated it. + invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath, posDetectOverhangsForLift }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); m_slicing_params.valid = false; + // The exact belt_floor_z_shift is recomputed when slice() runs again. + m_belt_floor_z_shift_cache_valid = false; } else if (step == posSupportMaterial) { invalidated |= this->invalidate_steps({ posSimplifySupportPath }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); + // SlicingParameters depend on support config (enable_support / + // raft_layers / enforce_support_layers feed min/max layer height in + // Slicing.cpp), so invalidate them here. The vertex-scan + // belt_floor_z_shift is preserved via m_belt_floor_z_shift_cached. m_slicing_params.valid = false; } @@ -1791,6 +1916,7 @@ bool PrintObject::invalidate_all_steps() bool result = inherited_invalidated || print_invalidated; // Then reset some of the depending values. m_slicing_params.valid = false; + m_belt_floor_z_shift_cache_valid = false; return result; } @@ -4127,8 +4253,36 @@ void PrintObject::update_slicing_parameters() { // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { - m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, this->model_object()->max_z(), + coordf_t object_height = this->model_object()->max_z(); + BeltTransformPipeline::BeltFloorParams belt_floor; + const auto &pcfg = this->print()->config(); + if (pcfg.belt_printer.value) { + // The box of the mesh in the frame it is sliced in: XY centred and Z as + // placed on the bed (trafo_centered()). raw_bounding_box() has the + // instance's Z offset removed, and the belt floor is not invariant to a + // Z shift (a point's z and the floor under it move in opposite + // directions under the rotation), so an offset box under-estimates the + // height by twice the shift and the layers stop part way up the object. + BoundingBoxf3 bb; + const Transform3d trafo = this->trafo_centered(); + for (const ModelVolume *v : this->model_object()->volumes) + if (v->is_model_part()) + bb.merge(v->mesh().transformed_bounding_box(trafo * v->get_matrix())); + auto hr = BeltTransformPipeline::compute_belt_height_and_floor(pcfg, bb, object_height); + object_height = hr.object_height; + belt_floor = hr.floor_params; + } + m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); + // Populate belt floor parameters into slicing params for support clipping. + m_slicing_params.belt_floor_shear_factor = belt_floor.shear_factor; + m_slicing_params.belt_floor_from_axis = belt_floor.from_axis; + m_slicing_params.belt_floor_z_shift = belt_floor.z_shift; + // Prefer the vertex-scan z_shift over the bbox approximation when + // slice() has already produced one (e.g. this rebuild was triggered + // by a support-config change, which doesn't move the belt floor). + if (m_belt_floor_z_shift_cache_valid) + m_slicing_params.belt_floor_z_shift = m_belt_floor_z_shift_cached; } } @@ -4169,9 +4323,28 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders - if (object_max_z <= 0.f) - object_max_z = (float)model_object.raw_bounding_box().size().z(); - return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + BeltTransformPipeline::BeltFloorParams belt_floor; + if (object_max_z <= 0.f) { + BoundingBoxf3 bb = model_object.raw_bounding_box(); + object_max_z = (float)bb.size().z(); + if (print_config.belt_printer.value) { + // Z as placed on the bed, XY around the instance origin: the belt floor + // depends on where the box sits in Z (see update_slicing_parameters()). + if (! model_object.instances.empty()) { + bb = model_object.instance_bounding_box(0, false); + const Vec3d off = model_object.instances.front()->get_offset(); + bb.translate(-off.x(), -off.y(), 0.); + } + auto hr = BeltTransformPipeline::compute_belt_height_and_floor(print_config, bb, object_max_z); + object_max_z = (float)hr.object_height; + belt_floor = hr.floor_params; + } + } + SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + params.belt_floor_shear_factor = belt_floor.shear_factor; + params.belt_floor_from_axis = belt_floor.from_axis; + params.belt_floor_z_shift = belt_floor.z_shift; + return params; } // returns 0-based indices of extruders used to print the object (without brim, support and other helper extrusions) @@ -4688,9 +4861,58 @@ void PrintObject::_generate_support_material() tree_support.generate(); } else { - PrintObjectSupportMaterial support_material(this, m_slicing_params); - support_material.generate(*this); + // The normal generator anchors its layer grid at the slicing frame origin + // (SlicingParameters: first layer at first_print_layer_height, raft at + // z = 0), so it has to see the object layers in that frame. On a belt the + // object layers carry the global Z offset (PrintObject::slice()), which is + // negative for the leading half of the belt: a top contact below z = 0 + // then turns the intermediate-layer count negative and the generator + // allocates layers until memory runs out. Lift the offset off the object + // layers and the belt floor for the duration of the run and put it back + // on everything, including the new support layers, afterwards (organic + // tree support is shifted the same way below). + const double global_z = m_belt_global_z_offset; + const bool unshift = std::abs(global_z) > EPSILON; + auto shift_object_frame = [this, global_z](double sign) { + for (Layer *layer : m_layers) + layer->print_z += sign * global_z; + m_slicing_params.belt_floor_z_shift += sign * global_z; + }; + if (unshift) + shift_object_frame(-1.); + try { + PrintObjectSupportMaterial support_material(this, m_slicing_params); + support_material.generate(*this); + } catch (...) { + if (unshift) + shift_object_frame(1.); + throw; + } + if (unshift) { + shift_object_frame(1.); + for (SupportLayer *sl : m_support_layers) + sl->print_z += global_z; + } } + // Global Z offset for support layers: + // - Normal support: generated in the object frame above and shifted afterwards. + // - Non-organic tree support (slim/strong/hybrid): plan_layer_heights() reads + // from globally-offset object layers, so support layers already have it. + // - Organic tree support: generate_tree_support_3D() computes its own Z values + // independently and does NOT inherit the offset — apply it here. + // Belt floor polygon clipping for non-organic tree support is done inside + // draw_circles() before area_groups and toolpaths are built. + if (is_tree(m_config.support_type.value) && std::abs(m_belt_global_z_offset) > EPSILON) { + // Resolve effective support style (same logic as SupportParameters). + auto style = m_config.support_style.value; + if (style == smsDefault) + style = smsTreeOrganic; + if (style == smsTreeOrganic) { + for (SupportLayer *sl : m_support_layers) + sl->print_z += m_belt_global_z_offset; + } + } + } // BBS @@ -5040,6 +5262,7 @@ static void project_triangles_to_slabs(ConstLayerPtrsAdaptor layers, const index void PrintObject::project_and_append_custom_facets( bool seam, EnforcerBlockerType type, std::vector& out, std::vector>* vertical_points) const { + const Transform3d object_trafo = this->trafo_sliced(); for (const ModelVolume* mv : this->model_object()->volumes) if (mv->is_model_part()) { const indexed_triangle_set custom_facets = seam @@ -5048,12 +5271,12 @@ void PrintObject::project_and_append_custom_facets( if (! custom_facets.indices.empty()) { if (seam) project_triangles_to_slabs(this->layers(), custom_facets, - (this->trafo_centered() * mv->get_matrix()).cast(), + (object_trafo * mv->get_matrix()).cast(), seam, out); else { std::vector projected; // Support blockers or enforcers. Project downward facing painted areas upwards to their respective slicing plane. - slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &projected, vertical_points, [](){}); + slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), object_trafo * mv->get_matrix(), nullptr, &projected, vertical_points, [](){}); // Merge these projections with the output, layer by layer. assert(! projected.empty()); assert(out.empty() || out.size() == projected.size()); diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 038797ed3c..facce01787 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -27,6 +28,9 @@ #include "Point.hpp" #include "Polygon.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" +#include "BeltSliceStrategy.hpp" +#include "Geometry.hpp" //BBS #include "PrintConfig.hpp" #include "PrintBase.hpp" @@ -180,7 +184,8 @@ static std::vector slice_volumes_inner( ModelVolumePtrs model_volumes, const std::vector &layer_ranges, const std::vector &zs, - const std::function &throw_on_cancel_callback) + const std::function &throw_on_cancel_callback, + double *out_belt_min_z = nullptr) { model_volumes_sort_by_id(model_volumes); @@ -195,6 +200,10 @@ static std::vector slice_volumes_inner( params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.extra_offset = 0; params_base.trafo = object_trafo; + // Pre-slice mesh transforms: axis remap (standalone — works without belt + // mode), belt rotation, and the per-object Z-shift. Owned by BeltSliceStrategy + // so this belt/remap-specific logic stays out of the generic slicing pipeline. + BeltSliceStrategy::apply_preslice_transforms(params_base.trafo, print_config, model_volumes, out_belt_min_z); //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025; @@ -328,14 +337,38 @@ static std::vector> slices_to_regions( } } else { zs_complex.reserve(zs.size()); + // region.bbox is computed in pre-belt-transform slicer space (see PrintApply.cpp::trafo_for_bbox). + // When belt transforms are active, layer Z values are in post-rotation/shear/scale/remap space, + // so the Z components of region.bbox aren't comparable to z. Skipping the Z filter here + // pushes those layers into the parallel_for path below, which handles multi-volume + // clipping per layer without relying on the bbox Z range. + const bool bbox_z_in_layer_frame = !(print_config.belt_printer.value && + BeltTransformPipeline::has_rotation(print_config)); + // Belt-transform addendum: with bbox-Z untrusted, the simple path's + // "first model_part wins" logic drops subsequent volumes' slices unless + // they XY-overlap with the first. Assemblies whose volumes are stacked + // or side-by-side in pre-transform Z (different bbox.z ranges) thus lose + // the volumes that originally sat outside the first volume's Z range — + // showing up as truncation at the top or bottom of the assembly. Force + // every layer in a multi-volume range through the parallel_for path, + // which correctly merges all volumes per layer. + int num_model_parts = 0; + for (const PrintObjectRegions::VolumeRegion &vr : layer_range.volume_regions) + if (vr.model_volume->is_model_part()) + ++num_model_parts; + const bool force_complex_for_belt = !bbox_z_in_layer_frame && num_model_parts > 1; for (; z_idx < zs.size() && zs[z_idx] < layer_range.layer_height_range.second; ++ z_idx) { float z = zs[z_idx]; + if (force_complex_for_belt) { + zs_complex.push_back({ z_idx, z }); + continue; + } int idx_first_printable_region = -1; bool complex = false; std::vector printable_region_ids; for (int idx_region = 0; idx_region < int(layer_range.volume_regions.size()); ++ idx_region) { const PrintObjectRegions::VolumeRegion ®ion = layer_range.volume_regions[idx_region]; - if (region.bbox->min().z() <= z && region.bbox->max().z() >= z) { + if (!bbox_z_in_layer_frame || (region.bbox->min().z() <= z && region.bbox->max().z() >= z)) { if (region.model_volume->is_model_part()) printable_region_ids.push_back(idx_region); @@ -346,7 +379,9 @@ static std::vector> slices_to_regions( // Test for overlap with some other region. for (int idx_region2 = idx_first_printable_region; idx_region2 < idx_region; ++ idx_region2) { const PrintObjectRegions::VolumeRegion ®ion2 = layer_range.volume_regions[idx_region2]; - if (region2.bbox->min().z() <= z && region2.bbox->max().z() >= z && overlap_in_xy(*region.bbox, *region2.bbox)) { + const bool region2_in_z = !bbox_z_in_layer_frame + || (region2.bbox->min().z() <= z && region2.bbox->max().z() >= z); + if (region2_in_z && overlap_in_xy(*region.bbox, *region2.bbox)) { complex = true; break; } @@ -859,10 +894,32 @@ void PrintObject::slice() this->update_layer_height_profile(*this->model_object(), m_slicing_params, layer_height_profile); m_print->throw_if_canceled(); m_typed_slices = false; + // The belt state below is only written while belt mode is on (and the min-Z + // lift only when there is a rotation or remap). Start every slice from zero, + // or a project switched from a belt printer to a normal one, or whose tilt + // axis was set to None, keeps the previous offsets: the adaptive infill octree + // and the organic support layers (PrintObject.cpp) would still be shifted by + // them. + m_belt_min_z = 0.; + m_belt_global_z_offset = 0.; this->clear_layers(); m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); m_print->throw_if_canceled(); + + // Belt floor Z-shift: where is the belt surface in final slicer space? + // + // The belt surface is the model's Z=0 plane. After the belt rotation and the + // Z-shift it is the plane Z_belt = shear_factor * from_axis + z_shift_val in + // slicer space, with z_shift_val = max(0, -m_belt_min_z), the lift that starts + // the slicing frame at the belt below the footprint. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { + double z_shift_val = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + // The belt surface is at Z=0 in centered slicer space and bb.min.z() is + // already folded into m_belt_min_z. + m_slicing_params.belt_floor_z_shift = z_shift_val; + } + int firstLayerReplacedBy = 0; #if 0 @@ -901,6 +958,105 @@ void PrintObject::slice() if (m_layers.empty()) throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n")); + // Belt printer: offset all layer Z values so objects at different positions + // along the belt print at different heights on the tilted belt. This is a + // post-slicing adjustment: the sliced geometry is the same, only the output Z + // coordinates change. + { + const auto &pcfg = this->print()->config(); + BOOST_LOG_TRIVIAL(trace) << "Belt global check: belt_printer=" << pcfg.belt_printer.value + << " belt_slice_rotation=" << int(pcfg.belt_slice_rotation.value) + << " object=" << this->model_object()->name; + if (pcfg.belt_printer.value) { + + Point inst_shift = this->instances().empty() ? Point(0, 0) + : this->instances().front().shift - this->center_offset(); + BOOST_LOG_TRIVIAL(trace) << "Belt global: object " << this->model_object()->name + << " instances=" << this->instances().size() + << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; + + // Per-object Z-shift compensation, applied regardless of global mode. + // + // BeltSliceStrategy::apply_preslice_transforms lifts the mesh by max(0, -m_belt_min_z) + // so the slicer can slice with slicer_z >= 0. BeltBackTransform inverts + // build_forward_transform() which DOES NOT include this per-object + // Z-shift (it's not known until vertex scan time). Result: G-code + // coords emerge offset by the un-undone Z-shift — the inverse rotation + // couples slicer_z back into both machine_y and machine_z. Compensating + // layer.print_z by belt_z_shift here makes the back-transform produce + // correct machine-frame coordinates whether or not a global mode is active. + // The compensation must mirror the Z-shift actually applied, which + // is max(0, -m_belt_min_z): when the transformed mesh starts ABOVE + // slicer Z=0 (m_belt_min_z > 0 — possible for counter-rotated or + // asymmetric geometry whose centered-frame minimum lands positive) + // no lift was applied, and an unclamped m_belt_min_z here would + // leak straight into the layer Z values, floating the whole object + // off the belt by exactly that amount. + double belt_z_shift = std::min(m_belt_min_z, 0.); // the belt surface is Z=0 in centered slicer space + double global_z_offset = belt_z_shift; + + // Centering correction: trafo_centered pretranslates by + // -m_center_offset.{x,y}. Under the belt forward transform, the + // Y component of that pretranslate couples into slicer-Z (shear: + // tan*c.y, rotation: sin*c.y). BeltBackTransform inverts the + // rotation/shear but doesn't undo centering, so this Z component + // leaks into machine output as a position offset whenever + // m_center_offset != 0. When a user moves a volume within an + // assembly such that the combined bbox center shifts, this shows + // up as a small Z translation in the print. Compensate by adding + // the Z component of the centering through the forward transform. + { + Transform3d T_fwd = BeltTransformPipeline::build_forward_transform(pcfg); + Vec3d c_off(unscale(m_center_offset.x()), + unscale(m_center_offset.y()), + 0.); + double centering_z_corr = (T_fwd.linear() * c_off).z(); + global_z_offset += centering_z_corr; + } + + { + // Global pre-slice mode: compute full correction c = (T.linear() - I) * d + // where T is the belt forward transform and d is the bed position, so + // objects at different bed positions print at different machine Z values + // along the inclined belt. + Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg); + Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); + Vec3d c = T.linear() * d - d; + global_z_offset += c.z(); + + BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" + << c.x() << ", " << c.y() << ", " << c.z() << ")" + << " belt_z_shift=" << belt_z_shift << " (m_belt_min_z=" << m_belt_min_z << ")"; + } + + BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset + << " (" << this->print()->objects().size() << " objects on the plate)"; + m_belt_global_z_offset = global_z_offset; + if (std::abs(global_z_offset) > EPSILON) { + for (Layer *layer : m_layers) + layer->print_z += global_z_offset; + // Keep belt floor clipping in sync with the shifted print_z + // values — the support generator sees globally-offset object + // layer print_z, so belt_floor_z_shift must match. + m_slicing_params.belt_floor_z_shift += global_z_offset; + } + if (!m_layers.empty()) { + BOOST_LOG_TRIVIAL(trace) << "Belt global: first_layer_z=" << m_layers.front()->print_z + << " last_layer_z=" << m_layers.back()->print_z + << " num_layers=" << m_layers.size() + << " center_offset=(" << unscale(m_center_offset.x()) + << ", " << unscale(m_center_offset.y()) << ")"; + } + + // Cache the final patched belt_floor_z_shift so a later support-only + // invalidation can rebuild m_slicing_params without losing this exact + // (vertex-scan-derived) value. update_slicing_parameters() will + // restore it after create_from_config() seeds the bbox approximation. + m_belt_floor_z_shift_cached = m_slicing_params.belt_floor_z_shift; + m_belt_floor_z_shift_cache_valid = true; + } + } + // BBS this->set_done(posSlice); } @@ -1172,6 +1328,7 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro }); // end of parallel_for } + // 1) Decides Z positions of the layers, // 2) Initializes layers and their regions // 3) Slices the object meshes @@ -1205,7 +1362,8 @@ void PrintObject::slice_volumes() if (!slice_zs.empty()) { objSliceByVolume = slice_volumes_inner( print->config(), this->config(), this->trafo_centered(), - this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback); + this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback, + &m_belt_min_z); } //BBS: "model_part" volumes are grouded according to their connections @@ -1577,6 +1735,13 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_ return union_ex(new_ex_polys); } +Transform3d PrintObject::trafo_sliced() const +{ + Transform3d trafo = this->trafo_centered(); + BeltSliceStrategy::apply_preslice_transforms(trafo, this->print()->config(), this->model_object()->volumes); + return trafo; +} + std::vector PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const { auto it_volume = this->model_object()->volumes.begin(); @@ -1591,7 +1756,7 @@ std::vector PrintObject::slice_support_volumes(const ModelVolumeType m const Print *print = this->print(); auto throw_on_cancel_callback = std::function([print](){ print->throw_if_canceled(); }); MeshSlicingParamsEx params; - params.trafo = this->trafo_centered(); + params.trafo = this->trafo_sliced(); for (; it_volume != it_volume_end; ++ it_volume) if ((*it_volume)->type() == model_volume_type) { std::vector slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback); @@ -1634,10 +1799,11 @@ std::vector PrintObject::slice_single_volume_regions(const ModelVolu { if (volume == nullptr) return {}; - // Match the existing slicing heights and centered transform without flattening holes. + // Match the existing slicing heights and the frame the layers were sliced in (belt + // pre-slice transforms included) without flattening holes. const std::vector zs = zs_from_layers(this->layers()); MeshSlicingParamsEx params; - params.trafo = this->trafo_centered(); + params.trafo = this->trafo_sliced(); const Print *print = this->print(); return slice_volume(*volume, zs, params, [print]() { print->throw_if_canceled(); }); } diff --git a/src/libslic3r/Slicing.hpp b/src/libslic3r/Slicing.hpp index 59ae27c8dd..a9bb4937ea 100644 --- a/src/libslic3r/Slicing.hpp +++ b/src/libslic3r/Slicing.hpp @@ -113,6 +113,13 @@ struct SlicingParameters coordf_t object_print_z_uncompensated_max { 0 }; // Scaling factor for compensating shrinkage in Z-axis. coordf_t object_shrinkage_compensation_z { 0 }; + + // Belt printer: floor plane parameters for support clipping. + // Belt contact surface in slicing coords: Z = bb_min_z + sf*Y + slicing_z_shift. + // cutoff = (print_z - belt_floor_z_shift - floor_offset) / shear_factor + double belt_floor_shear_factor { 0.0 }; // shear factor (e.g. cot(45deg)) + int belt_floor_from_axis { 1 }; // which axis the shear is from (0=X, 1=Y) + double belt_floor_z_shift { 0.0 }; // bb_min_z + max(0, -min_z_after_shear) }; static_assert(IsTriviallyCopyable::value, "SlicingParameters class is not POD (and it should be - see constructor)."); diff --git a/src/libslic3r/Support/BeltFloorContext.cpp b/src/libslic3r/Support/BeltFloorContext.cpp new file mode 100644 index 0000000000..26078413dd --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.cpp @@ -0,0 +1,132 @@ +#include "BeltFloorContext.hpp" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../PrintConfig.hpp" +#include "../Slicing.hpp" +#include "../libslic3r.h" + +#include +#include +#include +#include +#include + +namespace Slic3r { + +bool BeltFloorContext::init(const SlicingParameters &sp, const PrintConfig &pcfg) +{ + m_active = false; + m_shear_factor = sp.belt_floor_shear_factor; + m_from_axis = sp.belt_floor_from_axis; + m_z_shift = sp.belt_floor_z_shift; + m_floor_offset = pcfg.belt_support_floor_offset.value; + + if (std::abs(m_shear_factor) < EPSILON) + return false; + + m_active = true; + return true; +} + +bool BeltFloorContext::init_local(const SlicingParameters &sp, const PrintConfig &pcfg, + double global_z_offset) +{ + if (!init(sp, pcfg)) + return false; + // Local Z: subtract the global Z offset so polygon computation + // works in the object's local coordinate space. + m_z_shift -= global_z_offset; + return true; +} + +Polygons BeltFloorContext::surface_polygon(coordf_t print_z) const +{ + return half_plane(print_z, /*belt_surface=*/true); +} + +Polygons BeltFloorContext::valid_region_polygon(coordf_t print_z) const +{ + return half_plane(print_z, /*belt_surface=*/false); +} + +double BeltFloorContext::floor_print_z(const Point &pos_slicing) const +{ + if (!m_active) + return -std::numeric_limits::infinity(); + double pos = unscale(m_from_axis == 0 ? pos_slicing.x() : pos_slicing.y()); + return m_shear_factor * pos + m_floor_offset + m_z_shift; +} + +std::vector BeltFloorContext::compute_per_layer_floors( + size_t num_layers, + const std::function &layer_print_z) const +{ + std::vector result(num_layers); + if (!m_active) + return result; + for (size_t i = 0; i < num_layers; ++i) + result[i] = surface_polygon(layer_print_z(i)); + return result; +} + +Polygons BeltFloorContext::half_plane(coordf_t print_z, bool belt_surface) const +{ + if (!m_active) + return {}; + + const double cutoff = this->cutoff_u(print_z); + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e3); + + // The belt surface is on one side of the cutoff line; the valid region + // is on the other side. Which side depends on shear_factor sign. + // + // belt_surface=true → the belt side (where support should NOT exist) + // belt_surface=false → the valid side (where support IS allowed) + // + // For shear_factor > 0: belt surface is from_axis >= cutoff + // For shear_factor < 0: belt surface is from_axis <= cutoff + bool high_side = (m_shear_factor > 0) == belt_surface; + + Polygon poly; + if (m_from_axis == 0) { + if (high_side) { + // X >= cutoff + poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } else { + // X < cutoff + poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } + } else { + if (high_side) { + // Y >= cutoff + poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Y < cutoff + poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } + } + return { poly }; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Support/BeltFloorContext.hpp b/src/libslic3r/Support/BeltFloorContext.hpp new file mode 100644 index 0000000000..629dfce4b6 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Slicing.hpp" +#include "../PrintConfig.hpp" +#include +#include +#include + +namespace Slic3r { + +class PrintObject; + +// Belt floor context: encapsulates the parameters and polygon computation +// for belt printer floor clipping in support generation. +// +// All belt floor code across SupportMaterial, TreeSupport, TreeSupport3D, +// and TreeModelVolumes uses the same 4 parameters and the same 4-case +// polygon construction. This class consolidates that logic. +// +// Construct once per PrintObject, then call surface_polygon() or +// valid_region_polygon() per layer with the layer's print_z. +class BeltFloorContext +{ +public: + BeltFloorContext() = default; + + // Initialize from slicing parameters and print config. + // Uses global Z coordinates (for SupportMaterial, non-organic TreeSupport). + bool init(const SlicingParameters &sp, const PrintConfig &pcfg); + + // Initialize with a Z offset subtracted from z_shift. + // Uses local Z coordinates (for TreeSupport3D, TreeModelVolumes organic pipeline). + bool init_local(const SlicingParameters &sp, const PrintConfig &pcfg, + double global_z_offset); + + bool is_active() const { return m_active; } + + // Compute the belt-side half-plane polygon at a given print_z. + // This is the region where the belt surface exists. + Polygons surface_polygon(coordf_t print_z) const; + + // Compute the valid-region half-plane polygon at a given print_z. + // This is the complement: the region where support is allowed. + Polygons valid_region_polygon(coordf_t print_z) const; + + // Compute the belt floor Z position at a given XY position (in slicing coords). + // Returns -infinity if not active. + double floor_print_z(const Point &pos_slicing) const; + + // The from_axis coordinate (unscaled, slicing frame) where the belt surface + // crosses a horizontal plane at print_z. Inverse of floor_print_z() along + // the shear axis. Only meaningful when is_active(). + coordf_t cutoff_u(coordf_t print_z) const + { return (print_z - m_z_shift - m_floor_offset) / m_shear_factor; } + + // Pre-compute belt floor polygons for a range of layers. + // layer_print_z(i) returns the print_z for layer index i. + std::vector compute_per_layer_floors( + size_t num_layers, + const std::function &layer_print_z) const; + + // Accessors + double shear_factor() const { return m_shear_factor; } + int from_axis() const { return m_from_axis; } + double z_shift() const { return m_z_shift; } + double floor_offset() const { return m_floor_offset; } + +private: + bool m_active = false; + double m_shear_factor = 0.0; + int m_from_axis = 1; // 0=X, 1=Y + double m_z_shift = 0.0; + double m_floor_offset = 0.0; + + // Internal: compute the raw half-plane polygon. + // If belt_surface=true, returns the belt side; otherwise the valid (complement) side. + Polygons half_plane(coordf_t print_z, bool belt_surface) const; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 59463b4cc6..481213fe48 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -290,7 +290,9 @@ SupportGeneratorLayersPtr generate_raft_base( // The object does not have a raft. // Calculate the area covered by the brim. const BrimType brim_type = object.config().brim_type; - const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner; + // btLeadingEdgeOnly only means anything on a belt printer, where this code path + // does not run; elsewhere it degrades to an outer brim (see Brim.cpp). + const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly; const bool brim_inner = brim_type == btInnerOnly || brim_type == btOuterAndInner; // BBS: the pattern of raft and brim are the same, thus the brim can be serpated by support raft. const auto brim_object_gap = scaled(object.config().brim_object_gap.value); @@ -318,7 +320,13 @@ SupportGeneratorLayersPtr generate_raft_base( // How much to inflate the support columns to be stable. This also applies to the 1st layer, if no raft layers are to be printed. const float inflate_factor_fine = float(scale_((slicing_params.raft_layers() > 1) ? 0.5 : EPSILON)); - const float inflate_factor_1st_layer = std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine); + // On a belt the first support layer is the leading tip of the support, a sliver + // where the belt crosses the layer, not a flange on a flat bed: inflating it + // puts lines in the air ahead of the belt crossing (and into the belt behind + // it). The belt brim takes the adhesion role instead. + const bool belt_floor_active = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON; + const float inflate_factor_1st_layer = belt_floor_active ? 0.f : + std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine); SupportGeneratorLayer *contacts = top_contacts .empty() ? nullptr : top_contacts .front(); SupportGeneratorLayer *interfaces = interface_layers .empty() ? nullptr : interface_layers .front(); SupportGeneratorLayer *base_interfaces = base_interface_layers.empty() ? nullptr : base_interface_layers.front(); @@ -1795,7 +1803,14 @@ void generate_support_toolpaths( bool sheath = support_params.with_sheath; bool no_sort = false; bool done = false; - if (base_layer.layer->bottom_z < EPSILON) { + // Belt printers have no flat bed first layer — the belt is the tilted + // build surface — so the dense raft_first_layer_density flange must not + // fire anywhere, including the layer at z=0 (the belt-surface line). + // (belt_floor_shear_factor is non-zero only when belt_printer is on.) + // For every other printer type, support z is never negative, so this + // matches the original "first layer at z=0" behaviour unchanged. + const bool is_belt_printer = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON; + if (! is_belt_printer && base_layer.layer->bottom_z < EPSILON) { // Base flange (the 1st layer). filler = filler_first_layer; filler->angle = Geometry::deg2rad(float(config.support_angle.value + 90.)); @@ -2053,4 +2068,10 @@ sub clip_with_shape { } */ +Vec2d build_plate_tilt_slope(const PrintConfig &print_config) +{ + auto slope = [](double tilt_deg) { return std::tan(Geometry::deg2rad(std::clamp(tilt_deg, -89., 89.))); }; + return { slope(print_config.build_plate_tilt_y.value), slope(print_config.build_plate_tilt_x.value) }; +} + } // namespace Slic3r diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index c32efd4697..ca91ecc5ae 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -7,6 +7,8 @@ #include "libslic3r/ExtrusionEntity.hpp" #include "SupportLayer.hpp" #include "SupportParameters.hpp" +#include "../Point.hpp" +#include "../libslic3r.h" #include #include #include @@ -152,6 +154,16 @@ int idx_lower_or_equal(const std::vector &vec, int idx, FN_LOWER_EQUAL fn_lo return idx_lower_or_equal(vec.begin(), vec.end(), idx, fn_lower_equal); } +// Belt floor: compute the belt-side half-plane polygon at a given print_z. +// Used to clip support polygons against the belt surface. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); + +// Build plate tilt: XY drift of gravity per unit of layer height, zero on a level plate. +// The tilt is capped below 90 degrees to keep the drift finite. +Vec2d build_plate_tilt_slope(const PrintConfig &print_config); + } // namespace Slic3r #endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index cfd259501f..813acecec4 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -12,6 +12,7 @@ #include "libslic3r/Support/SupportLayer.hpp" #include "libslic3r/PrintConfig.hpp" #include "SupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "Geometry.hpp" #include "Point.hpp" #include "MutablePolygon.hpp" @@ -397,10 +398,21 @@ inline void layers_append(SupportGeneratorLayersPtr &dst, const SupportGenerator } // Support layer that is covered by some form of dense interface. -static constexpr const std::initializer_list support_types_interface { +static constexpr const std::initializer_list support_types_interface { SupporLayerType::RaftInterface, SupporLayerType::BottomContact, SupporLayerType::BottomInterface, SupporLayerType::TopContact, SupporLayerType::TopInterface }; +// Forward declarations for belt floor helpers (defined later in this file). +// belt_floor_surface_polygon is declared in SupportCommon.hpp (non-static, +// shared with TreeSupport.cpp). + +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, SupportGeneratorLayersPtr &support_layers); + void PrintObjectSupportMaterial::generate(PrintObject &object) { BOOST_LOG_TRIVIAL(info) << "Support generator - Start"; @@ -473,11 +485,12 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) object, bottom_contacts, top_contacts, layer_storage); this->trim_support_layers_by_object(object, top_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, top_contacts); #ifdef SLIC3R_DEBUG for (const SupportGeneratorLayer *layer : top_contacts) Slic3r::SVG::export_expolygons( - debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), + debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), union_ex(layer->polygons)); #endif @@ -633,6 +646,37 @@ Polygons collect_region_slices_by_type(const Layer &layer, SurfaceType surface_t return out; } +// Belt printer: compute the belt-side half-plane polygon at a given print_z. +// This represents the region where the belt surface exists (the "phantom top surface"). +// Support that overlaps with this polygon should terminate with a bottom contact. +// Returns empty if belt floor is not active. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) + return {}; + return ctx.surface_polygon(print_z); +} + +// Belt printer: compute the valid-region half-plane polygon at a given print_z. +// This is the region where support is allowed to exist (above the belt). +// Used to clip the downward-propagating support projection. +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) + return {}; + return ctx.valid_region_polygon(print_z); +} + // Collect outer contours of all slices of this layer. // This is useful for calculating the support base with holes filled. Polygons collect_slices_outer(const Layer &layer) @@ -1424,6 +1468,9 @@ static inline ExPolygons detect_overhangs( const double threshold_rad = Geometry::deg2rad(thresh_angle); const bool bridge_no_support = object_config.bridge_no_support.value; const coordf_t xy_expansion = scale_(object_config.support_expansion.value); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const Vec2d tilt_slope = build_plate_tilt_slope(print_config); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; float lower_layer_offset = 0; if (layer_id == 0) @@ -1457,6 +1504,16 @@ static inline ExPolygons detect_overhangs( } } + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity. + // This is loop-invariant across regions, so compute it once here. + const Polygons *effective_lower = &lower_layer_polygons; + Polygons tilted_lower; + if (has_tilt) { + tilted_lower = lower_layer_polygons; + translate(tilted_lower, Point::new_scale(tilt_slope * lower_layer.height)); + effective_lower = &tilted_lower; + } + for (LayerRegion *layerm : layer.regions()) { // Extrusion width accounts for the roundings of the extrudates. // It is the maximum widh of the extrudate. @@ -1475,7 +1532,7 @@ static inline ExPolygons detect_overhangs( Polygons layerm_polygons = to_polygons(layerm->slices.surfaces); if (lower_layer_offset == 0.f) { // Support everything. - diff_polygons = diff(layerm_polygons, lower_layer_polygons); + diff_polygons = diff(layerm_polygons, *effective_lower); if (buildplate_only) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1484,9 +1541,9 @@ static inline ExPolygons detect_overhangs( } else if (auto_normal_support) { // Get the regions needing a suport, collapse very tiny spots. //FIXME cache the lower layer offset if this layer has multiple regions. - diff_polygons = + diff_polygons = diff(layerm_polygons, - expand(lower_layer_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + expand(*effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS)); if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) { // Don't support overhangs above the top surfaces. // This step is done before the contact surface is calculated by growing the overhang region. @@ -1494,9 +1551,9 @@ static inline ExPolygons detect_overhangs( } if (! diff_polygons.empty()) { // Offset the support regions back to a full overhang, restrict them to the full overhang. - // This is done to increase size of the supporting columns below, as they are calculated by + // This is done to increase size of the supporting columns below, as they are calculated by // propagating these contact surfaces downwards. - diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), lower_layer_polygons); + diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), *effective_lower); } //FIXME add user defined filtering here based on minimal area or minimum radius or whatever. @@ -2536,6 +2593,82 @@ static inline SupportGeneratorLayer* detect_bottom_contacts( return &layer_new; } +// Belt printer: detect bottom contacts where support meets the belt floor plane. +// Modeled on detect_bottom_contacts() but uses the belt plane polygon instead of stTop surfaces. +static inline SupportGeneratorLayer* detect_belt_floor_bottom_contacts( + const SlicingParameters &slicing_params, + const SupportParameters &support_params, + const PrintConfig &print_config, + const PrintObject &object, + const Layer &layer, + // Existing top contact layers, for snapping. + const SupportGeneratorLayersPtr &top_contacts, + size_t contact_idx, + SupportGeneratorLayerStorage &layer_storage, + std::vector &layer_support_areas, + const Polygons &supports_projected) +{ + // Compute the belt surface polygon at this layer's Z. + Polygons belt_surface = belt_floor_surface_polygon(slicing_params, print_config, object, layer.print_z); + if (belt_surface.empty()) + return nullptr; + + // Find where projected support overlaps the belt surface. + Polygons touching = intersection(belt_surface, supports_projected); + if (touching.empty()) + return nullptr; + + assert(layer.id() >= slicing_params.raft_layers()); + size_t layer_id = layer.id() - slicing_params.raft_layers(); + + // Allocate a new bottom contact layer resting on the belt plane. + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::BottomContact); + + // No object layer to sync with -- compute heights directly from flow parameters. + layer_new.height = support_params.support_material_bottom_interface_flow.height(); + layer_new.print_z = layer.print_z + layer_new.height + slicing_params.gap_object_support; + layer_new.bottom_z = layer.print_z; + layer_new.idx_object_layer_below = layer_id; + layer_new.bridging = ! slicing_params.zero_gap_interface_bottom && object.config().thick_bridges; + layer_new.polygons = expand(touching, float(support_params.support_material_flow.scaled_width()), SUPPORT_SURFACES_OFFSET_PARAMETERS); + + if (! slicing_params.zero_gap_interface_bottom) { + // Snap to nearby top contact layers to avoid very thin support layers. + for (size_t top_idx = size_t(std::max(0, int(contact_idx))); + top_idx < top_contacts.size() && top_contacts[top_idx]->print_z < layer_new.print_z + support_params.support_layer_height_min + EPSILON; + ++ top_idx) { + if (top_contacts[top_idx]->print_z > layer_new.print_z - support_params.support_layer_height_min - EPSILON) { + coordf_t diff = layer_new.print_z - top_contacts[top_idx]->print_z; + assert(std::abs(diff) <= support_params.support_layer_height_min + EPSILON); + if (diff > 0.F) { + if (layer_new.height - diff > support_params.support_layer_height_min) { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } else { + continue; + } + } else { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } + break; + } + } + } + + // Trim the already created base layers above this belt contact. + touching = expand(touching, float(SCALED_EPSILON)); + for (int layer_id_above = int(layer_id) + 1; layer_id_above < int(object.total_layer_count()); ++ layer_id_above) { + const Layer &layer_above = *object.layers()[layer_id_above]; + if (layer_above.print_z > layer_new.print_z - EPSILON) + break; + if (Polygons &above = layer_support_areas[layer_id_above]; ! above.empty()) + above = diff(above, touching); + } + + return &layer_new; +} + // Returns polygons to print + polygons to propagate downwards. // Called twice: First for normal supports, possibly trimmed by "on build plate only", second for support enforcers not trimmed by "on build plate only". static inline std::pair project_support_to_grid(const Layer &layer, const SupportGridParams &grid_params, const Polygons &overhangs, Polygons *layer_buildplate_covered @@ -2635,6 +2768,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ //const auto expansion_to_slice = m_support_material_flow.scaled_spacing() / 2 + 25; const SupportGridParams grid_params(*m_object_config, m_support_params.support_material_flow); const bool buildplate_only = ! buildplate_covered.empty(); + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON; // Allocate empty surface areas, one per object layer. layer_support_areas.assign(object.total_layer_count(), Polygons()); @@ -2695,8 +2829,9 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ tbb::task_group task_group; const Polygons &overhangs_for_bottom_contacts = buildplate_only ? enforcers_projection_raw : overhangs_projection_raw; if (! overhangs_for_bottom_contacts.empty()) - // Find the bottom contact layers above the top surfaces of this layer. - task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts + // Find the bottom contact layers above the top surfaces of this layer, + // and also detect belt floor contacts if belt mode is active. + task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts, has_belt_floor #ifdef SLIC3R_DEBUG , iRun, &polygons_new #endif // SLIC3R_DEBUG @@ -2710,6 +2845,15 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ ); if (layer_new) bottom_contacts.push_back(layer_new); + // Belt floor phantom surface: detect where support meets the belt plane. + if (has_belt_floor) { + SupportGeneratorLayer *belt_layer = detect_belt_floor_bottom_contacts( + m_slicing_params, m_support_params, *m_print_config, object, + layer, top_contacts, contact_idx, layer_storage, + layer_support_areas, overhangs_for_bottom_contacts); + if (belt_layer) + bottom_contacts.push_back(belt_layer); + } }); Polygons &layer_support_area = layer_support_areas[layer_id]; @@ -2748,6 +2892,23 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ task_group.wait(); + // Belt floor: clip projections and support areas so support doesn't + // propagate below the belt plane. + if (has_belt_floor) { + Polygons valid_region = belt_floor_valid_region_polygon( + m_slicing_params, *m_print_config, object, layer.print_z); + if (! valid_region.empty()) { + if (! overhangs_projection.empty()) + overhangs_projection = intersection(overhangs_projection, valid_region); + if (! enforcers_projection.empty()) + enforcers_projection = intersection(enforcers_projection, valid_region); + if (! layer_support_area.empty()) + layer_support_area = intersection(layer_support_area, valid_region); + if (! layer_support_area_enforcers.empty()) + layer_support_area_enforcers = intersection(layer_support_area_enforcers, valid_region); + } + } + if (! layer_support_area_enforcers.empty()) { if (layer_support_area.empty()) layer_support_area = std::move(layer_support_area_enforcers); @@ -2758,6 +2919,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ std::reverse(bottom_contacts.begin(), bottom_contacts.end()); trim_support_layers_by_object(object, bottom_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, bottom_contacts); return bottom_contacts; } @@ -3131,6 +3293,31 @@ void PrintObjectSupportMaterial::generate_base_layers( #endif /* SLIC3R_DEBUG */ this->trim_support_layers_by_object(object, intermediate_layers, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, intermediate_layers); +} + +// Belt printer: trim support layer polygons by the belt floor plane. +// For each support layer, computes the belt floor half-plane at that layer's print_z +// and subtracts it from the support polygons. This follows the same diff() pattern +// as trim_support_layers_by_object() so that interface layers derived from trimmed +// intermediates automatically inherit the belt floor trimming. +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + SupportGeneratorLayersPtr &support_layers) +{ + BeltFloorContext ctx; + if (!ctx.init(slicing_params, print_config)) + return; + + tbb::parallel_for(tbb::blocked_range(0, support_layers.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) + if (support_layers[i]) + support_layers[i]->polygons = diff(support_layers[i]->polygons, + ctx.surface_polygon(support_layers[i]->print_z)); + }); } void PrintObjectSupportMaterial::trim_support_layers_by_object( diff --git a/src/libslic3r/Support/SupportSpotsGenerator.cpp b/src/libslic3r/Support/SupportSpotsGenerator.cpp index 740521df78..539c174f91 100644 --- a/src/libslic3r/Support/SupportSpotsGenerator.cpp +++ b/src/libslic3r/Support/SupportSpotsGenerator.cpp @@ -752,7 +752,9 @@ std::tuple build_object_part_from_slice(const size_t &slice_i // thus has lower adhesion. For now this effect will be neglected. ExPolygon slice_poly = layer->lslices[slice_idx]; ExPolygons brim; - if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly) { + // btLeadingEdgeOnly degrades to an outer brim off belt printers (see Brim.cpp). + if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly + || params.brim_type == BrimType::btLeadingEdgeOnly) { Polygon brim_hole = slice_poly.contour; brim_hole.reverse(); Polygons c = expand(slice_poly.contour, scale_(params.brim_width)); // For very small polygons, the expand may result in empty vector, even thought the input is correct. diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index d1d1b4d8cc..33d379c2b5 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -10,6 +10,7 @@ #include "libslic3r/Polygon.hpp" #include "libslic3r/ExPolygon.hpp" #include "TreeSupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "../BuildVolume.hpp" #include "../ClipperUtils.hpp" @@ -19,6 +20,7 @@ #include "../Utils.hpp" #include "../format.hpp" #include "libslic3r/libslic3r.h" +#include "../PrintConfig.hpp" #include #include @@ -115,6 +117,21 @@ TreeModelVolumes::TreeModelVolumes( m_increase_until_radius = config.increase_radius_until_radius; m_radius_0 = config.getRadius(0); m_raft_layers = config.raft_layers; + // Support blockers are consumed in the same index space as m_layer_outlines + // (object layer i lives at index num_raft_layers + i), but + // slice_support_blockers() returns them in object-layer space. Shift them. + // + // The belt surface is deliberately NOT a blocker. A blocker is a collision, + // and a branch descending onto a collision slides off it: on a belt that + // walks the branch down the tilted surface, ahead of the part, until it + // reaches the bottom layer floating in mid-air. The belt is where branches + // END: organic_draw_branches() clips their slices with m_belt_floor and the + // first clipped slice is the contact. + { + const size_t num_raft = m_raft_layers.size(); + if (num_raft > 0 && ! m_anti_overhang.empty()) + m_anti_overhang.insert(m_anti_overhang.begin(), num_raft, Polygons{}); + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); @@ -127,6 +144,32 @@ TreeModelVolumes::TreeModelVolumes( for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution); }); + + // Belt floor: pre-compute belt surface polygon per-layer for clipping. + // Branches grow toward the belt and their slices are clipped at the belt + // surface in organic_draw_branches(). The organic pipeline works in LOCAL + // Z (no global_z_offset), so use local z_shift and local print_z. + { + const auto &slicing_params = print_object.slicing_parameters(); + const auto &pcfg2 = print_object.print()->config(); + BeltFloorContext ctx; + ctx.init_local(slicing_params, pcfg2, print_object.belt_global_z_offset()); + if (ctx.is_active()) { + m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double { + // Object layers: local print_z (subtract global offset). + if (layer_idx >= num_raft_layers) + return print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset(); + // Belt raft layers (below the object): each carries its own + // local print_z in m_raft_layers. The belt floor is a tilted + // plane, so the half-plane to clip grows as print_z drops — + // using 0 here clipped every below-object layer against the + // Z=0 belt surface, under-clipping the raft region and leaving + // a dense support mass below the floor. + return (layer_idx < m_raft_layers.size()) ? m_raft_layers[layer_idx] : 0.; + }); + } + } } #endif @@ -484,7 +527,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // 2) Sum over top / bottom ranges. const bool processing_last_mesh = outline_idx == layer_outline_indices.back(); tbb::parallel_for(tbb::blocked_range(data.begin(), data.end()), - [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, + [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel] (const tbb::blocked_range& range) { for (LayerIndex layer_idx = range.begin(); layer_idx != range.end(); ++ layer_idx) { @@ -530,9 +573,14 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // not support an overhang<90 degree than to risk fusing to it. append(collisions, offset(union_ex(collision_areas_original), radius + required_range_x, jtMiter, 1.2)); } - collisions = processing_last_mesh && layer_idx < int(anti_overhang.size()) ? - union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, jtMiter, 1.2)) : - union_(collisions); + if (processing_last_mesh) { + if (layer_idx < int(anti_overhang.size())) + append(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, jtMiter, 1.2)); + // NOTE: m_belt_floor is NOT added to collision here — branches + // should grow toward the belt and terminate at it, not avoid it. + // Belt floor clipping is done post-generation in organic_draw_branches(). + } + collisions = union_(collisions); auto &dst = data[layer_idx]; if (processing_last_mesh) { if (! dst.empty()) diff --git a/src/libslic3r/Support/TreeModelVolumes.hpp b/src/libslic3r/Support/TreeModelVolumes.hpp index 36f5de9fd4..716ef90c9b 100644 --- a/src/libslic3r/Support/TreeModelVolumes.hpp +++ b/src/libslic3r/Support/TreeModelVolumes.hpp @@ -176,6 +176,9 @@ public: } Polygon m_bed_area; + // Belt floor polygons per layer — used for post-generation clipping + // in organic_draw_branches(). Public so the organic pipeline can access it. + std::vector m_belt_floor; private: // Caching polygons for a range of layers. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index ada9310dda..b1d0e0d1e8 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -38,6 +38,7 @@ #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" #include "TreeSupport3D.hpp" +#include "BeltFloorContext.hpp" #include "libslic3r/libslic3r.h" #include #include @@ -707,6 +708,33 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p } +double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const +{ + BeltFloorContext ctx; + if (!ctx.init(m_slicing_params, *m_print_config)) + return -std::numeric_limits::infinity(); + return ctx.floor_print_z(pos_slicing); +} + +bool TreeSupport::belt_node_landed(const Point &pos_slicing, double radius, double print_z) const +{ + BeltFloorContext ctx; + if (!ctx.init(m_slicing_params, *m_print_config)) + return false; + return print_z <= ctx.floor_print_z(pos_slicing) - std::abs(ctx.shear_factor()) * std::max(0., radius); +} + +bool TreeSupport::belt_polygon_landed(const ExPolygon &poly, double print_z) const +{ + BeltFloorContext ctx; + if (!ctx.init(m_slicing_params, *m_print_config)) + return false; + double min_floor = std::numeric_limits::max(); + for (const Point &pt : poly.contour.points) + min_floor = std::min(min_floor, ctx.floor_print_z(pt)); + return print_z <= min_floor; +} + #define SUPPORT_SURFACES_OFFSET_PARAMETERS jtSquare, 0. void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) { @@ -741,6 +769,21 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) double thresh_angle = config.support_threshold_angle.value > EPSILON ? config.support_threshold_angle.value + 1 : 30; thresh_angle = std::min(thresh_angle, 89.); // should be smaller than 90 const double threshold_rad = Geometry::deg2rad(thresh_angle); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const PrintConfig& print_cfg = m_object->print()->config(); + const Vec2d tilt_slope = build_plate_tilt_slope(print_cfg); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; + + // Belt printers: the object is pre-rotated by the belt angle before slicing, so a wall + // that is vertical in the world advances by one layer height per layer in the sliced + // frame. The build-plate tilt shift above compensates for that, but its direction has to + // follow the belt shear -- the sign and axis are already known exactly from the slicing + // parameters, so take them from there rather than from tan(build_plate_tilt), which + // carries a magnitude but no direction. Non-belt tilted beds keep the existing behaviour. + BeltFloorContext ovh_belt_ctx; + const bool belt_ovh_active = ovh_belt_ctx.init(m_slicing_params, print_cfg); + const double belt_shear = ovh_belt_ctx.shear_factor(); + const int belt_axis = ovh_belt_ctx.from_axis(); // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -883,8 +926,64 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons& curr_polys = layer->lslices_extrudable; ExPolygons& lower_polys = lower_layer->lslices_extrudable; + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + // + // On a belt the object's very first slice can come out empty (the bottom + // vertex is a sub-extrudable sliver), leaving the layer above it with an + // empty predecessor even though it rests on the belt. That case needs no + // special handling here: the belt surface is unioned into effective_lower + // below and sampled at the bottom of the layer, so a contacting island is + // covered and a genuinely floating one still reports its overhang. Doing it + // that way keeps the decision per-island -- an earlier whole-layer skip, + // conditioned on the nearest point of the *union* of the cross-section, + // let one contacting island silence a separate floating one. + ExPolygons shifted_lower; + if (belt_ovh_active || has_tilt) { + shifted_lower = lower_polys; // copy + const double lh = lower_layer->height; + Point tilt_shift(0, 0); + if (belt_ovh_active) { + // Advance the lower layer along the belt by exactly the amount a + // world-vertical wall moves per layer, so such a wall stops reading + // as an overhang. Sign comes from the shear, not from a tilt angle. + const coord_t d = coord_t(-scale_(lh * belt_shear)); + if (belt_axis == 0) tilt_shift.x() = d; else tilt_shift.y() = d; + } else { + tilt_shift = Point::new_scale(tilt_slope * lh); + } + translate(shifted_lower, tilt_shift); + } + ExPolygons effective_lower = (belt_ovh_active || has_tilt) ? shifted_lower : lower_polys; + + // Belt printers: material resting on the belt is held up by the belt, not by + // the layer below it, so the belt surface counts as support from underneath. + // Without this the object's belt-contact face reads as a fresh overhang on + // every layer -- the leading strip that produced the spurious support nub. + if (belt_ovh_active) { + // surface_polygon() is a +/-1000mm half-plane. Unioning that raw with + // 20mm-scale geometry and then offsetting it puts a huge dynamic range + // through Clipper, which left intermittent artefacts every few layers. + // Clip it to the layer's own bounding box first. + // Evaluate the belt surface at the BOTTOM of the layer, not its top: + // a layer meets the belt across its whole thickness, and print_z is the + // top. On the object's first layer -- which is thicker, and whose lower + // layer is empty -- using print_z left the leading 0.37mm uncovered and + // produced the one remaining spurious overhang. + Polygons belt_surface = ovh_belt_ctx.surface_polygon(layer->print_z - layer->height); + if (! belt_surface.empty()) { + BoundingBox clip_bb = get_extents(curr_polys); + clip_bb.merge(get_extents(lower_polys)); + clip_bb.offset(scale_(10.)); + belt_surface = intersection(belt_surface, Polygons{ clip_bb.polygon() }); + if (! belt_surface.empty()) { + append(effective_lower, union_ex(belt_surface)); + effective_lower = union_ex(effective_lower); + } + } + } + // normal overhang - ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); + ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); overhangs_all_layers[layer_nr] = diff_ex(curr_polys, lower_layer_offseted); double duration{ std::chrono::duration_cast(clock_::now() - t0).count() }; @@ -900,8 +999,13 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) for (const ExPolygon& expoly : curr_polys) { bool is_sharp_tail = false; // 1. nothing below - // this is a sharp tail region if it's floating and non-ignorable - if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), lower_polys)) { + // this is a sharp tail region if it's floating and non-ignorable. + // On a belt, "below" has to include the belt itself and the + // shear-advanced lower layer, or every belt-contact island reads as + // a sharp tail -- which is what the empty-predecessor skip above was + // really masking. effective_lower is exactly that notion of below. + const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys; + if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) { is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty(); } @@ -1524,6 +1628,15 @@ void TreeSupport::generate_toolpaths() // ORCA: base angle used for explicit interlaced interface orientation. const float base_support_angle = Geometry::deg2rad(object_config.support_angle.value); + // Belt floor: the lowest support layer rests on the moving, tilted belt, not + // on a flat bed — so it must NOT get the bed first-layer treatment (a brim on + // interface areas, a first-layer-flow sheath at raft_first_layer_density on + // base areas). That treatment draws a loop along the Z=0 belt-floor line that + // reads as a stray brim/skirt. Gate those layer_id==0 special cases off when + // the belt floor is active; false on non-belt printers so behavior is unchanged. + BeltFloorContext belt_ctx; + const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config); + // generate tree support tool paths tbb::parallel_for( tbb::blocked_range(m_raft_layers, m_object->support_layer_count()), @@ -1559,7 +1672,7 @@ void TreeSupport::generate_toolpaths() filler_interface->angle = m_support_params.support_interface_angle(area_group.interface_id); if (area_group.type != SupportLayer::BaseType) { // interface - if (layer_id == 0) { + if (layer_id == 0 && !belt_floor_active) { Flow flow = m_raft_layers == 0 ? m_object->print()->brim_flow() : support_flow; ExtrusionRole brim_role = (area_group.type == SupportLayer::RoofType && !area_group.interface_as_base) ? erSupportMaterialInterface : erSupportMaterial; @@ -1632,7 +1745,7 @@ void TreeSupport::generate_toolpaths() } else { // base_areas - bool support_base_on_bed = (layer_id == 0 && m_raft_layers == 0); + bool support_base_on_bed = (layer_id == 0 && m_raft_layers == 0 && !belt_floor_active); Flow flow = support_base_on_bed ? m_support_params.first_layer_flow : support_flow; bool need_infill = with_infill; if(m_object_config->support_base_pattern==smpDefault) @@ -1653,7 +1766,7 @@ void TreeSupport::generate_toolpaths() std::unique_ptr base_eec = std::make_unique(); base_eec->no_sort = true; ExtrusionEntitiesPtr &base_dst = base_eec->entities; - if (layer_id == 0) { + if (layer_id == 0 && !belt_floor_active) { float density = float(m_object_config->raft_first_layer_density.value * 0.01); fill_expolygons_with_sheath_generate_paths(base_dst, loops, filler_support.get(), density, erSupportMaterial, flow, m_support_params, true, false); @@ -2059,6 +2172,15 @@ void TreeSupport::draw_circles() coordf_t support_extrusion_width = m_support_params.support_extrusion_width; const float tree_brim_width = config.tree_support_brim_width.value; + // Belt floor: the first object layer is not on a flat bed — it rests on the + // tilted, moving belt. So the first-object-layer adhesion features (the tree + // support brim, the hybrid first-layer base expansion) must be suppressed: + // their expanded contact rings project to a stray brim/skirt loop sitting in + // the Z=0 belt plane around the support footprint. false on non-belt printers, + // so behavior there is unchanged. + BeltFloorContext belt_ctx; + const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config); + if (m_object->support_layer_count() <= m_raft_layers) return; BOOST_LOG_TRIVIAL(info) << "draw_circles for object: " << m_object->model_object()->name; @@ -2169,7 +2291,7 @@ void TreeSupport::draw_circles() circle.points[i] = circle.points[i] * scale + node.position; } } - if (obj_layer_nr == 0 && m_raft_layers == 0) { + if (obj_layer_nr == 0 && m_raft_layers == 0 && !belt_floor_active) { double brim_width = !config.tree_support_auto_brim ? tree_brim_width : std::max(MIN_BRANCH_RADIUS_FIRST_LAYER, std::min(node.radius + node.dist_mm_to_top / (scale * branch_radius) * 0.5, MAX_BRANCH_RADIUS_FIRST_LAYER) - node.radius); auto tmp=offset(circle, scale_(brim_width)); if(!tmp.empty()) @@ -2239,6 +2361,29 @@ void TreeSupport::draw_circles() base_areas = diff_ex(base_areas, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roofs, get_extents(base_areas))); base_areas = intersection_ex(base_areas, m_machine_border); + // Belt floor: clip tree support polygons by the belt surface plane. + // Non-organic tree support layers inherit their print_z from the + // (already globally-offset) object layers — see plan_layer_heights() + // and add_tree_support_layer(); only ORGANIC layers get the global + // Z offset applied later in _generate_support_material(). So here + // ts_layer->print_z is in the GLOBAL frame and we must use init() + // (global), not init_local(): mixing a local-frame clip plane with + // a global print_z displaces the cutoff line by belt_global_z_offset + // along the shear axis, leaving an un-clipped wedge of support below + // the belt floor. In per-object (non-global) mode belt_global_z_offset + // is 0 so init() and init_local() coincide — this is a no-op there. + { + BeltFloorContext ctx; + if (ctx.init(m_slicing_params, *m_print_config)) { + Polygons belt_surface = ctx.surface_polygon(ts_layer->print_z); + base_areas = diff_ex(base_areas, belt_surface); + roof_areas = diff_ex(roof_areas, belt_surface); + roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); + floor_areas = diff_ex(floor_areas, belt_surface); + roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); + } + } + if (SQUARE_SUPPORT) { // simplify support contours ExPolygons base_areas_simplified; @@ -2371,7 +2516,7 @@ void TreeSupport::draw_circles() // part. area_poly is collected from ePolygon nodes above, which are the normal // support nodes in Hybrid mode. Apply the expansion before area_groups and // lslices are built so toolpaths and brim avoidance use the same footprint. - if (layer_nr == 0 && m_raft_layers == 0 && m_support_params.support_style == smsTreeHybrid && + if (layer_nr == 0 && m_raft_layers == 0 && !belt_floor_active && m_support_params.support_style == smsTreeHybrid && m_object_config->raft_first_layer_expansion.value > 0.f) { ExPolygons expanded_base_areas; const float inflate_factor_1st_layer = float(scale_(m_object_config->raft_first_layer_expansion.value)); @@ -2706,6 +2851,7 @@ void TreeSupport::drop_nodes() const size_t tip_layers = base_radius / layer_height; //The number of layers to be shrinking the circle to create a tip. This produces a 45 degree angle. const coordf_t radius_sample_resolution = m_ts_data->m_radius_sample_resolution; const bool support_on_buildplate_only = config.support_on_build_plate_only.value; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON; const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config); SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor; float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm @@ -2941,16 +3087,27 @@ void TreeSupport::drop_nodes() node_parent = p_node->parent ? p_node : neighbour; // Make sure the next pass doesn't drop down either of these (since that already happened). node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node); - const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); - SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, - node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0), - to_buildplate, node_parent, print_z_next, height_next); - get_max_move_dist(next_node); - m_ts_data->m_mutex.lock(); - contact_nodes[layer_nr_next].push_back(next_node); - neighbour->valid = false; - p_node->valid = false; - m_ts_data->m_mutex.unlock(); + // Belt floor: a merged node ends once its whole circle is in the belt + // (its slices are clipped to the belt plane in draw_circles(), so it + // tapers to a tip on the belt). Treat as object-surface termination + // (not buildplate) so the node gets floor/interface areas instead of + // base pads. + if (has_belt_floor && belt_node_landed(next_position, std::max(node.radius, neighbour->radius), print_z_next)) { + std::scoped_lock lock(m_ts_data->m_mutex); + node_parent->to_buildplate = false; + neighbour->valid = false; + p_node->valid = false; + } else { + const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); + SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, + node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0), + to_buildplate, node_parent, print_z_next, height_next); + get_max_move_dist(next_node); + std::scoped_lock lock(m_ts_data->m_mutex); + contact_nodes[layer_nr_next].push_back(next_node); + neighbour->valid = false; + p_node->valid = false; + } } else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance. { @@ -3019,6 +3176,12 @@ void TreeSupport::drop_nodes() ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next)); for(auto& overhang:overhangs_next) { Point next_pt = overhang.contour.centroid(); + // Belt floor: a polygon node ends once all of it is in the belt. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && belt_polygon_landed(overhang, print_z_next)) { + p_node->to_buildplate = false; + continue; + } PendingNode pending; pending.position = next_pt; pending.distance_to_top = p_node->distance_to_top + 1; @@ -3166,6 +3329,14 @@ void TreeSupport::drop_nodes() if (is_outside) { next_layer_vertex = candidate_vertex; } } } + // Belt floor: a node ends once its whole circle is in the belt; until + // then it keeps dropping and draw_circles() clips each layer's circle + // to the belt plane, so the branch tapers to a tip on the belt. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && belt_node_landed(next_layer_vertex, node.radius, print_z_next)) { + p_node->to_buildplate = false; + return; // from parallel_for_each lambda + } auto next_collision = get_collision(0, obj_layer_nr_next); const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex); // don't increase radius if next node will collide partially with the object (STUDIO-7883) @@ -3491,6 +3662,8 @@ void TreeSupport::generate_contact_points() const coordf_t max_bridge_length = scale_(config.max_bridge_length.value); coord_t radius_scaled = scale_(base_radius); bool on_buildplate_only = m_object_config->support_on_build_plate_only.value; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON; + //First generate grid points to cover the entire area of the print. BoundingBox bounding_box = m_object->bounding_box(); const Point bounding_box_size = bounding_box.max - bounding_box.min; @@ -3578,6 +3751,10 @@ void TreeSupport::generate_contact_points() auto insert_point = [&](Point pt, const ExPolygon& overhang, double radius, bool force_add = false, bool add_interface=true) { + // Belt floor: skip contact points whose bottom_z is at or below + // the belt floor at this XY position (overhang rests on the belt). + if (has_belt_floor && bottom_z <= belt_floor_print_z(pt)) + return (SupportNode*) nullptr; Point hash_pos = pt / ((radius_scaled + 1) / 1); SupportNode* contact_node = nullptr; if (force_add || !already_inserted.count(hash_pos)) { @@ -3613,8 +3790,10 @@ void TreeSupport::generate_contact_points() double radius = unscale_(overhang_bounds.radius()); Point candidate = overhang_bounds.center(); SupportNode *contact_node = insert_point(candidate, overhang, radius, true, true); - contact_node->type = ePolygon; - curr_nodes.emplace_back(contact_node); + if (contact_node) { + contact_node->type = ePolygon; + curr_nodes.emplace_back(contact_node); + } } }else{ // otherwise, all nodes should be circle nodes @@ -3748,6 +3927,14 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance poly.simplify(scale_(m_radius_sample_resolution), &outline); } + // The belt surface is deliberately NOT part of the outlines. The outlines + // feed the collision and avoidance maps, and a node descending onto an + // obstacle is pushed out of it: with the belt as an obstacle the nodes slid + // down the tilted surface, ahead of the part, instead of landing on it. The + // belt is where a branch ENDS: drop_nodes() stops a node once its whole + // circle is in the belt (belt_node_landed()) and draw_circles() clips every + // layer's circles to the belt plane, so the branch tapers to a tip on it. + if (layer_nr == 0) m_layer_outlines_below.push_back(outline); else diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index e0da1f40c7..c6acd806e0 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -460,6 +460,17 @@ private: bool is_slim = false; bool with_infill = false; + // Belt printer: compute the belt floor print_z at a given XY position (in slicing coords). + // Returns -infinity if belt floor is not active. + double belt_floor_print_z(const Point &pos_slicing) const; + // Whether a node's whole circle (radius in mm) sits at or below the belt at + // print_z. The belt is a tilted plane, so the circle's leading edge crosses it + // |shear| * radius lower than its centre; stopping a node when its centre crosses + // would leave that edge floating a radius above the belt. + bool belt_node_landed(const Point &pos_slicing, double radius, double print_z) const; + // The same for a polygon: every point of it is at or below the belt. + bool belt_polygon_landed(const ExPolygon &poly, double print_z) const; + /*! diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index efd7492b00..ef267fba68 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -25,6 +25,7 @@ #include "Polygon.hpp" #include "Polyline.hpp" #include "MutablePolygon.hpp" +#include "BeltFloorContext.hpp" #include "libslic3r/Support/TreeSupportCommon.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Support/TreeModelVolumes.hpp" @@ -244,6 +245,9 @@ static std::vector>> group_me const bool support_threshold_auto = support_threshold == 0; // +1 makes the threshold inclusive double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.); + // Build plate tilt: compute per-layer XY shift for tilted gravity direction + const Vec2d tilt_slope = build_plate_tilt_slope(print_config); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; //FIXME this is a fudge constant! auto enforcer_overhang_offset = scaled(config.tree_support_tip_diameter.value); const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution; @@ -265,7 +269,7 @@ static std::vector>> group_me size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size())); tbb::parallel_for(tbb::blocked_range(1, num_overhang_layers), [&print_object, &config, &print_config, &enforcers_layers, &blockers_layers, - support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, &throw_on_cancel, &out] + support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_slope, &throw_on_cancel, &out] (const tbb::blocked_range &range) { for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { const Layer ¤t_layer = *print_object.get_layer(layer_id); @@ -289,7 +293,15 @@ static std::vector>> group_me lower_layer_offset = external_perimeter_width - float(scale_(config.support_threshold_overlap.get_abs_value(unscale_(external_perimeter_width)))); } else lower_layer_offset = scaled(lower_layer.height / tan_threshold); - Polygons lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons lower_layer_offseted; + if (has_tilt) { + Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); + translate(lower_src, Point::new_scale(tilt_slope * lower_layer.height)); + lower_layer_offseted = offset(lower_src, lower_layer_offset); + } else { + lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); + } overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted); if (lower_layer_offset == 0) { raw_overhangs = overhangs; @@ -3633,6 +3645,26 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons if (layer) layer->polygons = intersection(layer->polygons, volumes.m_bed_area); }); + // Belt floor: clip ALL organic support layers (including intermediate/base + // fill) against the belt surface. The branch slices were already clipped + // in organic_draw_branches(), but intermediate layers generated between + // branches and the build plate need clipping too. + // Compute the belt floor polygon directly from each layer's print_z + // rather than mapping to a layer index (avoids index mismatch issues). + { + const auto &sp = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + BeltFloorContext ctx; + ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); + if (ctx.is_active()) { + tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { + if (!layer || layer->polygons.empty()) + return; + layer->polygons = diff(layer->polygons, ctx.surface_polygon(layer->print_z)); + }); + } + } + print.set_status(69, _L("Generating support")); generate_support_toolpaths(print_object.support_layers(), print_object.config(), support_params, print_object.slicing_parameters(), raft_layers, bottom_contacts, top_contacts, intermediate_layers, interface_layers, base_interface_layers); @@ -3923,6 +3955,10 @@ void organic_draw_branches( // ORCA: safety offset when trimming collision/bed to improve robustness. slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true), ApplySafetyOffset::Yes); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); slices[i] = intersection(slices[i], volumes.m_bed_area, ApplySafetyOffset::Yes); + // Belt floor: clip branch slices against the belt surface plane. + LayerIndex belt_idx = layer_begin + i; + if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty()) + slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]); remove_small(slices[i], tiny_area); } @@ -3967,7 +4003,10 @@ void organic_draw_branches( if (!contacts.empty()) bottom_contacts.emplace_back(std::move(contacts)); } - } else if (layer_begin > 0) { + } else if (layer_begin > 0 && (volumes.m_belt_floor.empty() || num_empty == 0)) { + // Belt-floor clipping makes initial slices empty often; without this + // gate, "verylost" branches propagate rest_support down to layer 0 and + // OOM on tall belt prints. // Drop down areas that do rest non - gracefully on the model to ensure the branch actually rests on something. struct BottomExtraSlice { Polygons polygons; @@ -3976,12 +4015,20 @@ void organic_draw_branches( std::vector bottom_extra_slices; Polygons rest_support; coord_t bottom_radius = support_element_radius(config, *branch.path.front()); + // Belt printer (GeneratorOnly belt floor): the tilted belt surface is the + // build surface, so a branch should terminate ON the belt with a thin tip, + // not stamp its full footprint straight down to Z=0 and weld neighbouring + // branches into a solid floor slab. m_belt_floor is only populated in that + // mode, so it doubles as the gate (no effect on other printer types). + const bool belt_mode = !volumes.m_belt_floor.empty(); // Don't propagate further than 1.5 * bottom radius. //LayerIndex layers_propagate_max = 2 * bottom_radius / config.layer_height; LayerIndex layers_propagate_max = 5 * bottom_radius / config.layer_height; - LayerIndex layer_bottommost = branch.path.front()->state.verylost ? + LayerIndex layer_bottommost = (branch.path.front()->state.verylost && !belt_mode) ? // If the tree bottom is hanging in the air, bring it down to some surface. 0 : + // In belt mode never force-drop to Z=0 (the belt clip below handles + // termination); otherwise the "verylost" branch welds into the slab. //FIXME the "verylost" branches should stop when crossing another support. std::max(0, layer_begin - layers_propagate_max); double support_area_min_radius = M_PI * sqr(double(config.branch_radius)); @@ -3992,12 +4039,29 @@ void organic_draw_branches( LayerIndex collision_layer = (layer_idx == layer_begin - 1) ? layer_begin : layer_idx; Polygons collision = volumes.getCollision(0, collision_layer, false); rest_support = diff_clipped(rest_support.empty() ? slice_front_contact : rest_support, collision, ApplySafetyOffset::Yes); + // Belt floor: clip propagated support at belt surface. + bool belt_cut = false; + if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) { + double area_before = area(rest_support); + rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]); + // The belt counts as "reached" only when it actually removes part + // of this branch's footprint. The belt half-plane is non-empty at + // every near-belt layer, so testing non-emptiness alone would + // terminate a laterally-distant branch ~1 layer above true contact, + // leaving a gap. Require a real area reduction instead. + belt_cut = belt_mode && area(rest_support) < area_before - tiny_area; + } remove_small(rest_support, tiny_area); double rest_support_area = area(rest_support); if (rest_support_area < support_area_stop) // Don't propagate a fraction of the tree contact surface. break; bottom_extra_slices.push_back({ rest_support, rest_support_area }); + // Belt mode: once the belt surface actually starts cutting this branch + // it has reached the belt — keep this last (belt-clipped) slice as the + // contact and stop, rather than stamping the footprint further down. + if (belt_cut) + break; } // Now remove those bottom slices that are not supported at all. #if 0 @@ -4015,7 +4079,10 @@ void organic_draw_branches( } } #endif - if (config.settings.support_floor_layers > 0) { + // Belt mode: no solid support-floor pad under these branches — it is what + // welds neighbouring belt-terminating branches into the dense Z=0 slab. + // They simply taper out onto the tilted belt as distributed thin contacts. + if (!belt_mode && config.settings.support_floor_layers > 0) { Polygons contacts; if (!bottom_extra_slices.empty()) { const int contact_idx = int(bottom_extra_slices.size()) - 1; // Use the lowest contact slice as the footprint. @@ -4054,7 +4121,10 @@ void organic_draw_branches( } // ORCA: retain bottom contacts even when no placeable areas intersect. - if (branch.has_root && config.support_rests_on_model && branch.path.front()->state.layer_idx > 0 && + // Skipped in belt mode (m_belt_floor populated) so we don't re-introduce a + // solid floor pad for branches that terminate on the tilted belt surface. + if (volumes.m_belt_floor.empty() && + branch.has_root && config.support_rests_on_model && branch.path.front()->state.layer_idx > 0 && config.settings.support_floor_layers > 0 && config.z_distance_bottom_layers > 0 && bottom_contacts.empty() && !slice_front_contact.empty()) bottom_contacts.emplace_back(slice_front_contact); diff --git a/src/libslic3r/Support/TreeSupportCommon.hpp b/src/libslic3r/Support/TreeSupportCommon.hpp index 97a0e31eb3..6767d2193a 100644 --- a/src/libslic3r/Support/TreeSupportCommon.hpp +++ b/src/libslic3r/Support/TreeSupportCommon.hpp @@ -667,7 +667,22 @@ inline SupportGeneratorLayer& layer_initialize( const size_t layer_idx) { layer_new.print_z = layer_z(slicing_params, config, layer_idx); - layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0; + // Layer 0 has no layer below it, so its bottom is the build plate at z = 0 -- + // true for a flat bed, false for a belt, whose virtual support layers extend + // below zero. Taking 0 there made the bottom-most belt layer's height come out + // as its own (negative) print_z, which reached Flow::with_height() and threw + // FlowErrorNegativeFlow, so tree support could not slice any belt model whose + // branches reached down that far. + // + // Only the negative case is corrected. An earlier version used + // min(0, print_z - layer_height), which also fires whenever the initial layer + // is THINNER than the regular layer height -- e.g. 0.2 over 0.3, both + // independently configurable -- and silently changed flat-bed support layer + // heights. Keying on the sign leaves every non-negative print_z on exactly + // the previous value of 0. + layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0.; + if (layer_idx == 0 && layer_new.print_z < 0.) + layer_new.bottom_z = layer_new.print_z - slicing_params.layer_height; layer_new.height = layer_new.print_z - layer_new.bottom_z; return layer_new; } diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index df23e3db85..3944a3cba1 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -298,7 +298,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c return this->select_unsplit_triangle(hit, facet_idx, neighbors); } -void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const bool select_partially) +void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const Vec3f &up_direction, const bool select_partially) { assert(facet_start < m_orig_size_indices); @@ -359,8 +359,8 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&c int facet = facets_to_check[facet_idx]; const Vec3f& facet_normal = m_face_normals[m_triangles[facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix* facet_normal).normalized().z(); - if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (select_triangle(facet, new_state, triangle_splitting, select_partially)) { // add neighboring facets to list to be processed later for (int neighbor_idx : m_neighbors[facet]) @@ -385,7 +385,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp) void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate, const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg, - bool force_reselection) + const Vec3f &up_direction, bool force_reselection) { assert(facet_start < m_orig_size_indices); @@ -409,8 +409,8 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st const Vec3f &facet_normal = m_face_normals[m_triangles[current_facet].source_triangle]; Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); - float world_normal_z = (normal_matrix * facet_normal).normalized().z(); - if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) { + float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction); + if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) { if (m_triangles[current_facet].is_split()) { for (int split_triangle_idx = 0; split_triangle_idx <= m_triangles[current_facet].number_of_split_sides(); ++split_triangle_idx) { assert(split_triangle_idx < int(m_triangles[current_facet].children.size())); @@ -2605,7 +2605,7 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( if (TriangleCursor::check_normal(norm_b, -norm_a) && check_overlap(pv0, pv1, pv2, ta, tb, tc)) { // Paint this face target_selector.select_patch(face_idx, TriangleCursor::build_cursor(source_selector, tri), tri.get_state(), - Transform3d::Identity(), true, 0.f, true); + Transform3d::Identity(), true, 0.f, Vec3f::UnitZ(), true); } return true; // continue traversal }); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 3dd2efdaf0..46ebfb99df 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -350,6 +350,7 @@ public: const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation bool triangle_splitting, // If triangles will be split base on the cursor or not float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + const Vec3f &up_direction = Vec3f::UnitZ(), // Up direction for overhang detection (accounts for build plate tilt) bool select_partially = false); // Select a triangle if it's partially in the cursor but too small to be subdivided void seed_fill_select_triangles(const Vec3f &hit, // point where to start @@ -358,6 +359,7 @@ public: const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only float seed_fill_angle, // the maximal angle between two facets to be painted by the same color float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. + const Vec3f &up_direction = Vec3f::UnitZ(), // Up direction for overhang detection (accounts for build plate tilt) bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle void bucket_fill_select_triangles(const Vec3f &hit, // point where to start diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index a6aecda210..3217edf1da 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -1,4 +1,5 @@ #include "calib.hpp" +#include "GCode/BeltKinematics.hpp" #include "BoundingBox.hpp" #include "Config.hpp" #include "Flow.hpp" @@ -18,6 +19,8 @@ #include #include #include +#include +#include #include "clonable_ptr.hpp" namespace Slic3r { @@ -648,9 +651,9 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna refresh_setup(config, is_bbl_machine, object, origin); - gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), m_writer, "Move to start XY position"); - gcode << m_writer.travel_to_z(height_first_layer() + height_z_offset(), "Move to start Z position"); - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), *m_writer, "Move to start XY position"); + gcode << m_writer->travel_to_z(height_first_layer() + height_z_offset(), "Move to start Z position"); + gcode << m_writer->set_pressure_advance(m_params.start); const DrawBoxOptArgs default_box_opt_args(wall_count(), height_first_layer(), line_width_first_layer(), speed_adjust(speed_first_layer())); @@ -658,15 +661,15 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna // create anchoring frame //pattern uses outer wall speed/width gcode << ";" << GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) << "Outer wall\n"; - gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y(), print_size_x(), frame_size_y(), default_box_opt_args); + gcode << draw_box(*m_writer, m_starting_point.x(), m_starting_point.y(), print_size_x(), frame_size_y(), default_box_opt_args); // create tab for numbers DrawBoxOptArgs draw_box_opt_args = default_box_opt_args; draw_box_opt_args.is_filled = true; draw_box_opt_args.num_perimeters = wall_count(); //draw box as bottom surface, so numbers are clearly visible on top - gcode << ";" << GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) << "Bottom surface\n"; - gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(), + gcode << ";" << GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) << "Bottom surface\n"; + gcode << draw_box(*m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(), print_size_x(), max_numbering_height() + line_spacing_first_layer() + m_glyph_padding_vertical * 2, draw_box_opt_args); @@ -694,15 +697,15 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna gcode = std::stringstream(); // reset for next layer contents gcode << "; start pressure advance pattern for layer\n"; - gcode << m_writer.travel_to_z(layer_height, "Move to layer height"); - gcode << m_writer.reset_e(); + gcode << m_writer->travel_to_z(layer_height, "Move to layer height"); + gcode << m_writer->reset_e(); } // line numbering if (i == 1) { m_number_len = max_numbering_length(); - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << m_writer->set_pressure_advance(m_params.start); double number_e_per_mm = e_per_mm(line_width(), height_layer(), m_config.option("nozzle_diameter")->get_at(0), @@ -714,20 +717,20 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna for (int j = 0; j < num_patterns; j += 2) { gcode << draw_number(glyph_start_x(j), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), m_params.start + (j * m_params.step), m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); } // flow value int line_num = num_patterns + 2; gcode << draw_number(glyph_start_x(line_num), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), flow_val(), m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); // acceleration line_num = num_patterns + 4; gcode << draw_number(glyph_start_x(line_num), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), accel, m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); } @@ -746,20 +749,20 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna /* Draw a line at slightly slower accel and speed in order to trick gcode writer to force update acceleration and speed. * We do this since several tests may be generated by their own gcode writers which are * not aware about their neighbours updating acceleration/speed */ - gcode << m_writer.set_print_acceleration(std::max(1, accel - 1)); - gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), m_writer, "Move to starting point", zhop_height, layer_height); - gcode << draw_line(m_writer, Vec2d(m_starting_point.x(), m_starting_point.y() + frame_size_y()), line_width(), height_layer(), speed_adjust(std::max(1, speed_perimeter() - 1)), "Accel/flow trick line"); - gcode << m_writer.set_print_acceleration(accel); + gcode << m_writer->set_print_acceleration(std::max(1, accel - 1)); + gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), *m_writer, "Move to starting point", zhop_height, layer_height); + gcode << draw_line(*m_writer, Vec2d(m_starting_point.x(), m_starting_point.y() + frame_size_y()), line_width(), height_layer(), speed_adjust(std::max(1, speed_perimeter() - 1)), "Accel/flow trick line"); + gcode << m_writer->set_print_acceleration(accel); } double initial_x = to_x; double initial_y = to_y; - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to pattern start",zhop_height,layer_height); + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to pattern start",zhop_height,layer_height); for (int j = 0; j < num_patterns; ++j) { // increment pressure advance - gcode << m_writer.set_pressure_advance(m_params.start + (j * m_params.step)); + gcode << m_writer->set_pressure_advance(m_params.start + (j * m_params.step)); gcode << ";" << GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) << "Outer wall\n"; for (int k = 0; k < wall_count(); ++k) { to_x += std::cos(to_radians(m_corner_angle) / 2) * side_length; @@ -769,27 +772,27 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna auto draw_line_arg_line_width = line_width(); // don't use line_width_first_layer so results are consistent across all layers auto draw_line_arg_speed = i == 0 ? speed_adjust(speed_first_layer()) : speed_adjust(speed_perimeter()); auto draw_line_arg_comment = "Print pattern wall"; - gcode << draw_line(m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); + gcode << draw_line(*m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); to_x -= std::cos(to_radians(m_corner_angle) / 2) * side_length; to_y += std::sin(to_radians(m_corner_angle) / 2) * side_length; - gcode << draw_line(m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); + gcode << draw_line(*m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); to_y = initial_y; if (k != wall_count() - 1) { // perimeters not done yet. move to next perimeter to_x += line_spacing_angle(); - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to start next pattern wall", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to start next pattern wall", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift } else if (j != num_patterns - 1) { // patterns not done yet. move to next pattern to_x += m_pattern_spacing + line_width(); - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to next pattern", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to next pattern", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift } else if (i != m_num_layers - 1) { // layers not done yet. move back to start to_x = initial_x; - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move back to start position", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift - gcode << m_writer.reset_e(); // reset extruder before printing placeholder cube to avoid over extrusion + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move back to start position", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << m_writer->reset_e(); // reset extruder before printing placeholder cube to avoid over extrusion } else { // everything done } @@ -797,8 +800,8 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna } } - gcode << m_writer.reset_e(); - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << m_writer->reset_e(); + gcode << m_writer->set_pressure_advance(m_params.start); gcode << "; end pressure advance pattern for layer\n"; CustomGCode::Item item; @@ -875,13 +878,35 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod PrintConfig print_config; print_config.apply(m_config, true); - m_writer.apply_print_config(print_config); - m_writer.set_xy_offset(origin(0), origin(1)); - m_writer.set_is_bbl_machine(is_bbl_machine); + // ORCA-Belt: the pattern is drawn in logical bed coordinates directly on + // the build surface — on a belt printer that means the belt plane, which + // needs the machine kinematics (axis remap + frame shear/scale) with the + // coordinates interpreted as world points (see set_world_coordinates). + if (print_config.belt_printer.value) { + auto belt_writer = std::make_shared(); + install_belt_kinematics(*belt_writer, print_config, /*world_coordinates=*/true); + const int rx = int(print_config.gcode_remap_x.value); + const int ry = int(print_config.gcode_remap_y.value); + const int rz = int(print_config.gcode_remap_z.value); + if (rx != 0 || ry != 1 || rz != 2) { + belt_writer->set_axis_remap(rx, ry, rz); + BoundingBoxf bbox_bed(print_config.printable_area.values); + belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print_config.printable_height.value)); + } + m_writer = std::move(belt_writer); + } else if (m_writer && dynamic_cast(&m_writer->kinematics()) != nullptr) { + // Previously configured for a belt printer; drop back to a plain writer. + m_writer = std::make_shared(); + } + + m_writer->apply_print_config(print_config); + m_writer->set_xy_offset(origin(0), origin(1)); + m_writer->set_is_bbl_machine(is_bbl_machine); const unsigned int extruder_id = object.volumes.front()->extruder_id(); - m_writer.set_extruders({extruder_id}); - m_writer.set_extruder(extruder_id); + m_writer->set_extruders({extruder_id}); + m_writer->set_extruder(extruder_id); } double CalibPressureAdvancePattern::object_size_x() const diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index fd22d4a08f..4ef676cdb1 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #define calib_pressure_advance_dd #include "GCodeWriter.hpp" @@ -369,7 +370,10 @@ private: const Calib_Params &m_params; - GCodeWriter m_writer; + // Belt printers get belt kinematics installed on it (_refresh_writer); + // shared_ptr keeps the class copyable — the writer is rebuilt by + // refresh_setup() before every use anyway. + std::shared_ptr m_writer{std::make_shared()}; Vec3d m_starting_point; bool m_is_start_point_fixed = false; diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index b313c4fe42..de3b69565d 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -436,6 +436,8 @@ set(SLIC3R_GUI_SOURCES GUI/ParamsPanel.hpp GUI/PartPlate.cpp GUI/PartPlate.hpp + GUI/BeltPurgeTower.cpp + GUI/BeltPurgeTower.hpp GUI/FilamentGroupPopup.hpp GUI/FilamentGroupPopup.cpp GUI/PhysicalPrinterDialog.cpp diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 6d14a4d77c..d7fd871f27 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -36,9 +36,12 @@ #include #include "libslic3r/Preset.hpp" #include "libslic3r/Config.hpp" +#include #if BOOST_VERSION >= 107800 #include +#include +#include #else #include #endif @@ -410,6 +413,8 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; } } + render_gravity_arrow(view_matrix, projection_matrix); + glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -714,7 +719,7 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj if (shader != nullptr) { shader->start_using(); shader->set_uniform("emission_factor", 0.0f); - const Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); + Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); shader->set_uniform("volume_world_matrix", model_matrix); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); shader->set_uniform("projection_matrix", projection_matrix); @@ -753,6 +758,59 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co render_texture(bottom, canvas);*/ } +void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + // build_plate_tilt_{x,y} are kept in sync with the belt tilt (see TabPrinter), so + // reading them here covers both belt and non-belt tilted printers. + const Vec3d up_dir = build_plate_tilt_up_direction(); + if (up_dir == Vec3d::UnitZ()) { + m_gravity_arrow.reset(); + return; + } + + // A plain line along the tilted "up" direction -- the way the layers lean, i.e. + // the gantry -- drawn like the bed axes (no tip: the other direction is not + // possible) and shorter than them, so it reads as a hint inside the YZ corner. + const float length = 0.6f * m_axes.get_total_length(); + if (!m_gravity_arrow.is_initialized() || m_gravity_arrow_length != length) { + m_gravity_arrow.reset(); + m_gravity_arrow.init_from(smooth_cylinder(16, /*Radius*/ length / 75.f, length)); + m_gravity_arrow_length = length; + } + + // The cylinder model points along +Z. Compute the rotation that aligns it with + // up_dir: rotation axis = cross(+Z, up_dir), angle = acos(dot(+Z, up_dir)). + Vec3d from = Vec3d::UnitZ(); + Vec3d to = up_dir; + double dot = from.dot(to); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + // Nearly opposite -- rotate 180 degrees around X + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(to).normalized(); + double angle = std::acos(std::clamp(dot, -1.0, 1.0)); + rot = Eigen::AngleAxisd(angle, axis) * rot; + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = Eigen::Translation3d(m_axes.get_origin()) * rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_gravity_arrow.set_color({ 1.0f, 0.85f, 0.0f, 1.0f }); // yellow + m_gravity_arrow.render(); + + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 66e4bc7d0a..eeabe7da82 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -115,6 +115,8 @@ private: //GLTexture m_temp_texture; GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; + GLModel m_gravity_arrow; + float m_gravity_arrow_length{ 0.f }; Axes m_axes; float m_scale_factor{ 1.0f }; @@ -144,6 +146,7 @@ public: // Build volume geometry for various collision detection tasks. const BuildVolume& build_volume() const { return m_build_volume; } + BuildVolume& build_volume() { return m_build_volume; } // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } @@ -183,7 +186,8 @@ private: void render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); - + void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); }; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 339e545842..5a39f9a279 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1238,6 +1238,10 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, // default sampler unit 0, which can conflict with other sampler types. shader->set_uniform("depth_tex", OUTLINE_DEPTH_TEX_UNIT); + // Compute up direction accounting for build plate tilt. This is frame-invariant + // (config cannot change mid-render), so compute it once before the volume loop. + const Vec3f up_direction = GUI::build_plate_tilt_up_direction().cast(); + for (GLVolumeWithIdAndZ& volume : to_render) { #if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT if (type == ERenderType::Transparent) { @@ -1316,6 +1320,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower); shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast())); shader->set_uniform("slope.normal_z", support_normal_z); + shader->set_uniform("slope.up_direction", up_direction); #if ENABLE_ENVIRONMENT_MAP unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id(); diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp new file mode 100644 index 0000000000..c18d549db1 --- /dev/null +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -0,0 +1,465 @@ +#include "BeltPurgeTower.hpp" + +#include "GUI_App.hpp" +#include "GUI_ObjectList.hpp" +#include "PartPlate.hpp" +#include "I18N.hpp" + +#include "libslic3r/Model.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/FilamentMixer.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Geometry.hpp" +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/libslic3r.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +// The classic wipe tower is disabled in belt mode (its G-code bypasses the belt +// transform), so filament-change purging is routed into this prism via +// flush_into_objects (see Print::_plan_belt_purge()). The prism is a real model +// object so it is sliced through the normal pipeline and picks up the belt +// rotation. Width across the belt is user-set (belt_purge_tower_width); height +// is sized so each tilted slicing plane's cross-section through the prism can +// absorb the worst-case purge volume of one layer; length follows the printed +// objects along the belt (plus ramp/height compensation at both tilted ends). +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector &sigs) +{ + auto is_prism = [](const ModelObject *mo) { + const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); + return opt != nullptr && opt->getBool(); + }; + + // Every plate gets its own prism. A prism belongs to the plate it lies on; one + // that lies on no plate is stale. + const int plate_count = partplate_list.get_plate_count(); + sigs.resize(size_t(plate_count)); + std::vector> prisms_by_plate(static_cast(plate_count)); + std::vector stale_prisms; + for (int i = 0; i < (int) model.objects.size(); ++i) + if (is_prism(model.objects[i])) { + const int plate_idx = model.objects[i]->instances.empty() ? -1 : partplate_list.find_instance(i, 0); + if (plate_idx >= 0 && plate_idx < plate_count) + prisms_by_plate[size_t(plate_idx)].push_back(i); + else + stale_prisms.push_back(i); + } + + // Deletes prism objects, keeping the sidebar and part plates in sync + // (same primitives as Plater::priv::remove(), minus scene update — the + // caller refreshes the scene). Highest index first, so the others stay valid. + auto remove_prisms = [&](std::vector idxs) { + std::sort(idxs.begin(), idxs.end()); + for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { + model.delete_object(size_t(*it)); + partplate_list.notify_instance_removed(*it, -1); + obj_list->delete_object_from_list(size_t(*it)); + } + }; + auto all_prisms = [&]() { + std::vector all = stale_prisms; + for (const auto &v : prisms_by_plate) + all.insert(all.end(), v.begin(), v.end()); + return all; + }; + + // Cheap early-out for non-belt printers: only belt printers ever get a belt + // purge tower, and this runs on every background-process tick — so avoid the + // full_config() merge below unless this is actually a belt printer. (Also + // tidies up a stale prism if the user switched away from a belt printer.) + { + const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_pre == nullptr || !belt_pre->value) { + std::fill(sigs.begin(), sigs.end(), BeltPurgeSignature{}); + const std::vector all = all_prisms(); + if (all.empty()) + return false; + remove_prisms(all); + return true; + } + } + + // Read every sizing input from the MERGED full config — the exact same + // config the backend slices with (BackgroundSlicingProcess::apply uses + // preset_bundle->full_config()). Reading from the individual presets caused + // GUI/backend mismatches: e.g. purge_in_prime_tower or a populated + // flush_volumes_matrix present in the merged config but false/empty in the + // preset the GUI happened to read, so the tower was sized for the small + // prime_volume instead of the real color-change flush and could not absorb + // it. The three names below alias the one merged config so the rest of the + // function is unchanged. + const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config(); + const DynamicPrintConfig &printer_config = full_cfg; + const DynamicPrintConfig &print_config = full_cfg; + const DynamicPrintConfig &project_config = full_cfg; + + const auto *belt_opt = printer_config.option("belt_printer"); + const bool belt = belt_opt != nullptr && belt_opt->value; + // Belt purge tower is its own type, gated by the belt-only printer option + // enable_belt_purge_tower (not the classic process enable_prime_tower). + const bool prime_tower_enabled = printer_config.has("enable_belt_purge_tower") && printer_config.opt_bool("enable_belt_purge_tower"); + const auto *seq_opt = print_config.option>("print_sequence"); + const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; + + // Mixed filament slots are expanded to their physical components below. + std::vector is_mixed; + std::vector comp_strs; + if (const auto *o = full_cfg.option("filament_is_mixed")) + is_mixed = o->values; + if (const auto *o = full_cfg.option("filament_mixed_components")) + comp_strs = o->values; + + const double gap = 1.0; + const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; + + // Belt geometry. The rotation axis is the gantry tilt axis; the belt + // travels along the *other* horizontal axis (X-rotation -> belt along Y, + // the CR-30 default). The purge prism is a long bar laid along the belt + // travel direction, beside the parts. For no/Z rotation we fall back to + // vertical slicing geometry (theta = 90 deg). + const auto *axis_opt = printer_config.option>("belt_slice_rotation"); + const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); + const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X; + const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y + double theta = M_PI / 2.; + if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) + theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); + const double sin_t = std::sin(theta); + const double cot_t = std::cos(theta) / sin_t; + + // NOTE: both purge_in_prime_tower and single_extruder_multi_material are + // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the + // printer preset. Reading purge_in_prime_tower from the print preset returns + // has()==false, collapsing use_matrix to false and sizing the tower for the + // small prime_volume instead of the real color-change flush. This must match + // the backend Print::_plan_belt_purge() which reads both from the merged config. + const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) + && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); + const double prime_volume = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; + const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; + + // The bed (printable_area) is plate-local but model instances live in the + // plate's world frame, so the plate origin is added to every bed coordinate. + const double inset = 1.; + BoundingBoxf bed_ext; + if (const auto *bed_opt = printer_config.option("printable_area"); + bed_opt != nullptr && !bed_opt->values.empty()) + bed_ext = get_extents(bed_opt->values); + + // What each plate needs, decided before anything is deleted or created. + struct Plan + { + bool wanted = false; + BeltPurgeSignature sig; + int n_islands = 1; + double w_sub = 0., length = 0., height = 0.; + Vec3d center = Vec3d::Zero(); + }; + std::vector plans(static_cast(plate_count)); + + for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) { + Plan &plan = plans[size_t(plate_idx)]; + PartPlate *plate = partplate_list.get_plate(plate_idx); + + // Filaments used and bounding extent of the non-prism objects on this + // plate (1-based filament ids; volume extruder 0 = object default). + std::set filaments; + double x_min = std::numeric_limits::max(); + double x_max = -std::numeric_limits::max(); + double y_min = std::numeric_limits::max(); + double y_max = -std::numeric_limits::max(); + double z_max = 0.; + bool have_objects = false; + if (belt && plate != nullptr) { + for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { + const ModelObject *mo = model.objects[obj_idx]; + if (is_prism(mo)) + continue; + int obj_extruder = 1; + if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) + obj_extruder = opt->getInt(); + bool any_instance_on_plate = false; + for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { + if (!plate->contain_instance_totally(obj_idx, inst_idx)) + continue; + any_instance_on_plate = true; + const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + x_min = std::min(x_min, bb.min.x()); + x_max = std::max(x_max, bb.max.x()); + y_min = std::min(y_min, bb.min.y()); + y_max = std::max(y_max, bb.max.y()); + z_max = std::max(z_max, bb.max.z()); + } + if (!any_instance_on_plate) + continue; + have_objects = true; + for (const ModelVolume *mv : mo->volumes) + for (int e : mv->get_extruders()) + filaments.insert(e > 0 ? e : obj_extruder); + } + } + + // A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time + // ToolOrdering::resolve_mixed_filaments() replaces it with its physical + // components, so the toolchanges the prism has to absorb are between those + // components, not to the mixed slot itself. Counting the slot as a filament + // of its own therefore over-provisions the prism by one island per mixed slot + // -- the "extra purge tower" -- and, when every component is already used by + // another object, by an island that can never be reached at all. + // + // Expand here with the same helper the backend uses (Print.cpp's sequential + // path), so the GUI sizes the prism against the same filament set the slicer + // will actually produce. No-op when no filament is mixed. + if (has_any_mixed_filament(is_mixed)) { + std::vector zero_based; + zero_based.reserve(filaments.size()); + for (int f : filaments) + if (f > 0) + zero_based.push_back((unsigned int) (f - 1)); + zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs); + filaments.clear(); + for (unsigned int f : zero_based) + filaments.insert((int) f + 1); + } + + plan.wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; + if (!plan.wanted) + continue; + + // --- Sizing ----------------------------------------------------------- + const int n_islands = std::max(1, (int) filaments.size() - 1); + // Every disconnected island needs at least 1 mm of printable width. Honor + // the configured total width whenever possible, but never let the island + // layout silently grow past the footprint used for placement. + const double min_width = n_islands + (n_islands - 1) * gap; + const double width = std::max(min_width, + print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); + const double printable_width = width - (n_islands - 1) * gap; + + // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. + const double belt_min = belt_is_y ? y_min : x_min; + const double belt_max = belt_is_y ? y_max : x_max; + const double lat_min = belt_is_y ? x_min : y_min; + const double lat_max = belt_is_y ? x_max : y_max; + + // Worst-case purge volume of one layer: up to (filament count - 1) + // toolchanges, each needing the worst flush matrix entry (mirrors the + // volume selection in Print::_plan_belt_purge()). + double max_flush = prime_volume; + if (use_matrix) { + const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + const std::vector matrix = get_flush_volumes_matrix( + project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); + const auto * multi_opt = project_config.option("flush_multiplier"); + const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; + const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); + double m = 0.; + for (int i : filaments) + for (int j : filaments) + if (i != j && i <= n_total && j <= n_total) + m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); + if (m > 0.) + max_flush = m * multiplier; + } + const double v_layer = double(filaments.size() - 1) * max_flush; + + // Height from the per-layer purge demand. A tilted slicing plane cuts a + // printable_width x (height/sin) rectangle out of the bars, so one layer + // slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that + // holds the worst-case per-layer purge, with eta (infill/perimeter packing) + // and a safety margin for the tilt ramps / grid-alignment slop, plus a + // minimum so the tower is a real printable body rather than a sliver. + const double eta = 0.85; + const double safety = 1.6; + double height = safety * v_layer * sin_t / (printable_width * layer_h * eta); + height = std::clamp(height, 8.0, std::max(8.0, printable_height)); + + // --- Idempotence (input-keyed) ---------------------------------------- + auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization + BeltPurgeSignature &new_sig = plan.sig; + new_sig.valid = true; + new_sig.filament_count = (int) filaments.size(); + new_sig.key[0] = q(width); + new_sig.key[1] = q(layer_h); + new_sig.key[2] = q(height); + new_sig.key[3] = q(belt_min); + new_sig.key[4] = q(belt_max); + new_sig.key[5] = q(lat_min); + new_sig.key[6] = q(z_max); + new_sig.key[7] = static_cast(rot); + new_sig.key[8] = std::lround(theta * 10000.0); + new_sig.key[9] = q(lat_max); + const Vec3d plate_origin = plate->get_origin(); + new_sig.key[10] = q(plate_origin.x()); + new_sig.key[11] = q(plate_origin.y()); + if (bed_ext.defined) { + new_sig.key[12] = q(bed_ext.max.x()); + new_sig.key[13] = q(bed_ext.max.y()); + } + + // --- Position ---------------------------------------------------------- + // Belt-travel axis. With the mesh rotated by theta before slicing, a machine + // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts + // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's + // FULL-cross-section region (the part not in a triangular end ramp) spans + // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' + // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and + // belt_end >= y_max + z_max*cot (parts' top features print further up the + // belt). The trailing z_max*cot term dominates the bar's own ramp. + // + // Along the belt the bar stops at the end of the plate: a longer bar cannot be + // printed, and the cross-sections it loses there are reported by the purge + // planner when the parts' last layers then purge more than the bar holds. + const double margin = 5.; + const double ramp_compensation = height / sin_t; + const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; + double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + if (bed_ext.defined) + belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset); + const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin + const double length = std::max(belt_end - belt_start, 10.); + belt_end = belt_start + length; + const double belt_center = 0.5 * (belt_start + belt_end); + + // Across-belt: flush against the bed's maximum edge, inset by half the bar + // width so the bar's far edge sits on the boundary and the whole bar stays + // on the bed. lat_min/lat_max come from instance_bounding_box (world frame). + const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + if (bed_ext.defined) { + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); + lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; + } + + plan.n_islands = n_islands; + plan.w_sub = (width - (n_islands - 1) * gap) / n_islands; + plan.length = length; + plan.height = height; + plan.center = Vec3d(belt_is_y ? lat_center : belt_center, + belt_is_y ? belt_center : lat_center, + 0.5 * height); + } + + // --- Decide --------------------------------------------------------------- + // A plate whose prism exists and matches its recorded inputs is left alone, so + // subsequent ticks are no-ops until the parts/config actually change. + std::vector to_delete = stale_prisms; + std::vector to_create; + for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) { + const Plan &plan = plans[size_t(plate_idx)]; + const std::vector &existing = prisms_by_plate[size_t(plate_idx)]; + BeltPurgeSignature &sig = sigs[size_t(plate_idx)]; + if (!plan.wanted) { + sig = BeltPurgeSignature{}; + to_delete.insert(to_delete.end(), existing.begin(), existing.end()); + } else if (existing.size() == 1 && model.objects[size_t(existing.front())]->instances.size() == 1 && plan.sig == sig) { + continue; + } else { + to_delete.insert(to_delete.end(), existing.begin(), existing.end()); + to_create.push_back(plate_idx); + } + } + if (to_delete.empty() && to_create.empty()) + return false; + + remove_prisms(to_delete); + + // --- (Re)create ----------------------------------------------------------- + // Build the prism as N DISCONNECTED sub-bars side by side across the belt, + // N = (filaments - 1) = the worst-case number of toolchanges on one layer. + // Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS, + // and each disconnected island slices into its own infill collection. With a + // single solid box there is one collection per layer, so the FIRST toolchange + // on a layer grabs the entire collection (consuming all of it for one swap) + // and any further swaps on that layer find nothing left and go unabsorbed + // (the classic wipe tower hides this by spilling leftover into the real + // tower; the belt prism has no fallback). One island per simultaneous swap + // lets each swap claim its own island. Total lateral footprint stays `width` + // (each island width/N wide, separated by a small gap), so per-layer capacity + // per island ~= max_flush, matching the height sizing. + // Minimal gap between sub-bars: they must stay just-separated so the slicer + // keeps them as distinct islands (hence distinct infill collections, one per + // simultaneous swap). Zero gap would union them into one collection and + // reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line + // widths also keeps gap-fill from bridging them. 1 mm is about as close as + // they can butt up while staying individually purgeable. + for (int plate_idx : to_create) { + const Plan &plan = plans[size_t(plate_idx)]; + + TriangleMesh prism_mesh; + for (int i = 0; i < plan.n_islands; ++i) { + const double lat_off = i * (plan.w_sub + gap); + // Box dims: lateral = w_sub, along-belt = length, vertical = height. + TriangleMesh box = belt_is_y ? make_cube(plan.w_sub, plan.length, plan.height) // X = lateral, Y = belt + : make_cube(plan.length, plan.w_sub, plan.height); // X = belt, Y = lateral + box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f)); + prism_mesh.merge(box); + } + + ModelObject *new_object = model.add_object(); + new_object->name = _u8L("Belt Purge Tower"); + new_object->add_instance(); + ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh)); + new_volume->name = new_object->name; + + auto &cfg = new_object->config; + cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); + cfg.set_key_value("extruder", new ConfigOptionInt(1)); + // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — + // every extrusion is overriddable, so the absorbed volume matches the + // cross-section x layer-height estimate used for the height above. + cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); + cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); + cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + cfg.set_key_value("enable_support", new ConfigOptionBool(false)); + cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + // Position by the belt-calibration pattern: drop to the bed, then translate + // the instance by the delta between the object's ACTUAL bbox center and the + // target. Setting the instance offset directly is unreliable here — the + // freshly added cube's local frame is not centered, so set_offset() lands + // the min corner (not the center) on the target, leaving the bar centered + // on the bed edge with half of it hanging off. + new_object->invalidate_bounding_box(); + new_object->ensure_on_bed(); + const BoundingBoxf3 cur = new_object->bounding_box_exact(); + new_object->translate_instances(Vec3d(plan.center.x() - cur.center().x(), + plan.center.y() - cur.center().y(), + 0.0)); + new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); + + const size_t obj_idx = model.objects.size() - 1; + // Registers the object in the sidebar and notifies the part plates; + // selection is left untouched (auto-managed object). + obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false); + + sigs[size_t(plate_idx)] = plan.sig; + } + + return true; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp new file mode 100644 index 0000000000..a009eb4d4b --- /dev/null +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +// ORCA-Belt: auto-managed purge prism for belt printers. +// +// Kept in its own translation unit (not buried in Plater.cpp) so it stays out +// of the way of unrelated upstream changes and carries no regression risk for +// normal printers: nothing here runs unless the printer is a belt printer with +// the belt purge tower enabled. See Slic3r::GUI::ensure_belt_purge_tower(). + +namespace Slic3r { +class Model; +class PartPlateList; +namespace GUI { +class ObjectList; + +// Inputs of the last belt purge prism generation. Idempotence is keyed on these +// (not the prism's resulting bbox): the prism gets nudged by plate assignment +// after creation, so comparing its bbox would falsely detect a change and +// regenerate it on every background-process tick — which would re-invalidate a +// freshly sliced result and make the G-code preview unreachable. +struct BeltPurgeSignature +{ + bool valid = false; + int filament_count = 0; + long key[14] = {0}; // rounded geometry, plate and bed inputs (0.1 mm units) + bool operator==(const BeltPurgeSignature &o) const + { + if (valid != o.valid || filament_count != o.filament_count) + return false; + for (int i = 0; i < 14; ++i) + if (key[i] != o.key[i]) + return false; + return true; + } +}; + +// Keep the auto-generated belt purge prisms, one per plate, in sync with the +// current config and plate contents. Creates / updates / removes the marked prism +// ModelObjects; `sigs` holds the last inputs per plate index. +// Returns true when the model was mutated (caller should refresh the scene). +// Runs on every background-process update, so it is idempotent: it only mutates +// the model when the desired prism differs from the cached signature in `sig`. +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector &sigs); + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 8ca79e6b64..b156f32cb8 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -16,6 +16,7 @@ #include "slic3r/GUI/GUI.hpp" #include "libslic3r/libslic3r.h" #include "slic3r/GUI/Field.hpp" +#include "libslic3r/BeltBrim.hpp" #include #include #include @@ -351,6 +352,31 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con bool is_object_config = (!is_global_config && !is_plate_config); + // Belt printer: a raft and a draft shield are refused by Print::validate(), and + // the fields that would clear them are greyed out in belt mode, so a preset that + // carries either could not be sliced at all. Reset them instead of only disabling + // the fields. + if (GUI::wxGetApp().preset_bundle != nullptr) { + const auto *belt_opt = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + const auto *raft_opt = config->option("raft_layers"); + const auto *shield_opt = config->option>("draft_shield"); + const bool has_raft = raft_opt != nullptr && raft_opt->value > 0; + const bool has_shield = shield_opt != nullptr && shield_opt->value != dsDisabled; + if (belt_opt != nullptr && belt_opt->value && (has_raft || has_shield)) { + const wxString msg_text = _(L("Raft and draft shield are not available on belt printers.\nThey have been disabled.")); + MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK); + DynamicPrintConfig new_conf = *config; + is_msg_dlg_already_exist = true; + dialog.ShowModal(); + if (has_raft) + new_conf.set_key_value("raft_layers", new ConfigOptionInt(0)); + if (has_shield) + new_conf.set_key_value("draft_shield", new ConfigOptionEnum(dsDisabled)); + apply(config, &new_conf); + is_msg_dlg_already_exist = false; + } + } + // layer_height shouldn't be equal to zero auto layer_height = config->opt_float("layer_height"); if (layer_height < EPSILON) @@ -754,9 +780,31 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in const bool gcf_is_marlin_firmware = gcflavor == GCodeFlavor::gcfMarlinFirmware; const bool gcf_is_klipper = gcflavor == GCodeFlavor::gcfKlipper; + // Belt printer: detect early since it affects multiple toggle decisions below. + // `is_belt_tilted` is the stricter test that mirrors PrintObject::has_belt_brim(): + // only a tilted belt gets the belt-plane brim, while a belt printer with no + // rotation is geometrically a flat bed and keeps the ordinary plate brim. + bool is_belt_printer = false; + bool is_belt_tilted = false; + { + const auto &printer_cfg = preset_bundle->printers.get_edited_preset().config; + const auto *belt_opt = printer_cfg.option("belt_printer"); + if (belt_opt) + is_belt_printer = belt_opt->value; + const auto *axis = printer_cfg.option>("belt_slice_rotation"); + const auto *angle = printer_cfg.option("belt_slice_rotation_angle"); + if (is_belt_printer && axis != nullptr && angle != nullptr) { + // Same window as Print::has_tilted_belt(); shared constants so the GUI and + // the backend cannot drift apart. + const double tilt = std::abs(angle->value); + is_belt_tilted = (axis->value == BeltRotationAxis::X || axis->value == BeltRotationAxis::Y) + && tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG; + } + } + bool have_volumetric_extrusion_rate_slope = config->option("max_volumetric_extrusion_rate_slope")->value > 0; float have_volumetric_extrusion_rate_slope_segment_length = config->option("max_volumetric_extrusion_rate_slope_segment_length")->value; - toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope); + toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope && !is_belt_printer); toggle_line("max_volumetric_extrusion_rate_slope_segment_length", have_volumetric_extrusion_rate_slope); toggle_line("extrusion_rate_smoothing_external_perimeter_only", have_volumetric_extrusion_rate_slope); if(have_volumetric_extrusion_rate_slope) config->set_key_value("enable_arc_fitting", new ConfigOptionBool(false)); @@ -923,27 +971,47 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in } } - bool have_skirt = config->opt_int("skirt_loops") > 0; + // Belt printer: disable skirt, brim, raft, and draft shield controls. + bool have_skirt = config->opt_int("skirt_loops") > 0 && !is_belt_printer; toggle_field("skirt_height", have_skirt && config->opt_enum("draft_shield") != dsEnabled); toggle_line("single_loop_draft_shield", have_skirt); // ORCA: Display one wall if skirt enabled for (auto el : {"skirt_type", "min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_speed", "draft_shield"}) toggle_field(el, have_skirt); + if (is_belt_printer) { + toggle_field("skirt_loops", false); + toggle_field("skirt_height", false); + } - bool have_brim = (config->opt_enum("brim_type") != btNoBrim); + // Belt printers now get a brim too, laid onto the tilted belt by BeltBrim.cpp, + // so brim type / width / object gap all apply. A belt printer with no tilt is + // geometrically a flat bed and uses the ordinary plate brim, hence the separate + // is_belt_tilted test. + bool have_brim = config->opt_enum("brim_type") != btNoBrim; toggle_field("brim_object_gap", have_brim); - toggle_field("brim_use_efc_outline", have_brim); - toggle_field("combine_brims", have_brim); - bool have_brim_width = (config->opt_enum("brim_type") != btNoBrim) && config->opt_enum("brim_type") != btAutoBrim && + // Both are first-layer-only concepts that the belt path cannot honour. + toggle_field("brim_use_efc_outline", have_brim && !is_belt_tilted); + toggle_field("combine_brims", have_brim && !is_belt_tilted); + bool have_brim_width = have_brim && config->opt_enum("brim_type") != btAutoBrim && config->opt_enum("brim_type") != btPainted; - toggle_field("brim_width", have_brim_width); + // On a tilted belt Auto / Mouse ear / Painted all collapse to outer-only at the + // configured width, so the width field has to stay live for them too. + toggle_field("brim_width", have_brim_width || (have_brim && is_belt_tilted)); toggle_field("brim_flow_ratio", have_brim); + // Paired toggle_line + toggle_field: cb_toggle_line is null in the per-object + // override panel, so the row cannot be hidden there and greying out is the + // fallback. Both extras are belt-only: one extends the brim ahead along the belt, + // the other widens it across the belt. + for (auto el : { "leading_brim_length", "extra_brim_width" }) { + toggle_line(el, is_belt_tilted); + toggle_field(el, is_belt_tilted && have_brim); + } // Wall filament selectors use the same logic as in Print::extruders(). toggle_field("outer_wall_filament_id", have_perimeters || have_brim); toggle_field("inner_wall_filament_id", have_perimeters || have_brim); const BrimType brim_type = config->opt_enum("brim_type"); - const bool have_auto_brim_ear = brim_type == btEar; - const bool have_painted_brim_ear = brim_type == btPainted; + const bool have_auto_brim_ear = brim_type == btEar && !is_belt_tilted; + const bool have_painted_brim_ear = brim_type == btPainted && !is_belt_tilted; set_option_label("brim_width", have_auto_brim_ear ? _L("Brim ear radius") : _L("Brim width")); const auto brim_width = config->opt_float("brim_width"); // Automatic brim ear settings require a non-zero brim width. @@ -958,7 +1026,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in // Hide Elephant foot compensation layers if elefant_foot_compensation is not enabled toggle_line("elefant_foot_compensation_layers", config->opt_float("elefant_foot_compensation") > 0 || config->option("elefant_foot_layers_density")->get_abs_value(1.0f) < 1.0f); - bool have_raft = config->opt_int("raft_layers") > 0; + bool have_raft = config->opt_int("raft_layers") > 0 && !is_belt_printer; + if (is_belt_printer) + toggle_field("raft_layers", false); bool have_support_material = config->opt_bool("enable_support") || have_raft; SupportType support_type = config->opt_enum("support_type"); @@ -1066,33 +1136,41 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("preheat_steps", have_ooze_prevention && (preheat_steps > 0)); bool have_prime_tower = config->opt_bool("enable_prime_tower"); + // ORCA-Belt: belt printers replace the classic wipe tower with the + // auto-generated belt purge prism, enabled by the belt-only printer option + // enable_belt_purge_tower. Its width (a process option) is only relevant + // then. (is_belt_printer is computed at the top of this function.) + const bool have_belt_purge_tower = is_belt_printer + && preset_bundle->printers.get_edited_preset().config.has("enable_belt_purge_tower") + && preset_bundle->printers.get_edited_preset().config.opt_bool("enable_belt_purge_tower"); + toggle_line("belt_purge_tower_width", have_belt_purge_tower); for (auto el : {"prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "wipe_tower_wall_type", "prime_tower_infill_gap","prime_tower_enable_framework", "enable_tower_interface_features"}) - toggle_line(el, have_prime_tower); + toggle_line(el, have_prime_tower && !is_belt_printer); toggle_line("enable_tower_interface_cooldown_during_tower", - have_prime_tower && config->opt_bool("enable_tower_interface_features")); + have_prime_tower && !is_belt_printer && config->opt_bool("enable_tower_interface_features")); bool purge_in_primetower = preset_bundle->printers.get_edited_preset().config.opt_bool("purge_in_prime_tower"); for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_max_purge_speed", "wipe_tower_bridging", "wipe_tower_extra_flow"}) - toggle_line(el, have_prime_tower && supports_wipe_tower_2); + toggle_line(el, have_prime_tower && supports_wipe_tower_2 && !is_belt_printer); // Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive. - toggle_line("wipe_tower_no_sparse_layers", have_prime_tower); + toggle_line("wipe_tower_no_sparse_layers", have_prime_tower && !is_belt_printer); // Dropping the sparse layers outright leaves nothing to combine, so the two are exclusive. - toggle_line("wipe_tower_sparse_layers_combination", have_prime_tower && !config->opt_bool("wipe_tower_no_sparse_layers")); + toggle_line("wipe_tower_sparse_layers_combination", have_prime_tower && !is_belt_printer && !config->opt_bool("wipe_tower_no_sparse_layers")); WipeTowerWallType wipe_tower_wall_type = config->opt_enum("wipe_tower_wall_type"); - bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower; - toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone); + bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower&&!is_belt_printer; + toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && !is_belt_printer && wipe_tower_wall_type == WipeTowerWallType::wtwCone); toggle_line("wipe_tower_extra_rib_length", have_rib_wall); toggle_line("wipe_tower_rib_width", have_rib_wall); toggle_line("wipe_tower_fillet_wall", have_rib_wall); - toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall); + toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall && !is_belt_printer); - toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2); + toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2 && !is_belt_printer); bool use_cyclic_ordering = config->opt_enum("toolchange_ordering") == ToolChangeOrderingType::Cyclic; toggle_line("toolchange_cyclic_order", use_cyclic_ordering); @@ -1188,8 +1266,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("small_area_infill_flow_compensation_model", have_small_area_infill_flow_compensation); - toggle_field("seam_slope_type", !has_spiral_vase); - bool has_seam_slope = !has_spiral_vase && config->opt_enum("seam_slope_type") != SeamScarfType::None; + // Belt printers: the scarf would start one layer back along the belt, inside the + // previous layer (GCode::extrude_loop skips it there too). + toggle_field("seam_slope_type", !has_spiral_vase && !is_belt_printer); + bool has_seam_slope = !has_spiral_vase && !is_belt_printer && config->opt_enum("seam_slope_type") != SeamScarfType::None; toggle_line("seam_slope_conditional", has_seam_slope); toggle_line("seam_slope_start_height", has_seam_slope); toggle_line("seam_slope_entire_loop", has_seam_slope); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 97b566530d..aa4b3ca2bb 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -11,6 +11,7 @@ //BBS: add convex hull logic for toolpath check #include "GUI_App.hpp" +#include "Shortcuts.hpp" #include "Plater.hpp" #include "Camera.hpp" #include "I18N.hpp" @@ -19,6 +20,8 @@ #include "GLCanvas3D.hpp" #include "FilamentGroupPopup.hpp" #include "GLToolbar.hpp" +#include "libslic3r/BeltTransform.hpp" +#include "libslic3r/GCode/MachineFrameTransform.hpp" #include "MsgDialog.hpp" #include #include "slic3r/GUI/MeshUtils.hpp" @@ -85,8 +88,10 @@ #include #include +#include #include #include +#include #include "libslic3r/AppConfig.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/CutUtils.hpp" @@ -1314,6 +1319,52 @@ std::vector GCodeViewer::get_plater_extruder() return m_plater_extruder; } +// Belt printers: compute the full machine->model back-transform from the print +// config, so the "designed" (upright) G-code preview maps each toolpath vertex +// back to Cartesian space. The G-code forward pipeline is (BeltKinematics:: +// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X the model +// already un-rotated to Cartesian by the back-transform. So the inverse is: +// model = AxisRemap^-1 . MachineFrame^-1 +// (origin-snap is a per-instance translation that only shifts position, not +// orientation, so it is intentionally omitted.) +static Transform3d compute_belt_back_transform(const PrintConfig& cfg) +{ + if (!cfg.belt_printer.value) + return Transform3d::Identity(); + + MachineFrameTransform mft; + mft.init_from_config(cfg); + const Transform3d mf_inv = mft.is_active() ? Transform3d(mft.transform().inverse()) + : Transform3d::Identity(); + + // Forward G-code axis remap as an affine matrix (out_i = sign * in[r_i % 3], plus a + // build-volume offset for Rev axes). This is the matrix form of the per-point + // GCodeWriter::apply_axis_remap (row convention: each OUTPUT axis selects an input + // axis + sign) and MUST stay in sync with it. The build-volume max matches what the + // writer is fed in GCode.cpp (printable_area max + printable_height). (Follow-up: + // precompute this matrix once in GCodeWriter and share it with apply_axis_remap to + // remove the parallel encoding.) + Transform3d ar = Transform3d::Identity(); + const int rr[3] = { int(cfg.gcode_remap_x.value), int(cfg.gcode_remap_y.value), int(cfg.gcode_remap_z.value) }; + if (rr[0] != 0 || rr[1] != 1 || rr[2] != 2) { + BoundingBoxf bbox_bed(cfg.printable_area.values); + const Vec3d vmax(bbox_bed.max.x(), bbox_bed.max.y(), cfg.printable_height.value); + Matrix3d M = Matrix3d::Zero(); + Vec3d t = Vec3d::Zero(); + for (int i = 0; i < 3; ++i) { + const int axis = rr[i] % 3; + if (rr[i] < 3) M(i, axis) = 1.0; + else if (rr[i] < 6) M(i, axis) = -1.0; + else { M(i, axis) = -1.0; t[i] = vmax[axis]; } + } + ar.linear() = M; + ar.translation() = t; + } + const Transform3d ar_inv = ar.inverse(); + + return ar_inv * mf_inv; +} + //BBS: always load shell at preview void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const Print& print, const std::vector& str_tool_colors, const std::vector& str_color_print_colors, const BuildVolume& build_volume, @@ -1321,6 +1372,15 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const { m_loaded_as_preview = false; + // Belt printers: drive the designed/raw view UI (legend checkbox, hotkey B, canvas-toolbar + // menu item) from the G-code itself. Only BeltGCode writes the belt header, so its tilt + // (gcode_result.belt_tilt_angle, abs of the slicing rotation) says whether this is belt + // G-code; the selected printer does not, for a file opened from disk. The back-transform + // below still reads print.config(), which Plater::load_gcode() fills from the file's own + // config block (GCodeProcessor::export_config_for_render). + m_belt_view_enabled = gcode_result.belt_tilt_angle > 0.f; + m_belt_angle_deg = gcode_result.belt_tilt_angle; + const bool current_top_layer_only = m_viewer.is_top_layer_only_view_range(); const bool required_top_layer_only = get_app_config()->get_bool("seq_top_layer_only"); if (current_top_layer_only != required_top_layer_only) @@ -1330,8 +1390,12 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_viewer.set_dim_previous_layers(get_app_config()->get_bool("preview_dim_previous_layers")); m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness"))); - // avoid processing if called with the same gcode_result - if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) { + // avoid processing if called with the same gcode_result. + // On a belt printer the toolpath geometry fed to libvgcode also depends on the + // designed/raw view state (the back-transform is applied in convert), so the + // same result is converted again only when that view has been toggled. + const bool same_belt_view = !m_belt_view_enabled || m_last_belt_show_designed == m_belt_show_designed; + if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && same_belt_view) { //BBS: add logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result); @@ -1372,8 +1436,112 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const return; } - // convert data from PrusaSlicer format to libvgcode format - libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer); + // convert data from PrusaSlicer format to libvgcode format. + // Belt printers: when the designed (upright) view is active, back-transform + // the toolpath geometry into model/Cartesian space using the general belt + // inverse (handles the mesh rotation, shear and axis remap). When off, the + // raw machine-frame G-code is shown (useful for checking the transform itself). + const bool is_belt = m_belt_view_enabled && print.config().belt_printer.value; + Transform3d belt_inv = (is_belt && m_belt_show_designed) + ? compute_belt_back_transform(print.config()) : Transform3d::Identity(); + // Belt: move positions are stored as gcode_Z + belt_z_origin (the start G-code's + // purge-blob advance baked into the machine-Z origin by its G92 Z0 resets). Subtract + // that constant before the linear back-transform so every toolpath maps to the model's + // belt coordinate. Without it the back-transform mixes the offset with the gantry-Y + // term, leaving a per-move designed-Y error that min-corner anchoring cannot remove + // when a bridge/keel move happens to cancel it at the bbox minimum. + if (is_belt && m_belt_show_designed && gcode_result.belt_z_origin != 0.0f) + belt_inv = belt_inv * Transform3d(Eigen::Translation3d(Vec3d(0.0, 0.0, -double(gcode_result.belt_z_origin)))); + const bool apply_belt = is_belt && m_belt_show_designed + && !belt_inv.matrix().isApprox(Transform3d::Identity().matrix()); + if (apply_belt) { + // The linear belt back-transform recovers the print's shape and orientation but not + // the per-object placement/lift translation: the object's position on the belt, the + // BeltSliceStrategy min-Z lift, and the centering pre-translate are applied OUTSIDE + // build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse leaves + // them un-undone. Uncorrected, the upright toolpaths float a fixed offset from the + // model shell. Recover the translation generally — independent of the offset's exact + // source or the axis remap — by anchoring the back-transformed object body onto the + // upright model bounding box (the same space the shells render in). + BoundingBoxf3 model_bb; + for (const PrintObject* po : print.objects()) + for (const ModelInstance* mi : po->model_object()->instances) + model_bb.merge(po->model_object()->instance_bounding_box(*mi)); + // Build the anchor bbox from surface toolpaths only. After the belt_z_origin + // correction the surface back-transforms onto the model, but a few elevated + // features (bridges/overhangs over the chevron gap) are mis-mapped by the linear + // inverse to well outside the model body; if one becomes the bbox minimum it + // drags the min-corner anchor by ~20mm. Drop moves that land clearly outside the + // (correct) model bbox — a geometric filter, not a role guess. + // Tolerance around the model bbox for outlier rejection, and the minimum fraction + // of MOVES (by count) the clip must retain before we trust it over the full bbox. + // Count, not bbox extent: a single far-flung outlier move (e.g. a belt-entry purge + // that maps to y~0) inflates the full bbox so much that a size-fraction test would + // wrongly reject the clip and re-include the outlier, dragging the min-corner anchor + // by ~the offset. By count the clip keeps ~100% of moves here and ~0% only when the + // object is genuinely offset from the belt entry (the real fall-back-to-full case). + constexpr double ANCHOR_CLIP_MARGIN_MM = 10.0; + constexpr double ANCHOR_CLIP_MIN_KEEP = 0.5; + BoundingBoxf3 tp_bb_clip, tp_bb_full; + size_t n_filtered = 0, n_clip = 0; + const double y_lo = model_bb.defined ? model_bb.min.y() - ANCHOR_CLIP_MARGIN_MM : -1e30; + const double y_hi = model_bb.defined ? model_bb.max.y() + ANCHOR_CLIP_MARGIN_MM : 1e30; + // The anchor aligns the toolpath body onto the object-only model bbox, so it must be + // built from OBJECT-body toolpaths only. Support, skirt, brim and wipe-tower extrusions + // are not part of the model mesh and extend past it (support especially reaches well + // beyond the object on a belt), so including them drags tp_bb.min and shifts the whole + // back-transformed g-code off the mesh — but only when those features are present. That + // is the "g-code shifts vs mesh as soon as supports are enabled" bug. Filter them out. + auto is_object_body = [](ExtrusionRole r) { + return r != erSupportMaterial && r != erSupportMaterialInterface + && r != erSupportTransition + && r != erSkirt && r != erBrim && r != erWipeTower; + }; + auto accumulate = [&](bool body_only) { + tp_bb_clip = BoundingBoxf3(); + tp_bb_full = BoundingBoxf3(); + n_filtered = 0; + n_clip = 0; + for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) + if (mv.type == EMoveType::Extrude && mv.layer_id >= 1 // skip layer-0 prime/skirt + && (!body_only || is_object_body(mv.extrusion_role))) { + ++n_filtered; + const Vec3d p = belt_inv * mv.position.cast(); + tp_bb_full.merge(p); + if (p.y() >= y_lo && p.y() <= y_hi) { + tp_bb_clip.merge(p); + ++n_clip; + } + } + }; + accumulate(/*body_only=*/true); + // Fallback: a plate with no object-body extrusions above layer 0 (e.g. a + // support-only object) would leave tp_bb undefined and silently skip the + // anchor. Re-accumulate over ALL extrude moves so the min-corner anchor is + // still computed rather than letting the preview float by the placement offset. + if (n_filtered == 0) + accumulate(/*body_only=*/false); + // Use the clipped bbox when it still holds the bulk of the moves (outliers removed). + // If the object sits far from the belt entry the toolpaths are grossly offset and the + // clip drops most of them — fall back to the full bbox so the min-corner anchor still + // recovers that gross translation rather than breaking. + const BoundingBoxf3& tp_bb = + (tp_bb_clip.defined && n_filtered > 0 && + double(n_clip) >= ANCHOR_CLIP_MIN_KEEP * double(n_filtered)) ? tp_bb_clip : tp_bb_full; + if (model_bb.defined && tp_bb.defined) { + // Anchor the back-transformed toolpath body onto the upright model bbox by + // its MIN corner. (Center anchoring was tried and regressed when the toolpath + // and model bounding boxes differ in extent.) With the belt_z_origin + // correction above and the outlier-robust bbox below, the surface overlaps the + // shell to well under a millimetre when the object is at the belt entry; an + // object placed elsewhere in global-rotation mode still carries the placement + // translation, which this min-corner step recovers. + const Vec3d d = model_bb.min - tp_bb.min; + belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv; + } + } + libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, + apply_belt ? &belt_inv : nullptr); //#define ENABLE_DATA_EXPORT 1 //#if ENABLE_DATA_EXPORT @@ -1471,6 +1639,26 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_viewer.reset_default_extrusion_roles_colors(); m_viewer.load(std::move(data)); + // Belt printers: libvgcode labels a layer with the height of its toolpaths, which + // on a tilted layer is wherever its last extrusion happened to end, and the layer + // slider looks its labels and the colour-change ticks up in that list assuming it + // increases. Give it the layers' print Z instead (the slicer's layer Z, which + // increases along the belt), numbered the way libvgcode::convert() numbers the + // layers: consecutively over the moves that exist. + m_belt_layer_zs.clear(); + if (is_belt) { + unsigned int src_layer_id = std::numeric_limits::max(); + for (size_t i = 1; i < gcode_result.moves.size(); ++ i) { + const GCodeProcessorResult::MoveVertex &mv = gcode_result.moves[i]; + if (mv.layer_id != src_layer_id) { + src_layer_id = mv.layer_id; + m_belt_layer_zs.emplace_back(double(mv.print_z)); + } + } + if (m_belt_layer_zs.size() != m_viewer.get_layers_count()) + m_belt_layer_zs.clear(); + } + // #if !VGCODE_ENABLE_COG_AND_TOOL_MARKERS // const size_t vertices_count = m_viewer.get_vertices_count(); // m_cog.reset(); @@ -1504,13 +1692,24 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_paths_bounding_box = BoundingBoxf3(libvgcode::convert(bbox[0]).cast(), libvgcode::convert(bbox[1]).cast()); m_max_bounding_box = m_paths_bounding_box; - if (wxGetApp().is_editor()) - m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box); + if (wxGetApp().is_editor()) { + if (is_belt) { + // The moves are machine-frame coordinates (Z is belt travel), so the per-move + // test inside all_paths_inside() can never pass on a belt. Judge the + // back-transformed box instead, with room for the designed view's min-corner + // anchor, which is only accurate to a fraction of a millimetre. + BoundingBoxf3 bed = wxGetApp().plater()->build_volume().bounding_volume(); + bed.offset(1.); + m_contained_in_bed = !m_paths_bounding_box.defined || (bed.contains(m_paths_bounding_box.min) && bed.contains(m_paths_bounding_box.max)); + } else + m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box); + } m_extruders_count = gcode_result.filaments_count; //BBS: move the id to the end of reset m_last_result_id = gcode_result.id; + m_last_belt_show_designed = m_belt_show_designed; m_gcode_result = &gcode_result; m_move_type_counts.fill(0); for (auto& move_type_times : m_move_type_times) @@ -1541,8 +1740,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_custom_gcode_per_print_z = gcode_result.custom_gcode_per_print_z; m_max_print_height = gcode_result.printable_height; + m_machine_frame_transform_active = gcode_result.machine_frame_transform_active; m_z_offset = gcode_result.z_offset; + // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); // ORCA: Apply the default view type now that we know how many tools the G-code actually uses. @@ -1728,6 +1929,7 @@ void GCodeViewer::reset() //BBS: should also reset the result id BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current result id %1% ")%m_last_result_id; m_last_result_id = -1; + m_belt_layer_zs.clear(); //BBS: add only gcode mode m_only_gcode_in_preview = false; @@ -1736,6 +1938,7 @@ void GCodeViewer::reset() m_paths_bounding_box = BoundingBoxf3(); m_max_bounding_box = BoundingBoxf3(); m_max_print_height = 0.0f; + m_machine_frame_transform_active = false; m_z_offset = 0.0f; m_extruders_count = 0; m_filament_diameters = std::vector(); @@ -2554,7 +2757,14 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); - const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(static_cast(camera.get_view_matrix().matrix().cast())); + Matrix4f view = camera.get_view_matrix().matrix().cast(); + // Belt "designed" (upright) view is produced by back-transforming the toolpath + // GEOMETRY into model space at load time (see load_as_gcode -> + // compute_belt_back_transform -> libvgcode::convert). The camera is left untouched + // here; transforming the view as well would double-apply the inverse. Baking the + // transform into the geometry (not the camera) also keeps the bed and toolpaths in + // the same frame so they stay aligned. + const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS m_viewer.set_cog_marker_scale_factor(m_cog_marker_fixed_screen_size ? 10.0f * m_cog_marker_size * camera.get_inv_zoom() : m_cog_marker_size); @@ -4930,6 +5140,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab render_legend_color_arr_recommen(window_padding); + legend_height = ImGui::GetCurrentWindow()->Size.y; imgui.end(); ImGui::PopStyleColor(7); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 764fc2c0d6..c038aec671 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -199,6 +199,10 @@ private: std::vector m_plater_extruder; bool m_gl_data_initialized{ false }; unsigned int m_last_result_id{ 0 }; + // Belt printers: the view the loaded result was converted for (see load_as_gcode). + bool m_last_belt_show_designed{ true }; + // Belt printers: the print Z of each viewer layer, in the viewer's layer numbering. + std::vector m_belt_layer_zs; //BBS: save m_gcode_result as well const GCodeProcessorResult* m_gcode_result; std::array(EMoveType::Count)> m_move_type_counts{}; @@ -219,6 +223,7 @@ private: //BBS: add shell bounding box BoundingBoxf3 m_shell_bounding_box; float m_max_print_height{ 0.0f }; + bool m_machine_frame_transform_active{ false }; float m_z_offset{ 0.0f }; ConfigOptionMode m_user_mode; @@ -260,6 +265,11 @@ private: mutable bool m_no_render_path { false }; bool m_is_dark = false; + bool m_belt_view_enabled = false; + bool m_belt_show_designed = true; // Designed (upright, back-transformed) view by default; off shows + // the raw machine-frame G-code (canvas view menu, hotkey B). + float m_belt_angle_deg = 0.f; + libvgcode::Viewer m_viewer; // ORCA: section view, as the viewer has it. What it cuts away casts no shadow. std::array m_clipping_plane{ 0.0f, 0.0f, 0.0f, 1.0f }; @@ -316,10 +326,15 @@ public: std::vector get_plater_extruder(); const float get_max_print_height() const { return m_max_print_height; } + bool is_machine_frame_transform_active() const { return m_machine_frame_transform_active; } const BoundingBoxf3& get_paths_bounding_box() const { return m_paths_bounding_box; } const BoundingBoxf3& get_max_bounding_box() const { return m_max_bounding_box; } const BoundingBoxf3& get_shell_bounding_box() const { return m_shell_bounding_box; } std::vector get_layers_zs() const { + // Belt printers: the layer Z the slider labels and the colour-change ticks + // use is the layer's print Z (see load_as_gcode), not a toolpath height. + if (! m_belt_layer_zs.empty()) + return m_belt_layer_zs; const std::vector zs = m_viewer.get_layers_zs(); std::vector ret; std::transform(zs.begin(), zs.end(), std::back_inserter(ret), [](float z) { return static_cast(z); }); @@ -395,6 +410,11 @@ public: void export_toolpaths_to_obj(const char* filename) const; + void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; } + bool is_belt_view() const { return m_belt_view_enabled && m_belt_angle_deg > 0.f; } + void toggle_belt_show_designed() { if (m_belt_view_enabled) m_belt_show_designed = !m_belt_show_designed; } + bool is_belt_show_designed() const { return m_belt_show_designed; } + size_t get_extruders_count() { return m_extruders_count; } void push_combo_style(); void pop_combo_style(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 94ecfb69b6..2cc6c6747f 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3176,7 +3176,13 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re need_wipe_tower |= dynamic_cast(dconfig.option("enable_wrapping_detection"))->value; } - if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) { + // Belt printers replace the classic wipe tower with the auto-generated + // belt purge prism (a real model object), so never draw the tower widget. + bool is_belt_printer = false; + if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer")) + is_belt_printer = belt_opt->value; + + if (wt && !is_belt_printer && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) { // The tower size estimate reads printer- and filament-scope keys, which the print preset // does not carry; built once here rather than per plate. const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(); @@ -3244,6 +3250,28 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } update_volumes_colors_by_extruder(); + + // ORCA-Belt: render the auto-generated belt purge prism like a wipe tower — + // semi-transparent, in its (filament) color. It is a real sliced object, so + // the G-code preview already shows the actual per-layer purge colors; here in + // the editor we just make the block translucent so it reads as a purge tower + // rather than a solid part. update_colors_by_extruder() preserves alpha when + // not updating alpha, so lowering it once sticks across recolors. + if (m_model != nullptr) { + for (GLVolume *volume : m_volumes.volumes) { + if (volume == nullptr || volume->volume_idx() < 0) + continue; + const int obj_idx = volume->object_idx(); + if (obj_idx < 0 || obj_idx >= (int) m_model->objects.size()) + continue; + const ConfigOption *opt = m_model->objects[obj_idx]->config.option("belt_purge_tower_object"); + if (opt != nullptr && opt->getBool()) { + volume->color.a(0.66f); + volume->force_transparent = true; + } + } + } + // Update selection indices based on the old/new GLVolumeCollection. if (m_selection.get_mode() == Selection::Instance) m_selection.instances_changed(instance_ids_selected); @@ -3782,6 +3810,16 @@ bool GLCanvas3D::handle_shortcut(const KeyChord& chord) get_gcode_viewer().get_layers_slider()->switch_one_layer_mode(); m_dirty = true; break; + case Shortcut::ToggleBeltRawGcode: + // Same state as the canvas view menu item. The designed-view back-transform is + // baked into the toolpaths at load time, so the preview is re-converted. + if (m_gcode_viewer.is_belt_view()) { + m_gcode_viewer.toggle_belt_show_designed(); + if (Plater* plater = wxGetApp().plater()) + plater->refresh_belt_view(); + m_dirty = true; + } + break; case Shortcut::GoToLayer: if (!m_gizmos.is_enabled()) { get_gcode_viewer().get_layers_slider()->show_go_to_layer(true); @@ -10224,6 +10262,7 @@ void GLCanvas3D::_render_canvas_toolbar() ImGui::TextColored(enable ? ImVec4(1,1,1,1) : ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled), "%s", into_u8(condition ? ImGui::VisibleIcon : ImGui::HiddenIcon).c_str()); }; + create_menu_item( _utf8(L("3D Navigator")), m_canvas_type != ECanvasType::CanvasAssembleView, // not work on assembly wxGetApp().show_3d_navigator(), @@ -10311,6 +10350,22 @@ void GLCanvas3D::_render_canvas_toolbar() [p]{p->show_view3D_labels(!p->are_view3D_labels_shown());} ); + // Belt printers, G-code preview only: show the raw machine-frame G-code instead of + // the designed (upright) view. This menu is the only place the toggle lives (plus + // its shortcut); the reload is deferred (CallAfter) so the preview is not rebuilt + // mid-render. + if (m_canvas_type == ECanvasType::CanvasPreview && m_gcode_viewer.is_belt_view()) { + ImGui::Separator(); + create_menu_item( _utf8(L("Show raw G-code (belt only)")), + true, + !m_gcode_viewer.is_belt_show_designed(), // eye lit = raw machine-frame G-code (designed view off) + [this, p]{ + m_gcode_viewer.toggle_belt_show_designed(); + p->CallAfter([p]{ p->refresh_belt_view(); }); + } + ); + } + ImGui::PopItemFlag(); ImGui::EndPopup(); } @@ -11301,7 +11356,11 @@ void GLCanvas3D::_set_warning_notification_if_needed(EWarning warning) if (current_printer_technology() != ptSLA) { unsigned int max_z_layer = m_gcode_viewer.get_layers_z_range().back(); if (warning == EWarning::ToolHeightOutside) // check if max z_layer height exceed max print height - show = m_gcode_viewer.has_data() && (m_gcode_viewer.get_layers_zs()[max_z_layer] - m_gcode_viewer.get_max_print_height() >= 1e-6); + // Belt printer with active post-gcode machine-frame transform: layer Z values + // live in the machine frame, not the build-volume frame, so the comparison + // against printable_height is meaningless. Suppress the warning entirely. + show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_machine_frame_transform_active() + && (m_gcode_viewer.get_layers_zs()[max_z_layer] - m_gcode_viewer.get_max_print_height() >= 1e-6); else if (warning == EWarning::ToolpathOutside) { // check if max x,y coords exceed bed area show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed() && (m_gcode_viewer.get_max_print_height() -m_gcode_viewer.get_layers_zs()[max_z_layer] >= 1e-6); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 66362cf213..23ae29d8d7 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -121,6 +121,7 @@ // This is the only place where we want to allow that, so define an override macro. #define SLIC3R_ALLOW_LIBSLIC3R_I18N_IN_SLIC3R #include "libslic3r/I18N.hpp" +#include "libslic3r/Point.hpp" #undef SLIC3R_ALLOW_LIBSLIC3R_I18N_IN_SLIC3R #include "slic3r/GUI/I18N.hpp" @@ -171,6 +172,7 @@ #include #include "libslic3r/Utils.hpp" +#include "libslic3r/Geometry.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/InstanceLock.hpp" @@ -10113,5 +10115,17 @@ bool is_support_filament(int extruder_id, bool strict_check) return support_option->get_at(0); }; +Vec3d build_plate_tilt_up_direction() +{ + const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto *opt_x = cfg.option("build_plate_tilt_x"); + const auto *opt_y = cfg.option("build_plate_tilt_y"); + const double tilt_x = opt_x != nullptr ? opt_x->value : 0.; + const double tilt_y = opt_y != nullptr ? opt_y->value : 0.; + if (tilt_x == 0. && tilt_y == 0.) + return Vec3d::UnitZ(); + return Vec3d(std::tan(Geometry::deg2rad(tilt_y)), std::tan(Geometry::deg2rad(tilt_x)), 1.).normalized(); +} + } // GUI } //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 91efa51377..8b9ad1bae7 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -30,6 +30,7 @@ #include "slic3r/GUI/Jobs/UpgradeNetworkJob.hpp" #include "slic3r/GUI/HttpServer.hpp" #include "../Utils/PrintHost.hpp" +#include "libslic3r/Point.hpp" #include #include @@ -878,6 +879,8 @@ bool is_support_filament(int extruder_id, bool strict_check = true); bool is_soluble_filament(int extruder_id); // check if the filament for model is in the list bool has_filaments(const std::vector& model_filaments); +// Up direction of the edited printer's tilted build plate (+Z when untilted). +Vec3d build_plate_tilt_up_direction(); } // namespace GUI } // Slic3r diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 239e29ba41..28c0c1ceb4 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -110,14 +110,14 @@ std::map> SettingsFactory::OBJECT_C {"make_overhang_printable_angle","", 8},{"make_overhang_printable_hole_size","",9}, {"wall_sequence","",10}, {"precise_z_height", "",10} }}, - { L("Support"), {{"brim_type", "",1},{"brim_width", "",2},{"brim_object_gap", "",3},{"brim_flow_ratio", "",4},{"brim_use_efc_outline", "",5}, - {"enable_support", "",6},{"support_type", "",7},{"support_threshold_angle", "",8}, {"support_threshold_overlap", "",9}, {"support_on_build_plate_only", "",10}, - {"support_filament", "",11},{"support_interface_filament", "",12},{"support_expansion", "",13},{"support_style", "",14}, - {"tree_support_brim_width", "",15}, {"tree_support_branch_angle", "",16},{"tree_support_branch_angle_organic","",17}, {"tree_support_wall_count", "",18},{"tree_support_branch_diameter_angle", "",19},//tree support - {"support_bottom_z_distance", "",20},{"support_top_z_distance", "",21},{"support_base_pattern", "",22},{"support_base_pattern_spacing", "",23}, - {"support_interface_top_layers", "",24},{"support_interface_bottom_layers", "",25},{"support_interface_spacing", "",26},{"support_bottom_interface_spacing", "",27}, - {"support_object_xy_distance", "",28}, {"bridge_no_support", "",29},{"max_bridge_length", "",30},{"support_critical_regions_only", "",31},{"support_remove_small_overhang","",32}, - {"support_object_first_layer_gap","",33} + { L("Support"), {{"brim_type", "",1},{"brim_width", "",2},{"leading_brim_length", "",3},{"extra_brim_width", "",4},{"brim_object_gap", "",5},{"brim_flow_ratio", "",6},{"brim_use_efc_outline", "",7}, + {"enable_support", "",8},{"support_type", "",9},{"support_threshold_angle", "",10}, {"support_threshold_overlap", "",11}, {"support_on_build_plate_only", "",12}, + {"support_filament", "",13},{"support_interface_filament", "",14},{"support_expansion", "",15},{"support_style", "",16}, + {"tree_support_brim_width", "",17}, {"tree_support_branch_angle", "",18},{"tree_support_branch_angle_organic","",19}, {"tree_support_wall_count", "",20},{"tree_support_branch_diameter_angle", "",21},//tree support + {"support_bottom_z_distance", "",22},{"support_top_z_distance", "",23},{"support_base_pattern", "",24},{"support_base_pattern_spacing", "",25}, + {"support_interface_top_layers", "",26},{"support_interface_bottom_layers", "",27},{"support_interface_spacing", "",28},{"support_bottom_interface_spacing", "",29}, + {"support_object_xy_distance", "",30}, {"bridge_no_support", "",31},{"max_bridge_length", "",32},{"support_critical_regions_only", "",33},{"support_remove_small_overhang","",34}, + {"support_object_first_layer_gap","",35} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} @@ -1850,7 +1850,7 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men while (menu->GetMenuItemCount() > 0) menu->Destroy(menu->FindItemByPosition(0)); - //if (init) { // + //if (init) { // append_menu_item( menu, wxID_ANY, _L("Edit"), "", [](wxCommandEvent&) { plater()->sidebar().edit_filament(); }, "", nullptr, diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 47e391b2ed..21210b06b7 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2701,7 +2701,7 @@ void ObjectList::load_mesh_object(const std::vectorbuild_volume().bounding_volume2d().center(); + auto start_point = wxGetApp().plater()->build_volume().bed_center(); auto empty_cell = wxGetApp().plater()->canvas3D()->get_nearest_empty_cell({start_point(0), start_point(1)}); new_object->instances[0]->set_offset(center ? to_3d(Vec2d(empty_cell(0), empty_cell(1)), -new_object->origin_translation.z()) : bb.center()); diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index fd477cf767..3358715885 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -369,6 +369,25 @@ void Preview::reload_print(bool only_gcode) m_only_gcode = only_gcode; } +void Preview::refresh_belt_view() +{ + // Re-run the G-code preview conversion so the belt "designed view" toggle takes effect + // (the back-transform is baked into the toolpath geometry in GCodeViewer::load_as_gcode, + // whose same-result cache also keys on the view state, so the re-convert runs). + // Reset m_loaded_print to bypass the "already loaded" guard the way reload_print does, but + // keep the current layer range and only-gcode mode so the view doesn't jump on toggle. + // The layer Z values differ between the designed and the raw view (the raw view's are + // machine-frame heights), so keep_z_range alone cannot find the old span: carry the + // slider over by layer index instead. + IMSlider *layers_slider = m_canvas->get_gcode_viewer().get_layers_slider(); + const int lower = layers_slider->GetLowerValue(); + const int higher = layers_slider->GetHigherValue(); + m_loaded_print = nullptr; + load_print(true /*keep_z_range*/, m_only_gcode); + if (higher <= layers_slider->GetMaxValue()) + layers_slider->SetSelectionSpan(lower, higher); +} + //BBS: always load shell at preview void Preview::load_shells(const Print& print, bool force_previewing) { diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index b8bf09bd65..acb3211fd9 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -148,6 +148,8 @@ public: //BBS: add only gcode mode void load_print(bool keep_z_range = false, bool only_gcode = false); void reload_print(bool only_gcode = false); + // Belt printers: re-convert the G-code preview so the "designed view" toggle takes effect. + void refresh_belt_view(); //BBS: always load shell at preview void load_shells(const Print& print, bool force_previewing = false); void reset_shells(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index fb2137982a..9b390e8c64 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -552,6 +552,11 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const ModelObject* mo = m_c->selection_info()->model_object(); const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + // Compute gravity direction accounting for build plate tilt + const Vec3d up_dir = build_plate_tilt_up_direction(); + const bool has_tilt = up_dir != Vec3d::UnitZ(); + const Vec3d gravity_dir = -up_dir; + int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { if (! mv->is_model_part()) @@ -560,10 +565,17 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) ++mesh_id; const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); - Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); - Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); - - float dot_limit = limit.dot(down); + Vec3f down = (trafo_matrix.inverse() * gravity_dir).cast().normalized(); + float dot_limit; + if (!has_tilt) { + // Exact upstream computation: threshold derived from a tilted limit + // vector transformed into mesh space, so non-uniform/mirror/shear + // transforms behave identically to upstream. + Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); + dot_limit = limit.dot(down); + } else { + dot_limit = std::cos(threshold); + } // Now calculate dot product of vert_direction and facets' normals. int idx = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index f6266a0da0..81d4252be5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -108,6 +108,11 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl return clp_data_out; } +Vec3f GLGizmoPainterBase::get_tilt_up_direction() const +{ + return build_plate_tilt_up_direction().cast(); +} + void GLGizmoPainterBase::render_triangles(const Selection& selection) const { auto* shader = wxGetApp().get_shader("mm_gouraud"); @@ -154,11 +159,15 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const float normal_z = -::cos(Geometry::deg2rad(m_highlight_by_angle_threshold_deg)); Matrix3f normal_matrix = static_cast(trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); + // Compute up direction accounting for build plate tilt + Vec3f up_direction = get_tilt_up_direction(); + shader->set_uniform("volume_world_matrix", trafo_matrix); shader->set_uniform("volume_mirrored", is_left_handed); shader->set_uniform("slope.actived", m_parent.is_using_slope()); shader->set_uniform("slope.volume_world_normal_matrix", normal_matrix); shader->set_uniform("slope.normal_z", normal_z); + shader->set_uniform("slope.up_direction", up_direction); m_triangle_selectors[mesh_id]->render(m_imgui, trafo_matrix); if (is_left_handed) @@ -743,7 +752,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -841,7 +850,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous std::unique_ptr cursor = TriangleSelector::SinglePointCursor::cursor_factory(phr.z_world, camera_pos, m_cursor_height, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(phr.first_facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); m_triangle_selectors[mesh_idx]->request_update_render_data(true); m_last_mouse_click = _mouse_position; @@ -893,7 +902,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true); @@ -912,12 +921,12 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); m_triangle_selectors[mesh_idx]->select_patch(int(first_position.facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, - m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } else { for (auto first_position_it = projected_mouse_positions.cbegin(); first_position_it != projected_mouse_positions.cend() - 1; ++first_position_it) { auto second_position_it = first_position_it + 1; std::unique_ptr cursor = TriangleSelector::DoublePointCursor::cursor_factory(first_position_it->mesh_hit, second_position_it->mesh_hit, camera_pos, m_cursor_radius, m_cursor_type, trafo_matrix, clp); - m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction()); } } } @@ -991,7 +1000,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), false); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index a98f303fe1..60ae6f02a3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -294,6 +294,9 @@ protected: bool m_paint_on_overhangs_only = false; float m_highlight_by_angle_threshold_deg = 0.f; + // Returns the up direction accounting for build plate tilt (default: UnitZ) + Vec3f get_tilt_up_direction() const; + GLModel m_circle; Vec2d m_old_center{ Vec2d::Zero() }; float m_old_cursor_radius{ 0.0f }; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index cb39444db2..8f9d013bb6 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -37,6 +37,11 @@ #include #include "libslic3r/LifecycleEvents.hpp" #include +#include "libslic3r/Geometry.hpp" +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Config.hpp" +#include +#include #define SAVE_ARRANGE_POLY 0 @@ -85,6 +90,14 @@ public: } }; +// The belt purge prism is generated from the arranged parts (ensure_belt_purge_tower), +// so arrange neither moves it nor packs around it; it reserves the prism's strip instead. +static bool is_belt_purge_prism(const ModelObject *mo) +{ + const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); + return opt != nullptr && opt->getBool(); +} + // BBS: add partplate logic static WipeTower get_wipe_tower(const Plater &plater, int plate_idx) { @@ -99,6 +112,63 @@ arrangement::ArrangePolygon get_wipetower_arrange_poly(WipeTower* tower) return ap; } +// Belt printers pack their parts against the edges of the bed, so what has to stay +// free there is reserved with fixed virtual items on every plate, the way the bed's +// own exclusion areas are: +// - the strip the purge prism comes back to, flush with the far lateral edge (see +// ensure_belt_purge_tower), when the parts use more than one filament; +// - the brim along every edge: a belt brim is printed brim_width wide for every brim +// type but none. Between parts the brims may overlap, as on any printer. +void ArrangeJob::prepare_belt_regions(int num_plates) +{ + if (!params.is_belt || params.is_seq_print) + return; + const DynamicPrintConfig &config = wxGetApp().preset_bundle->full_config(); + const BoundingBoxf bed = get_extents(config.opt("printable_area")->values); + const bool belt_is_y = params.belt_axis == 1; + std::vector regions; + + std::set filaments; + for (const ArrangePolygons *items : { &m_selected, &m_unselected }) + for (const ArrangePolygon &ap : *items) + if (!ap.is_virt_object) + filaments.insert(ap.extrude_ids.begin(), ap.extrude_ids.end()); + if (config.opt_bool("enable_belt_purge_tower") && filaments.size() > 1) { + // The prism is at least one millimetre per island plus the gaps between them. + const int islands = int(filaments.size()) - 1; + const double width = std::max(config.opt_float("belt_purge_tower_width"), 2. * islands - 1.) + 1.; // + the prism's edge inset + BoundingBoxf strip = bed; + if (belt_is_y) + strip.min.x() = std::max(bed.min.x(), bed.max.x() - width); + else + strip.min.y() = std::max(bed.min.y(), bed.max.y() - width); + regions.push_back(strip); + } + + // Virtual items are inflated by the one millimetre exclusion gap already. + const double brim = config.opt_enum("brim_type") == btNoBrim ? 0. : + config.opt_float("brim_width") + config.opt_float("brim_object_gap") + config.opt_float("extra_brim_width") - 1.; + if (brim > 0.) { + regions.emplace_back(bed.min, Vec2d(bed.min.x() + brim, bed.max.y())); + regions.emplace_back(Vec2d(bed.max.x() - brim, bed.min.y()), bed.max); + regions.emplace_back(bed.min, Vec2d(bed.max.x(), bed.min.y() + brim)); + regions.emplace_back(Vec2d(bed.min.x(), bed.max.y() - brim), bed.max); + } + + for (int j = 0; j < num_plates; ++j) + for (size_t i = 0; i < regions.size(); ++i) { + ArrangePolygon ap; + ap.poly.contour = scaled(regions[i]).polygon(); + ap.translation = Vec2crd(0, 0); + ap.rotation = 0.f; + ap.is_virt_object = true; + ap.bed_idx = j; + ap.height = 1; + ap.name = "BeltRegion" + std::to_string(i); + m_unselected.emplace_back(std::move(ap)); + } +} + void ArrangeJob::clear_input() { const Model &model = m_plater->model(); @@ -150,6 +220,8 @@ void ArrangeJob::prepare_selected() { for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { const Selection::InstanceIdxsList* instlist = obj_sel[oidx]; ModelObject* mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; std::vector inst_sel(mo->instances.size(), false); @@ -198,6 +270,7 @@ void ArrangeJob::prepare_selected() { } prepare_wipe_tower(); + prepare_belt_regions(MAX_NUM_PLATES); // The strides have to be removed from the fixed items. For the @@ -228,6 +301,8 @@ void ArrangeJob::prepare_all() { // Go through the objects and check if inside the selection for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { ModelObject *mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; for (size_t i = 0; i < mo->instances.size(); ++i) { ModelInstance * mi = mo->instances[i]; @@ -274,6 +349,7 @@ void ArrangeJob::prepare_all() { // add the virtual object into unselect list if has plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, MAX_NUM_PLATES); + prepare_belt_regions(MAX_NUM_PLATES); } arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set& extruder_ids) @@ -312,6 +388,11 @@ void ArrangeJob::prepare_wipe_tower() bool enable_prime_tower = op && op->getBool(); if (!enable_prime_tower || params.is_seq_print) return; + // Belt printers have no classic wipe tower; purging goes into the belt purge + // prism, whose strip prepare_belt_regions() reserves. + if (params.is_belt) + return; + bool smooth_timelapse = false; auto sop = current_config.option("timelapse_type"); if (sop) { smooth_timelapse = sop->getInt() == TimelapseType::tlSmooth; } @@ -415,6 +496,8 @@ void ArrangeJob::prepare_partplate() { for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { ModelObject* mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; for (size_t inst_idx = 0; inst_idx < mo->instances.size(); ++inst_idx) { bool in_plate = plate->contain_instance(oidx, inst_idx) || plate->intersect_instance(oidx, inst_idx); @@ -450,6 +533,7 @@ void ArrangeJob::prepare_partplate() { // add the virtual object into unselect list if has plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, current_plate_index + 1); + prepare_belt_regions(current_plate_index + 1); } //BBS: add partplate logic @@ -808,6 +892,19 @@ arrangement::ArrangeParams init_arrange_params(Plater *p) params.is_seq_print = settings.is_seq_print; params.min_obj_distance = scaled(settings.distance); params.align_to_y_axis = settings.align_to_y_axis; + if (print_config.belt_printer.value) { + // Parts print in belt order: the belt runs across the gantry's tilt axis, a + // rotation about X prints toward +Y and one about Y toward -X (see + // BeltTransform), a negative angle flips that, and a tilted layer reaches + // cot(angle) * height past a part's far edge. + const BeltRotationAxis axis = print_config.belt_slice_rotation.value; + const double angle = print_config.belt_slice_rotation_angle.value; + const bool tilted = (axis == BeltRotationAxis::X || axis == BeltRotationAxis::Y) && std::abs(angle) > EPSILON; + params.is_belt = true; + params.belt_axis = axis == BeltRotationAxis::Y ? 0 : 1; + params.belt_reversed = tilted && ((axis == BeltRotationAxis::Y) != (angle < 0.)); + params.belt_tilt_slope = tilted ? float(1. / std::tan(Geometry::deg2rad(std::clamp(std::abs(angle), 5., 90.)))) : 0.f; + } int state = p->get_prepare_state(); if (state == Job::JobPrepareState::PREPARE_STATE_MENU) { diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index c161a55f2d..f6bbfece68 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -50,6 +50,7 @@ class ArrangeJob : public Job //BBS:prepare the items from current selected partplate void prepare_partplate(); void prepare_wipe_tower(); + void prepare_belt_regions(int num_plates); ArrangePolygon prepare_arrange_polygon(void* instance); diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index 40ff181fb9..ea010a41b5 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -17,6 +17,8 @@ #include "libslic3r/libslic3r.h" #include "LibVGCodeWrapper.hpp" + +#include #include "libslic3r/Print.hpp" #include "libslic3r/Color.hpp" #include "libslic3r/CustomGCode.hpp" @@ -199,10 +201,22 @@ Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode) } GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector& str_tool_colors, - const std::vector& str_color_print_colors, const Viewer& viewer) + const std::vector& str_color_print_colors, const Viewer& viewer, + const Slic3r::Transform3d* belt_xform) { GCodeInputData ret; + // Belt printers: optionally map each vertex DISPLAY position from machine + // (G-code) space back to model/Cartesian space using the general belt + // back-transform (handles any mesh rotation + shear + axis remap, not just + // 45 deg). Only the rendered position is transformed; layer_id, times and + // the volumetric/flow math below keep the original machine-space values. + auto xform_pos = [belt_xform](const Slic3r::Vec3f& v) -> Vec3 { + if (belt_xform != nullptr) + return convert(Slic3r::Vec3f((*belt_xform * v.cast()).cast())); + return convert(v); + }; + // collect tool colors ret.tools_colors.reserve(str_tool_colors.size()); for (const std::string& color : str_tool_colors) { @@ -218,10 +232,22 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve const std::vector& moves = result.moves; ret.vertices.reserve(2 * moves.size()); + // libvgcode numbers its layers from the vertices it is given and expects them to + // arrive one layer after the other with consecutive ids: a layer without any move + // (a belt file can carry layer changes that print nothing) would leave a gap, and + // every later vertex would then be folded into the last layer created. Renumber + // the ids consecutively over the moves that exist. + uint32_t src_layer_id = std::numeric_limits::max(); + uint32_t layer_id = 0; for (size_t i = 1; i < moves.size(); ++i) { const Slic3r::GCodeProcessorResult::MoveVertex& curr = moves[i]; const Slic3r::GCodeProcessorResult::MoveVertex& prev = moves[i - 1]; const EMoveType curr_type = convert(curr.type); + if (curr.layer_id != src_layer_id) { + if (src_layer_id != std::numeric_limits::max()) + ++ layer_id; + src_layer_id = curr.layer_id; + } const EOptionType option_type = move_type_to_option(curr_type); if (option_type == EOptionType::COUNT || option_type == EOptionType::Travels || option_type == EOptionType::Wipes) { if (ret.vertices.empty() || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role @@ -231,17 +257,17 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve // equal to the current one with the exception of the position, which should match the previous move position, // and the times, which are set to zero #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS - const libvgcode::PathVertex vertex = { convert(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, + const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type, - static_cast(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), { 0.0f, 0.0f }, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, /* ORCA: Add Jerk visualization support */ curr.jerk }; #else - const libvgcode::PathVertex vertex = { convert(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, + const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type, - static_cast(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), { 0.0f, 0.0f }, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, @@ -252,18 +278,18 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve } #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS - const libvgcode::PathVertex vertex = { convert(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, + const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, result.filament_densities[curr.extruder_id] * curr.mm3_per_mm * (curr.position - prev.position).norm(), - convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), static_cast(curr.layer_id), + convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), curr.time, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, /* ORCA: Add Jerk visualization support */ curr.jerk }; #else - const libvgcode::PathVertex vertex = { convert(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, + const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate, curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type, - static_cast(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), curr.time, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, @@ -273,6 +299,11 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve } ret.vertices.shrink_to_fit(); + // Note: the belt designed-view anchoring (recovering the per-object placement/ + // lift translation the linear back-transform cannot) is folded into belt_xform + // by the caller (GCodeViewer::load_as_gcode), which anchors onto the upright + // model bounding box. Nothing extra to do here. + ret.spiral_vase_mode = result.spiral_vase_mode; return ret; diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp index dfcce08b69..a780a5f033 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp @@ -70,7 +70,8 @@ extern Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode // mapping from Slic3r::GCodeProcessorResult to libvgcode::GCodeInputData extern GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector& str_tool_colors, - const std::vector& str_color_print_colors, const Viewer& viewer); + const std::vector& str_color_print_colors, const Viewer& viewer, + const Slic3r::Transform3d* belt_xform = nullptr); // mapping from Slic3r::Print to libvgcode::GCodeInputData extern GCodeInputData convert(const Slic3r::Print& print, const std::vector& str_tool_colors, diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 78ee62c680..ce1c2f8cf4 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -673,6 +673,17 @@ void PartPlate::calc_height_limit() { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to create height limit top lines\n"; } +// The plate's icons and labels grow with its depth, but they sit in the gap to the +// next plate, which grows with its width: on a long, narrow bed (a belt) they would +// otherwise run across the neighbouring plate. +float PartPlate::icon_scale_factor() const +{ + const BoundingBoxf bed_ext = get_extents(m_shape); + const double by_depth = bed_ext.size().y() / 200.; + const double by_gap = bed_ext.size().x() * LOGICAL_PART_PLATE_GAP / (PARTPLATE_ICON_SIZE + 2 * PARTPLATE_ICON_GAP_LEFT); + return float(std::min(by_depth, by_gap)); +} + void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buffer) { buffer.reset(); @@ -689,7 +700,7 @@ void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buff #else //in the bottom auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[1]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float size = PARTPLATE_ICON_SIZE * factor; float offset_y = PARTPLATE_TEXT_OFFSET_Y * factor; float offset_x = (one_number?PARTPLATE_TEXT_OFFSET_X1: PARTPLATE_TEXT_OFFSET_X2) * factor; @@ -711,7 +722,7 @@ void PartPlate::calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int ind ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[3]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE; float width = icon_sz; float height = icon_sz; @@ -744,7 +755,7 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model) ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[2]; - auto factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float size = PARTPLATE_ICON_SIZE * factor; float gap_left = PARTPLATE_ICON_GAP_LEFT * factor; float gap_y = PARTPLATE_ICON_GAP_Y * factor; @@ -2587,7 +2598,7 @@ void PartPlate::generate_plate_name_texture() ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[3]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE; float width = icon_sz * m_name_texture.get_width() / m_name_texture.get_height(); // icon size * text_bb_ratio float height = icon_sz; // scale with icon size to preserve ratio while system scaling @@ -2790,6 +2801,7 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi BoundingBoxf3 instance_box = bounding_box? *bounding_box: object->instance_convex_hull_bounding_box(instance_id); Polygon hull = instance->convex_hull_2d(); BoundingBoxf3 plate_box = get_plate_box(); + this->open_belt_y(plate_box); if (instance_box.max.z() > plate_box.min.z()) plate_box.min.z() += instance_box.min.z(); // not considering outsize if sinking @@ -3474,6 +3486,19 @@ Polygon PartPlate::get_shared_printable_polygon() const return m_extruder_areas.empty() ? Polygon::new_scale(m_shape) : get_shared_poly(m_extruder_areas); } + +bool PartPlate::belt_open_y() const +{ + // Headless (CLI) plates have no plater and no wxApp behind wxGetApp(); the CLI's own belt + // handling lives in Print::validate(). + if (m_plater == nullptr || wxGetApp().preset_bundle == nullptr) + return false; + const DynamicPrintConfig &printer = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto *belt = printer.option("belt_printer"); + const auto *infinite_y = printer.option("belt_printer_infinite_y"); + return belt != nullptr && belt->value && infinite_y != nullptr && infinite_y->value; +} + bool PartPlate::contains(const Vec3d& point) const { return m_bounding_box.contains(point); @@ -3493,6 +3518,7 @@ bool PartPlate::contains(const BoundingBoxf3& bb) const print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon; print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon; print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon; + this->open_belt_y(print_volume); return print_volume.contains(bb); } @@ -3505,6 +3531,7 @@ bool PartPlate::intersects(const BoundingBoxf3& bb) const print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon; print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon; print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon; + this->open_belt_y(print_volume); return print_volume.intersects(bb); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index fc1aeb7bd7..fb98717a6e 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -194,6 +194,7 @@ private: void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model); void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox); void calc_height_limit(); + float icon_scale_factor() const; void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); void calc_vertex_for_icons(int index, PickingModel &model); @@ -443,6 +444,10 @@ public: bool contains(const GLVolume& v) const; bool contains(const BoundingBoxf3& bb) const; bool intersects(const BoundingBoxf3& bb) const; + // A belt printer with belt_printer_infinite_y: the plate is open along Y for the + // containment tests (the drawn plate keeps its shape). + bool belt_open_y() const; + void open_belt_y(BoundingBoxf3 &box) const { if (this->belt_open_y()) { box.min.y() = -1e5; box.max.y() = 1e5; } } void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 965ae99b82..a666e23c9c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -210,6 +210,7 @@ #include "UVEditorCanvas.hpp" #include "3DBed.hpp" #include "PartPlate.hpp" +#include "BeltPurgeTower.hpp" #include "Camera.hpp" #include "Mouse3DController.hpp" #include "Tab.hpp" @@ -266,6 +267,7 @@ #include #include // Needs to be last because reasons :-/ #include +#include #include "WipeTowerDialog.hpp" #include "MixedFilamentDialog.hpp" #include "TextureImportDialog.hpp" @@ -7428,6 +7430,11 @@ struct Plater::priv void exit_gizmo(); void remove(size_t obj_idx); bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS + // ORCA-Belt: keep the auto-generated belt purge prism in sync with the + // config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower + // in BeltPurgeTower.cpp). Returns true when the model was mutated. + bool ensure_belt_purge_tower(); + std::vector m_belt_purge_sigs; void delete_all_objects_from_model(); void reset(bool apply_presets_change = false, bool reload_presets = true); void center_selection(); @@ -10358,7 +10365,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode // BBS: find an empty cell to put the copied object for (auto& instance : new_instances) { auto offset = instance->get_offset(); - auto start_point = this->bed.build_volume().bounding_volume2d().center(); + auto start_point = this->bed.build_volume().bed_center(); bool plate_empty = partplate_list.get_curr_plate()->empty(); Vec3d displacement; if (plate_empty) @@ -11217,6 +11224,16 @@ void Plater::priv::process_validation_warnings(const std::vectorobj_list(), m_belt_purge_sigs); +} + + // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) @@ -11227,6 +11244,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // If the update_background_process() was not called by the timer, kill the timer, // so the update_restart_background_process() will not be called again in vain. background_process_timer.Stop(); + // ORCA-Belt: sync the auto-managed belt purge prism before the model is + // applied to the Print below, so the prism change rides this same apply. + if (printer_technology == ptFFF && this->ensure_belt_purge_tower()) + return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE; // Update the "out of print bed" state of ModelInstances. update_print_volume_state(); // Apply new config to the possibly running background task. @@ -14621,6 +14642,7 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); + float prev_height_lid, prev_height_rod; partplate_list.get_height_limits(prev_height_lid, prev_height_rod); double height_to_lid = config->opt_float("extruder_clearance_height_to_lid"); @@ -16322,8 +16344,167 @@ bool Plater::add_model(bool imperial_units, std::string fname) return loaded; } +// ORCA-Belt: belt-printer handling for the desktop calibration tests. +// +// Belt slicing applies a global pre-slice rotation R(angle, axis) to every +// mesh (see BeltTransform.hpp) so the slicing planes match the tilted gantry. +// Calibration models are designed for upright slicing: their per-height test +// bands and XY-plane quality features assume slicer Z is the model's own Z +// axis. Counter-rotating each calibration object by the inverse rotation in +// world space cancels the global rotation, so in slicing space the object +// stands upright exactly as on a flat-bed printer and every test keeps its +// designed meaning. Physically the object then leans over the belt with its +// bottom face overhanging, so per-object supports fill the wedge between the +// bottom face and the belt. The wedge prints entirely below the object's base +// plane and leaves the test geometry untouched. Manual tree support is used +// so the deliberate bridge/overhang features of the test models stay +// unsupported; the wedge under the floating bottom face is built by the +// belt-floor extension in TreeSupport::generate(), which stacks the floating +// first-layer footprint down to the belt surface. +static bool belt_calib_rotation_params(double& angle_rad, Vec3d& axis) +{ + const auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto* belt_opt = printer_config.option("belt_printer"); + if (belt_opt == nullptr || !belt_opt->value) + return false; + const auto* axis_opt = printer_config.option>("belt_slice_rotation"); + const auto* angle_opt = printer_config.option("belt_slice_rotation_angle"); + if (axis_opt == nullptr || angle_opt == nullptr) + return false; + switch (axis_opt->value) { + case BeltRotationAxis::X: axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: axis = Vec3d::UnitY(); break; + // Z rotation is an in-plane spin and None means no tilt; objects already + // slice upright in those cases and need no special handling. + default: return false; + } + angle_rad = -Geometry::deg2rad(angle_opt->value); + return std::abs(angle_rad) > EPSILON; +} + +// ORCA-Belt: flip the ringing tower 180° about Z before the belt +// counter-rotation — its sloped face then leans over the belt and the +// support wedge gets much smaller. +static void belt_calib_flip_ringing_tower(Model &model) +{ + double angle_rad = 0.; + Vec3d axis = Vec3d::UnitX(); + if (belt_calib_rotation_params(angle_rad, axis) && !model.objects.empty()) + model.objects.front()->rotate(M_PI, Vec3d::UnitZ()); +} + +void Plater::_calib_apply_belt_mode() +{ + double angle_rad = 0.; + Vec3d axis = Vec3d::UnitX(); + if (!belt_calib_rotation_params(angle_rad, axis)) + return; + + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + // Spiral vase stays enabled where the tests request it: the support wedge + // lies strictly below the object, support layers never spiralize + // (spiral_vase_enable requires an object layer), and the spiral/support + // exclusivity check only applies to globally enabled supports — the wedge + // uses per-object supports. + // A skirt would be drawn in the first slicing plane, which lies mostly + // above the belt surface. + print_config->set_key_value("skirt_loops", new ConfigOptionInt(0)); + + const Matrix3d cancel_rotation = Eigen::AngleAxisd(angle_rad, axis).toRotationMatrix(); + std::vector obj_idxs; + for (size_t i = 0; i < model().objects.size(); ++i) { + ModelObject* obj = model().objects[i]; + if (obj->instances.size() != 1) + continue; + obj_idxs.emplace_back(i); + + // Manual tree support: only the floating bottom face gets a support + // wedge (via the belt-floor extension in TreeSupport::generate()), + // leaving the test features untouched. The style is pinned to hybrid + // because the default style resolves to organic, which bypasses the + // non-organic generator that hosts the belt-floor extension. + obj->config.set_key_value("enable_support", new ConfigOptionBool(true)); + obj->config.set_key_value("support_type", new ConfigOptionEnum(stTree)); + obj->config.set_key_value("support_style", new ConfigOptionEnum(smsTreeHybrid)); + obj->config.set_key_value("support_on_build_plate_only", new ConfigOptionBool(false)); + // With the default base pattern, tree support base areas print as + // hollow outlines (no infill) — the wedge needs a real pattern. + obj->config.set_key_value("support_base_pattern", new ConfigOptionEnum(smpRectilinear)); + + // Counter-rotate exactly the way the rotate gizmo would: rotation on + // the instance, then a plain drop to the bed. This leaves the object + // in the same state shape as any manually rotated object, which the + // belt pipeline is known to handle. + ModelInstance* inst = obj->instances.front(); + inst->rotate(cancel_rotation); + obj->invalidate_bounding_box(); + obj->ensure_on_bed(); + } + + // Each object's support wedge extends upstream of it by roughly its own + // depth (at 45°), so the tight flat-bed layouts of the multi-part tests + // leave wedges intersecting the neighbouring parts. Keep the grid rows of + // the test layouts together and open up the space between rows just + // enough for the wedge shadow. + if (obj_idxs.size() > 1) { + std::vector sorted_objs; + sorted_objs.reserve(obj_idxs.size()); + for (size_t i : obj_idxs) + sorted_objs.emplace_back(model().objects[i]); + std::sort(sorted_objs.begin(), sorted_objs.end(), [](const ModelObject* a, const ModelObject* b) { + return a->instances.front()->get_offset(Y) < b->instances.front()->get_offset(Y); + }); + std::vector> rows; + double row_y = std::numeric_limits::quiet_NaN(); + for (ModelObject* o : sorted_objs) { + const double oy = o->instances.front()->get_offset(Y); + if (rows.empty() || oy - row_y > 1.) + rows.emplace_back(); + rows.back().emplace_back(o); + row_y = oy; + } + const double wedge_factor = std::abs(std::tan(angle_rad)); + double cursor = std::numeric_limits::quiet_NaN(); + for (std::vector& row : rows) { + double rmin = std::numeric_limits::max(); + double rmax = std::numeric_limits::lowest(); + for (ModelObject* o : row) { + const BoundingBoxf3 bb = o->instance_bounding_box(0); + rmin = std::min(rmin, bb.min.y()); + rmax = std::max(rmax, bb.max.y()); + } + if (std::isnan(cursor)) + cursor = rmin; // the first row anchors the layout + const double shift = cursor - rmin; + for (ModelObject* o : row) { + ModelInstance* inst = o->instances.front(); + inst->set_offset(Y, inst->get_offset(Y) + shift); + o->invalidate_bounding_box(); + } + cursor += (rmax - rmin) * (1. + wedge_factor) + 5.; + } + } + + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + changed_objects(obj_idxs); +} + void Plater::calib_pa(const Calib_Params& params) { + // ORCA-Belt: PA Line / PA Pattern have the belt plumbing in place + // (belt kinematics in world-coordinates mode draws them on the belt surface) + // but are not validated yet — keep them gated to the PA Tower for now. + { + double angle_rad = 0.; + Vec3d axis = Vec3d::UnitX(); + if (belt_calib_rotation_params(angle_rad, axis) && params.mode != CalibMode::Calib_PA_Tower) { + MessageDialog msg_dlg(nullptr, _L("PA Line and PA Pattern tests are not enabled yet on belt printers.\nPlease use the PA Tower method instead."), + wxEmptyString, wxICON_WARNING | wxOK); + msg_dlg.ShowModal(); + return; + } + } const auto calib_pa_name = _L("Pressure Advance Test"); new_project(false, false, calib_pa_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); @@ -16652,6 +16833,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) { cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower); } + _calib_apply_belt_mode(); _calib_pa_select_added_objects(); } @@ -16832,22 +17014,143 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + _calib_apply_belt_mode(); + // Refresh object after scaling const std::vector object_idx(boost::counting_iterator(0), boost::counting_iterator(model().objects.size())); changed_objects(object_idx); } +// The belt provini tower (Calib_Params::test_model 1) is one embossed model per +// temperature range. +static std::string belt_temp_tower_asset(const Calib_Params ¶ms) +{ + const int t_start = (int) lround(params.start); + const int t_end = (int) lround(params.end); + return Slic3r::resources_dir() + "/calib/temperature_tower/belt_temp_tower_" + + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; +} + void Plater::calib_temp(const Calib_Params& params) { constexpr double base_temp_tower_nozzle_diameter = 0.4; constexpr double base_temp_tower_block_height = 10.0; constexpr int base_temp_tower_temp_step = 5; + // A belt provini tower exists only for the ranges it was embossed for, and another + // range's model would print numbers that do not match its temperatures. Refuse + // before the current project is replaced. + if (params.mode == CalibMode::Calib_Temp_Tower && params.test_model >= 1) { + const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (printer_config.has("belt_printer") && printer_config.opt_bool("belt_printer") && + ! boost::filesystem::exists(belt_temp_tower_asset(params))) { + MessageDialog dlg(static_cast(wxGetApp().mainframe), + format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. " + "Use a range the tower models cover, for example 230 to 190."), + (int) lround(params.start), (int) lround(params.end)), + _L("Temperature tower"), wxICON_ERROR | wxOK); + dlg.ShowModal(); + return; + } + } + const auto calib_temp_name = _L("Nozzle temperature test"); new_project(false, false, calib_temp_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - if (params.mode != CalibMode::Calib_Temp_Tower) return; - + if (params.mode != CalibMode::Calib_Temp_Tower) + return; + + // Belt printers build along the conveyor, not vertically — a tall tower cannot + // be sliced. Instead lay a row of DISCRETE provini (one per temperature) along + // the belt and change temperature in discrete steps via custom per-layer G-code + // (M104), injected in the empty gap just before each provino so the nozzle is + // settled by the time that provino prints. We deliberately do NOT call + // set_calib_params here: its per-layer interpolation (interpolate_value_across_ + // layers) would overwrite these discrete M104 events. + { + auto belt_printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (belt_printer_config->has("belt_printer") && belt_printer_config->opt_bool("belt_printer")) { + // Belt temperature-tower model selector (Calib_Params::test_model): + // 0 = "Standard" -> Joe's counter-rotated sectioned tower. + // 1 = "Overhang" -> engraved inverted-L provini that stress overhang + // print-quality at each discrete temperature (below). + if (params.test_model < 1) { + double belt_angle_rad = 0.; Vec3d belt_axis = Vec3d::UnitX(); + belt_calib_rotation_params(belt_angle_rad, belt_axis); + _calib_temp_belt_sectioned(params, std::abs(belt_angle_rad)); + return; + } + constexpr int TEMP_STEP = 5; // matches Temp_Calibration_Dlg + // Shared geometry contract with the offline asset generator + // (resources/calib/temperature_tower/gen_belt_temp_tower.py): provini are + // replicated along the belt (designed Y) at this pitch. The slicing plane + // is oblique (belt_slice_rotation_angle), so the per-zone advance in the + // layer print_z space the custom-gcode matcher uses is PITCH*cos(theta). + constexpr double PITCH_Y = 74.718; // designed-Y pitch == gen PITCH + const double angle = belt_printer_config->has("belt_slice_rotation_angle") + ? belt_printer_config->opt_float("belt_slice_rotation_angle") : 45.0; + const double zone_topz = PITCH_Y * std::cos(angle * M_PI / 180.0); + // The custom-gcode matcher attaches each event to a real sliced layer. The + // empty inter-provino gap has NO layers, so an event placed there is silently + // dropped. Fire it 70 layers ABOVE provino i's start instead — inside the + // provino body, past the gap and the overlap with provino i-1's tail, so the + // temperature change attaches and applies cleanly. (layer print_z steps by the + // process layer height.) + auto belt_print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + const double layer_h = belt_print_config->has("layer_height") + ? belt_print_config->opt_float("layer_height") : 0.2; + const double into_provino = 70.0 * layer_h; + + const int t_start = (int) lround(params.start); + const int t_end = (int) lround(params.end); + std::vector temps; + const int tstep = (t_start >= t_end) ? -TEMP_STEP : TEMP_STEP; + for (int t = t_start; (tstep < 0) ? (t >= t_end) : (t <= t_end); t += tstep) + temps.push_back(t); + if (temps.empty()) temps.push_back(t_start); + + const std::string asset = belt_temp_tower_asset(params); + if (!boost::filesystem::exists(asset)) // refused above, before new_project() + return; + if (!add_model(false, asset) || model().objects.empty()) + return; + + // Place keel-first asset at the belt entry (designed Y = 0) so Z_gcode + // starts at 0, centered laterally on the bed, resting on the conveyor. + ModelObject* obj = model().objects[0]; + obj->ensure_on_bed(); + BoundingBoxf3 obb = obj->bounding_box_exact(); + auto bed_shape = belt_printer_config->option("printable_area")->values; + BoundingBoxf bed_ext = get_extents(bed_shape); + obj->translate_instances(Vec3d(bed_ext.center().x() - obb.center().x(), -obb.min.y(), 0.0)); + + auto belt_filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + belt_filament_config->set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(1, temps.front())); + belt_filament_config->set_key_value("nozzle_temperature", new ConfigOptionInts(1, temps.front())); + obj->config.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); + belt_printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + + const int plate_idx = get_partplate_list().get_curr_plate_index(); + model().curr_plate_index = plate_idx; + CustomGCode::Info &cg_info = model().plates_custom_gcodes[plate_idx]; + cg_info.mode = CustomGCode::Mode::SingleExtruder; + cg_info.gcodes.clear(); + for (size_t i = 1; i < temps.size(); ++i) { + const double pz = double(i) * zone_topz + into_provino; // 70 layers into provino i + cg_info.gcodes.push_back(CustomGCode::Item{ + pz, CustomGCode::Custom, 1, "", + "M104 S" + std::to_string(temps[i]) + " ; belt temp zone " + std::to_string(temps[i]) }); + } + + changed_objects({ 0 }); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); + return; + } + } + if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc")) return; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -16921,6 +17224,107 @@ void Plater::calib_temp(const Calib_Params& params) { p->background_process.fff_print()->set_calib_params(params); } +// ORCA-Belt: sectioned temperature test. Each temperature gets its own block +// cut out of the temperature tower model, printed in native belt orientation +// (no counter-rotation, no support wedge) and spaced along the belt so the +// blocks' layer ranges are disjoint — they print strictly one after another, +// starting with the start temperature closest to the gantry. The temperature +// is encoded in the object name ("temp_230") and applied per object by the +// Calib_Temp_Tower handler at G-code time, replacing the per-layer-band ramp +// that only makes sense for a monolithic upright tower. +void Plater::_calib_temp_belt_sectioned(const Calib_Params& params, double belt_angle_rad) +{ + constexpr double base_temp_tower_nozzle_diameter = 0.4; + constexpr double base_temp_tower_block_height = 10.0; + constexpr int base_temp_tower_temp_step = 5; + + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + + const long start_temp = lround(params.start); + const long end_temp = lround(params.end); + const int n_blocks = std::max(1, int((start_temp - end_temp) / base_temp_tower_temp_step) + 1); + + const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); + size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); + double nozzle_diameter = base_temp_tower_nozzle_diameter; + if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) { + nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); + nozzle_diameter = nozzle_diameter_config->values[nozzle_id]; + } + if (nozzle_diameter <= 0.0) + nozzle_diameter = base_temp_tower_nozzle_diameter; + const double nozzle_scale = nozzle_diameter / base_temp_tower_nozzle_diameter; + + std::vector obj_idxs; + for (int i = 0; i < n_blocks; ++i) { + const long temp = start_temp - long(i) * base_temp_tower_temp_step; + const size_t count_before = model().objects.size(); + add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc"); + if (model().objects.size() <= count_before) + break; // model failed to load — don't index into an empty list + // The cut replaces the object at the END of the list, so re-acquire + // the index after every operation. + size_t obj_idx = model().objects.size() - 1; + + // Isolate this temperature's block (full-tower coordinates, the same + // 500-down-to-temp indexing the monolithic flow cuts with). + const double block_bottom = double(lround(double(500 - temp) / base_temp_tower_temp_step)) * base_temp_tower_block_height; + auto obj_bb = model().objects[obj_idx]->bounding_box_exact(); + if (block_bottom + base_temp_tower_block_height < obj_bb.size().z()) { + cut_horizontal(obj_idx, 0, block_bottom + base_temp_tower_block_height - EPSILON, ModelObjectCutAttribute::KeepLower); + obj_idx = model().objects.size() - 1; + } + if (block_bottom > 0) { + cut_horizontal(obj_idx, 0, block_bottom + EPSILON, ModelObjectCutAttribute::KeepUpper); + obj_idx = model().objects.size() - 1; + } + + ModelObject* obj = model().objects[obj_idx]; + if (std::abs(nozzle_scale - 1.0) > EPSILON) + obj->scale(nozzle_scale, nozzle_scale, nozzle_scale); + obj->name = std::string("temp_") + std::to_string(temp); + obj->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter / 2)); + obj->config.set_key_value("alternate_extra_wall", new ConfigOptionBool(false)); + obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); + obj->config.set_key_value("precise_z_height", new ConfigOptionBool(false)); + obj->ensure_on_bed(); + obj_idxs.emplace_back(obj_idx); + } + + // Space the blocks along the belt with strictly increasing layer ranges: + // each block must start past the previous block's highest slicing plane, + // which trails its far edge by height / tan(angle). Anchor the row near + // the gantry so the whole test stays in the plate area. + const double cot_a = 1. / std::max(0.1, std::tan(belt_angle_rad)); + double cursor = 20.; + for (size_t idx : obj_idxs) { + ModelObject* obj = model().objects[idx]; + ModelInstance* inst = obj->instances.front(); + const BoundingBoxf3 bb = obj->instance_bounding_box(0); + inst->set_offset(Y, inst->get_offset(Y) + (cursor - bb.min.y())); + obj->invalidate_bounding_box(); + cursor += bb.size().y() + bb.size().z() * cot_a + 5.; + } + + printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); + filament_config->set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(1, (int)start_temp)); + filament_config->set_key_value("nozzle_temperature", new ConfigOptionInts(1, (int)start_temp)); + print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter / 2)); + print_config->set_key_value("skirt_loops", new ConfigOptionInt(0)); + + changed_objects(obj_idxs); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config(); + + p->background_process.fff_print()->set_calib_params(params); +} + void Plater::calib_max_vol_speed(const Calib_Params& params) { const auto calib_vol_speed_name = _L("Max volumetric speed test"); @@ -16991,6 +17395,8 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } + _calib_apply_belt_mode(); + auto new_params = params; auto mm3_per_mm = Flow(line_width, layer_height, nozzle_diameter).mm3_per_mm() * filament_config->option("filament_flow_ratio")->get_at(0); new_params.end = params.end / mm3_per_mm; @@ -17059,6 +17465,7 @@ void Plater::calib_retraction(const Calib_Params& params) cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -17138,6 +17545,7 @@ void Plater::calib_VFA(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + _calib_apply_belt_mode(); // Pass the resolved layer height on (only meaningful when resized). GCode's VFA stepping is layer-based, so // it does not require it, but keep it consistent with the geometry. Calib_Params calib_params = params; @@ -17155,6 +17563,8 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc"))) return; + if (params.test_model < 1) + belt_calib_flip_ringing_tower(model()); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -17204,6 +17614,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -17217,6 +17628,8 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc"))) return; + if (params.test_model < 1) + belt_calib_flip_ringing_tower(model()); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -17265,6 +17678,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -17281,6 +17695,8 @@ void Plater::Calib_Cornering(const Calib_Params& params) : (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : "/calib/cornering/SCV-V2.drc"); if (!add_model(false, Slic3r::resources_dir() + cornering_model_path)) return; + if (params.test_model == 0) + belt_calib_flip_ringing_tower(model()); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -17330,6 +17746,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings(); + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -17449,6 +17866,15 @@ void Plater::load_gcode(const wxString& filename) current_print.set_gcode_file_ready(); + // Belt printer: detect the belt tilt from the loaded G-code header and enable + // belt view mode on the GCodeViewer so the "Show designed view" toggle appears. + if (current_result->belt_tilt_angle > 0.f) { + float angle = current_result->belt_tilt_angle; + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, angle); + } else { + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + // show results p->preview->reload_print(m_only_gcode); //BBS: zoom to bed 0 for gcode preview @@ -17483,6 +17909,11 @@ void Plater::reload_print() p->preview->reload_print(); } +void Plater::refresh_belt_view() +{ + p->preview->refresh_belt_view(); +} + // BBS wxString Plater::get_project_name() { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index acc9511502..c3b4523c49 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -383,6 +383,10 @@ public: void load_gcode(const wxString& filename); void reload_gcode_from_disk(); void reload_print(); + // Belt printers: re-run the G-code preview conversion so the "designed view" toggle + // (hotkey B / legend checkbox) takes effect; the back-transform is applied to the + void refresh_belt_view(); + // toolpath geometry at load time. Keeps the current layer range and only-gcode mode. // SoftFever void calib_pa(const Calib_Params& params); @@ -1071,6 +1075,8 @@ private: void _calib_pa_pattern_gen_gcode(); void _calib_pa_tower(const Calib_Params& params); void _calib_pa_select_added_objects(); + void _calib_apply_belt_mode(); + void _calib_temp_belt_sectioned(const Calib_Params& params, double belt_angle_rad); void cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes); diff --git a/src/slic3r/GUI/Shortcuts.cpp b/src/slic3r/GUI/Shortcuts.cpp index 07ff5bda63..df8d7c00ca 100644 --- a/src/slic3r/GUI/Shortcuts.cpp +++ b/src/slic3r/GUI/Shortcuts.cpp @@ -150,6 +150,7 @@ constexpr std::array shortcut_table = {{ SHORTCUT(ShowWireframe, "show_wireframe", L("Show/Hide wireframe"), CANVAS, { WXK_RETURN, CTRL_SHIFT }), SHORTCUT(ToggleGcodeWindow, "toggle_gcode_window", L("On/Off G-code window"), PREVIEW, { 'C' }), SHORTCUT(ToggleOneLayerMode, "toggle_one_layer_mode", L("On/Off one layer mode of the vertical slider"), PREVIEW, { 'L' }), + SHORTCUT(ToggleBeltRawGcode, "toggle_belt_raw_gcode", L("Show raw G-code (belt only)"), PREVIEW, { 'B' }), // Application SHORTCUT(Preferences, "preferences", L("Preferences"), GLOBAL, PREFERENCES_CHORD), diff --git a/src/slic3r/GUI/Shortcuts.hpp b/src/slic3r/GUI/Shortcuts.hpp index a7c6bda8cf..aba315d521 100644 --- a/src/slic3r/GUI/Shortcuts.hpp +++ b/src/slic3r/GUI/Shortcuts.hpp @@ -46,7 +46,7 @@ enum class Shortcut : uint8_t { // Camera ViewDefault, ViewTop, ViewBottom, ViewFront, ViewRear, ViewLeft, ViewRight, ViewPlate, ZoomIn, ZoomOut, Mouse3DSettings, // Display - ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, + ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, ToggleBeltRawGcode, // Application Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts, // Speed Dial diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 55f405a28f..49457e8bc8 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3,6 +3,7 @@ #include "PresetHints.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/BeltTransform.hpp" #include "libslic3r/FilamentMixer.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" @@ -1650,6 +1651,13 @@ void Tab::update_mode() { m_mode = wxGetApp().get_mode(); + // toggle_options reads m_mode to gate Lines whose contents are mode-mixed + // (e.g., a multi-option row where some options are Advanced and some Expert): + // when all of a Line's options would be hidden, we hide the Line itself. + // Without refreshing here, the toggle_visible state stays stale across mode + // switches and Lines stay hidden. + toggle_options(); + update_visibility(); update_changed_tree_ui(); @@ -3141,6 +3149,7 @@ void TabPrint::build() optgroup->append_single_option_line("enable_tower_interface_cooldown_during_tower", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_enable_framework", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_width", "multimaterial_settings_prime_tower#width"); + optgroup->append_single_option_line("belt_purge_tower_width", "multimaterial_settings_prime_tower#belt-purge-tower-width"); optgroup->append_single_option_line("prime_volume", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_brim_width", "multimaterial_settings_prime_tower#brim-width"); optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower"); @@ -3206,6 +3215,8 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Brim"), L"param_adhension"); optgroup->append_single_option_line("brim_type", "others_settings_brim#type"); optgroup->append_single_option_line("brim_width", "others_settings_brim#width"); + optgroup->append_single_option_line("leading_brim_length", "others_settings_brim#leading-length"); + optgroup->append_single_option_line("extra_brim_width", "others_settings_brim#extra-width"); optgroup->append_single_option_line("brim_object_gap", "others_settings_brim#brim-object-gap"); optgroup->append_single_option_line("brim_flow_ratio", "others_settings_brim#brim-flow-ratio"); optgroup->append_single_option_line("brim_use_efc_outline", "others_settings_brim#brim-use-efc-outline"); @@ -3366,6 +3377,42 @@ void TabPrint::toggle_options() cb->SetValue(n); } + // "Leading edge only" describes where a part meets a moving belt, so it is offered only + // on belt printers. Same pattern as support_style above: the field owns a copy of the + // option definition, and Choice maps the combobox selection straight onto that copy's + // enum_values, so rewriting both together keeps the mapping correct. + field = m_active_page->get_field("brim_type"); + if (auto choice = dynamic_cast(field)) { + bool is_belt_printer = false; + if (m_preset_bundle) { + const auto *belt_opt = m_preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_opt) + is_belt_printer = belt_opt->value; + } + auto def = print_config_def.get("brim_type"); + const auto current = m_config->opt_enum("brim_type"); + auto &opt = const_cast(field->m_opt); + auto cb = dynamic_cast(choice->window); + // Keep the entry if it is already selected, so switching to a non-belt + // printer cannot leave the control showing a value it does not offer. + const bool offer_leading_edge = is_belt_printer || current == btLeadingEdgeOnly; + const bool offered = std::find(opt.enum_values.begin(), opt.enum_values.end(), "leading_edge_only") != opt.enum_values.end(); + if (cb != nullptr && offer_leading_edge != offered) { + auto n = cb->GetValue(); + opt.enum_values.clear(); + opt.enum_labels.clear(); + cb->Clear(); + for (size_t i = 0; i < def->enum_values.size(); ++ i) { + if (def->enum_values[i] == "leading_edge_only" && ! offer_leading_edge) + continue; + opt.enum_values.push_back(def->enum_values[i]); + opt.enum_labels.push_back(def->enum_labels[i]); + cb->Append(_(def->enum_labels[i])); + } + cb->SetValue(n); + } + } + // BBL printers do not support cone wipe tower field = m_active_page->get_field("wipe_tower_wall_type"); if (auto choice = dynamic_cast(field)) { @@ -5230,6 +5277,35 @@ void TabPrinter::build_fff() //option.opt.full_width = true; //optgroup->append_single_option_line(option); optgroup->append_single_option_line("disable_m73", "printer_basic_information_advanced#disable-set-remaining-print-time"); + + // Belt printer: dedicated section. Everything except the "Enable belt printing" + // checkbox is hidden when belt_printer is off (see TabPrinter::toggle_options). + auto belt_og = page->new_optgroup(L("Belt printer"), L"param_advanced"); + belt_og->append_single_option_line("belt_printer", "printer_basic_information_belt_printer#enable-belt-printing"); + belt_og->append_single_option_line("belt_printer_infinite_y", "printer_basic_information_belt_printer#infinite-y-axis"); + // Belt tilt: the sole mesh-side transform and the single source of truth for + // the physical tilt (drives bed rendering and support gravity tilt too). + // Isometric rotation, no distortion; the back-transform inverts it before the + // machine-frame remap. The angle is what a user checks against the machine; + // the axis is a profile-level kinematics choice, so it is Develop-only. They + // are separate rows because a shared line is shown by its first option's mode. + belt_og->append_single_option_line("belt_slice_rotation_angle", "printer_basic_information_belt_printer#tilt-angle"); + belt_og->append_single_option_line("belt_slice_rotation", "printer_basic_information_belt_printer#tilt-axis"); + belt_og->append_single_option_line("belt_support_floor_offset", "printer_basic_information_belt_printer#support-floor-z-offset"); + + // Machine-frame transform: the shear (cot) + scale (1/sin) that map + // Cartesian G-code into the printer's physical machine frame are derived + // from the belt tilt angle. Only the post-slice axis remap and the expert + // decouple override are exposed here, one option per row. + { + auto mf = page->new_optgroup(L("Machine frame transforms"), L"param_advanced"); + mf->append_single_option_line("gcode_remap_x", "printer_basic_information_machine_frame_transforms#g-code-axis-remap"); + mf->append_single_option_line("gcode_remap_y", "printer_basic_information_machine_frame_transforms#g-code-axis-remap"); + mf->append_single_option_line("gcode_remap_z", "printer_basic_information_machine_frame_transforms#g-code-axis-remap"); + mf->append_single_option_line("belt_frame_tilt_decouple", "printer_basic_information_machine_frame_transforms#machine-frame-tilt"); + mf->append_single_option_line("belt_frame_tilt_angle", "printer_basic_information_machine_frame_transforms#machine-frame-tilt"); + } + option = optgroup->get_option("thumbnails"); option.opt.full_width = true; optgroup->append_single_option_line(option, "printer_basic_information_advanced#g-code-thumbnails"); @@ -5274,6 +5350,8 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("use_firmware_retraction", "printer_basic_information_advanced#use-firmware-retraction"); // optgroup->append_single_option_line("spaghetti_detector"); optgroup->append_single_option_line("time_cost", "printer_basic_information_advanced#time-cost"); + optgroup->append_single_option_line("build_plate_tilt_x", "printer_basic_information_advanced#build-plate-tilt"); + optgroup->append_single_option_line("build_plate_tilt_y", "printer_basic_information_advanced#build-plate-tilt"); optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode"); optgroup->append_single_option_line("printer_plugin_config_overrides"); @@ -5794,6 +5872,12 @@ if (is_marlin_flavor) optgroup->append_single_option_line("tool_change_on_wipe_tower", "printer_multimaterial_wipe_tower#tool-change-on-wipe-tower"); optgroup->append_single_option_line("wait_for_temp_on_wipe_tower", "printer_multimaterial_wipe_tower#wait-for-temperature-on-wipe-tower"); + // Orca-Belt: belt printers replace the classic wipe tower with an + // auto-generated purge prism; this is its enable (gated to belt printers + // in toggle_options()). + optgroup = page->new_optgroup(L("Belt purge tower"), "param_tower"); + optgroup->append_single_option_line("enable_belt_purge_tower", "printer_multimaterial_wipe_tower#belt-purge-tower"); + optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings"); optgroup->append_single_option_line("cooling_tube_retraction", "printer_multimaterial_semm_parameters#cooling-tube-position"); @@ -5964,6 +6048,18 @@ if (is_marlin_flavor) // this gets executed after preset is loaded and before GUI fields are updated void TabPrinter::on_preset_loaded() { + // R8: reset the belt-tilt transition tracking to reflect the freshly loaded preset WITHOUT + // running update_fff()'s reset logic. on_preset_loaded() is called from Tab::load_current_preset() + // on every printer preset load, right before update()->update_fff(). Seeding m_was_belt_printer + // from the loaded preset's belt_printer flag means a preset switch (belt preset -> non-belt preset) + // enters update_fff() with m_was_belt_printer==false, so the belt->off clear branch is skipped and + // the newly loaded preset's manual tilt is preserved. An in-place belt toggle does NOT go through + // here (only through on_value_change->update), so m_was_belt_printer stays true there and the clear + // still fires. Seed m_belt_synced_tilt from the loaded tilt as a safeguard. + m_was_belt_printer = m_config->opt_bool("belt_printer"); + m_belt_synced_tilt_x = m_config->opt_float("build_plate_tilt_x"); + m_belt_synced_tilt_y = m_config->opt_float("build_plate_tilt_y"); + // Orca //update nozzle_volume_type const Preset& current_printer = m_preset_bundle->printers.get_selected_preset(); @@ -6249,6 +6345,39 @@ void TabPrinter::toggle_options() bool gcf_is_marlin_firmware = m_config->option>("gcode_flavor")->value == GCodeFlavor::gcfMarlinFirmware; toggle_line("enable_power_loss_recovery", is_BBL_printer || gcf_is_marlin_firmware); + // Belt printer: show belt-specific settings only when belt_printer is enabled. + bool is_belt = m_config->opt_bool("belt_printer"); + // update_fff() derives build_plate_tilt_{x,y} from the belt tilt on a belt + // printer, so an edit here would be overwritten; keep them read-only there. + toggle_option("build_plate_tilt_x", !is_belt); + toggle_option("build_plate_tilt_y", !is_belt); + bool expert_or_above = (m_mode >= comExpert); + toggle_line("belt_printer_infinite_y", is_belt); + // Belt tilt: the sole mesh-side belt transform (visible by default in belt mode). + toggle_line("belt_slice_rotation_angle", is_belt); + toggle_line("belt_slice_rotation", is_belt); + + // Remap, back-transform, and global mesh-transforms toggles are gated by belt + // mode here; finer mode-based visibility is handled by each option's + // ConfigOptionMode in PrintConfig.cpp. The axis remap is Develop-only: a + // printer profile sets it once for its kinematics, and a wrong value sends + // the gantry outside the machine. + for (auto el : {"gcode_remap_x", "gcode_remap_y", "gcode_remap_z"}) + toggle_line(el, is_belt); + + // Rotation is the only mesh-side belt transform. Gray out its angle when no + // rotation axis is selected. + auto rot_axis = m_config->option>("belt_slice_rotation")->value; + toggle_option("belt_slice_rotation_angle", is_belt && rot_axis != BeltRotationAxis::None); + + // Machine-frame transform: derived from the belt tilt. Only the expert + // decouple override is exposed; its angle is shown only when decoupled. + toggle_line("belt_frame_tilt_decouple", is_belt && expert_or_above); + toggle_line("belt_frame_tilt_angle", + is_belt && expert_or_above && m_config->opt_bool("belt_frame_tilt_decouple")); + + + toggle_line("belt_support_floor_offset", is_belt); const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads"); toggle_line("parallel_printheads_count", support_parallel_printheads); @@ -6267,8 +6396,14 @@ void TabPrinter::toggle_options() } if (m_active_page->title() == L("Multimaterial")) { + // Orca-Belt: belt printers use the belt purge tower instead of the classic + // wipe tower — show its enable only on belt printers, and hide the classic + // wipe-tower fields there (the classic tower's G-code bypasses the belt transform). + const bool is_belt_printer = m_config->opt_bool("belt_printer"); + toggle_line("enable_belt_purge_tower", is_belt_printer); + const bool supports_wipe_tower_2 = !is_BBL_printer && m_config->opt_enum("wipe_tower_type") == WipeTowerType::Type2; - toggle_line("wipe_tower_type", !is_BBL_printer); + toggle_line("wipe_tower_type", !is_BBL_printer && !is_belt_printer); // SoftFever: hide specific settings for BBL printer for (auto el : { "enable_filament_ramming", @@ -6531,6 +6666,40 @@ void TabPrinter::update_fff() m_use_silent_mode = m_config->opt_bool("silent_mode"); } + // Belt printer: auto-sync build_plate_tilt_{x,y} (which drives support gravity tilt) + // from the belt slicing rotation, the single source of truth for the physical tilt. + // Tilt about X drives tilt_x, tilt about Y drives tilt_y. + // + // R8: value-guessing (zeroing any tilt matching the dormant belt-derived tilt) wiped a + // legitimate manual build_plate_tilt on a non-belt tilted-bed printer, because the belt + // defaults (rotation=X, angle=45) make a manual tilt of 45 look belt-derived. Instead we + // track the belt->non-belt transition and the exact values belt-sync wrote, and clear the + // tilt only on a genuine in-place belt-off toggle, and only if the value is still what + // belt-sync last wrote. Preset switches reset the tracking in on_preset_loaded(), so they + // never trip the reset. + if (m_config->opt_bool("belt_printer")) { + auto rot_axis = m_config->option>("belt_slice_rotation")->value; + const auto tilt = BeltTransformPipeline::physical_tilt( + rot_axis, m_config->opt_float("belt_slice_rotation_angle")); + if (m_config->opt_float("build_plate_tilt_x") != tilt.tilt_x_deg) + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg)); + if (m_config->opt_float("build_plate_tilt_y") != tilt.tilt_y_deg) + m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg)); + // Remember exactly what belt-sync wrote, so an in-place belt-off toggle can distinguish + // a still-belt-derived tilt (safe to clear) from a since-edited manual one (keep). + m_belt_synced_tilt_x = tilt.tilt_x_deg; + m_belt_synced_tilt_y = tilt.tilt_y_deg; + } else if (m_was_belt_printer) { + // Genuine in-place belt->off toggle on the same preset (on_preset_loaded() was not called + // since the last update, so m_was_belt_printer still reflects belt mode). Clear each axis + // only if it still holds the value belt-sync last wrote; a manual override is preserved. + if (m_config->opt_float("build_plate_tilt_x") == m_belt_synced_tilt_x) + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(0.)); + if (m_config->opt_float("build_plate_tilt_y") == m_belt_synced_tilt_y) + m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(0.)); + } + m_was_belt_printer = m_config->opt_bool("belt_printer"); + toggle_options(); } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index e916dd6f7d..4bcca83f43 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -672,6 +672,12 @@ private: bool m_rebuild_kinematics_page = false; void update_input_shaper_menu(GCodeFlavor flavor); + // R8: track the belt->non-belt transition so update_fff() only clears the belt-derived + // build_plate_tilt on a genuine in-place belt-off toggle, never on a manual tilt or a + // preset switch. m_belt_synced_tilt_{x,y} hold the exact values belt-sync last wrote. + bool m_was_belt_printer = false; + double m_belt_synced_tilt_x = 0.; + double m_belt_synced_tilt_y = 0.; std::vector m_pages_fff; std::vector m_pages_sla; diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index 10896ad693..ee8da8f0b2 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -125,6 +125,19 @@ std::vector make_shaper_type_labels() return labels; } +// ORCA-Belt: PA Line / PA Pattern have belt plumbing in place (drawn on the +// belt surface via BeltKinematics world-coordinates mode) but are not +// validated yet — belt printers are restricted to the PA Tower for now. +bool is_belt_printer_selected() +{ + if (auto* preset_bundle = wxGetApp().preset_bundle) { + const auto& cfg = preset_bundle->printers.get_edited_preset().config; + const auto* opt = cfg.option("belt_printer"); + return opt != nullptr && opt->value; + } + return false; +} + } PA_Calibration_Dlg::PA_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plater* plater) @@ -342,6 +355,14 @@ void PA_Calibration_Dlg::on_start(wxCommandEvent& event) { m_params.mode = CalibMode::Calib_PA_Tower; } + // ORCA-Belt: backstop in case the selection slipped past the UI guards. + if (is_belt_printer_selected() && m_params.mode != CalibMode::Calib_PA_Tower) { + MessageDialog msg_dlg(nullptr, _L("PA Line and PA Pattern tests are not enabled yet on belt printers.\nPlease use the PA Tower method instead."), + wxEmptyString, wxICON_WARNING | wxOK); + msg_dlg.ShowModal(); + return; + } + m_params.print_numbers = m_cbPrintNum->GetValue(); ParseStringValues(m_tiBMAccels->GetTextCtrl()->GetValue().ToStdString(), m_params.accelerations); ParseStringValues(m_tiBMSpeeds->GetTextCtrl()->GetValue().ToStdString(), m_params.speeds); @@ -368,6 +389,9 @@ void PA_Calibration_Dlg::on_extruder_type_changed(wxCommandEvent& event) { event.Skip(); } void PA_Calibration_Dlg::on_method_changed(wxCommandEvent& event) { + // ORCA-Belt: only the PA Tower method is enabled on belt printers so far. + if (is_belt_printer_selected() && m_rbMethod->GetSelection() != 0) + m_rbMethod->SetSelection(0, true); PA_Calibration_Dlg::reset_params(); event.Skip(); } @@ -378,6 +402,17 @@ void PA_Calibration_Dlg::on_dpi_changed(const wxRect& suggested_rect) { } void PA_Calibration_Dlg::on_show(wxShowEvent& event) { + // ORCA-Belt: the dialog is cached across printer switches, so refresh the + // belt restriction on every show. + if (is_belt_printer_selected()) { + m_rbMethod->SetSelection(0); + const wxString tip = _L("Not enabled yet on belt printers — use the PA Tower method instead."); + m_rbMethod->SetRadioTooltip(1, tip); + m_rbMethod->SetRadioTooltip(2, tip); + } else { + m_rbMethod->SetRadioTooltip(1, wxEmptyString); + m_rbMethod->SetRadioTooltip(2, wxEmptyString); + } PA_Calibration_Dlg::reset_params(); } @@ -414,6 +449,17 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat method_box->Add(m_rbFilamentType, 0, wxALL | wxEXPAND, FromDIP(4)); v_sizer->Add(method_box, 0, wxTOP | wxRIGHT | wxLEFT | wxEXPAND, FromDIP(10)); + // Belt temperature-tower model: Standard (sectioned tower) vs Overhang (engraved + // inverted-L provini that stress overhang quality per temperature). Only affects + // belt printers; the upright tower ignores it, so the picker is only shown on + // belts. The dialog is cached across printer switches, so visibility is toggled + // per-show in on_show() rather than gated here at construction time. + auto labeled_box_model = new LabeledStaticBox(this, _L("Test model")); + m_model_box = new wxStaticBoxSizer(labeled_box_model, wxHORIZONTAL); + m_rbModel = new RadioGroup(this, { _L("Standard"), _L("Overhang") }, wxVERTICAL); + m_model_box->Add(m_rbModel, 0, wxALL | wxEXPAND, FromDIP(4)); + v_sizer->Add(m_model_box, 0, wxTOP | wxRIGHT | wxLEFT | wxEXPAND, FromDIP(10)); + // Settings wxString start_temp_str = _L("Start temp: "); wxString end_temp_str = _L("End temp: "); @@ -484,6 +530,11 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat m_rbFilamentType->Connect(wxEVT_COMMAND_RADIOBOX_SELECTED, wxCommandEventHandler(Temp_Calibration_Dlg::on_filament_type_changed), NULL, this); + // Refresh the belt-only model picker on every show — the dialog is cached and + // reused across printer switches. + this->Connect(wxEVT_SHOW, wxShowEventHandler(Temp_Calibration_Dlg::on_show)); + m_model_box->ShowItems(is_belt_printer_selected()); + wxGetApp().UpdateDlgDarkUI(this); Layout(); @@ -542,11 +593,27 @@ void Temp_Calibration_Dlg::on_start(wxCommandEvent& event) { m_params.end = end; m_params.nozzle_based_resize = m_cbResize->GetValue(); m_params.mode = CalibMode::Calib_Temp_Tower; + // Picker only exists on belt printers; default non-belt to the Standard model. + m_params.test_model = m_rbModel ? m_rbModel->GetSelection() : 0; m_plater->calib_temp(m_params); EndModal(wxID_OK); } +void Temp_Calibration_Dlg::on_show(wxShowEvent& event) { + // ORCA-Belt: the dialog is cached across printer switches, so refresh the + // belt-only "Test model" picker on every show. The Overhang model only + // applies to belt printers; hide it (and resize the dialog) otherwise. + const bool belt = is_belt_printer_selected(); + if (m_model_box->AreAnyItemsShown() != belt) { + m_model_box->ShowItems(belt); + Layout(); + Fit(); + GetSizer()->SetSizeHints(this); + } + event.Skip(); +} + void Temp_Calibration_Dlg::on_filament_type_changed(wxCommandEvent& event) { int selection = event.GetSelection(); unsigned long start = 0, end = 0; diff --git a/src/slic3r/GUI/calib_dlg.hpp b/src/slic3r/GUI/calib_dlg.hpp index 81d6b15200..eb0c5c9c01 100644 --- a/src/slic3r/GUI/calib_dlg.hpp +++ b/src/slic3r/GUI/calib_dlg.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "libslic3r/calib.hpp" namespace Slic3r { namespace GUI { @@ -62,9 +63,12 @@ protected: virtual void on_start(wxCommandEvent& event); virtual void on_filament_type_changed(wxCommandEvent& event); + void on_show(wxShowEvent& event); Calib_Params m_params; RadioGroup* m_rbFilamentType; + RadioGroup* m_rbModel = nullptr; + wxStaticBoxSizer* m_model_box = nullptr; TextInput* m_tiStart; TextInput* m_tiEnd; TextInput* m_tiStep; diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 5c550931f8..2d588aa2e0 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests test_extrusion_processor.cpp test_fill.cpp test_flow.cpp + test_gcode_processor.cpp test_gcode_timing.cpp test_gcode.cpp test_gcodeprocessor.cpp diff --git a/tests/fff_print/test_gcode_processor.cpp b/tests/fff_print/test_gcode_processor.cpp new file mode 100644 index 0000000000..d45d6534f7 --- /dev/null +++ b/tests/fff_print/test_gcode_processor.cpp @@ -0,0 +1,95 @@ +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include "test_utils.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; + +namespace { + +float processed_belt_tilt(const std::string &gcode) +{ + ScopedTemporaryFile temp(".gcode"); + { + std::ofstream os(temp.string()); + os << gcode; + } + GCodeProcessor proc; + proc.apply_config(FullPrintConfig{}); + proc.process_file(temp.string()); + return proc.get_result().belt_tilt_angle; +} + +constexpr const char *body = "G1 X10 Y10 Z0.2 F3000\nG1 X20 Y10 E1 F1200\n"; + +} // namespace + +TEST_CASE("The config block's belt angle does not mark G-code as belt G-code", "[GCodeProcessor][belt]") +{ + // Every printer's config block lists belt_slice_rotation_angle (default 45), belt or not. + const std::string gcode = std::string("; CONFIG_BLOCK_START\n; belt_printer = 0\n; belt_slice_rotation_angle = 45\n; CONFIG_BLOCK_END\n") + body; + CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(0., 1e-6)); +} + +TEST_CASE("The belt header's angle marks G-code as belt G-code", "[GCodeProcessor][belt]") +{ + const std::string gcode = std::string("; belt_slice_rotation_angle = -45.0\n") + body + + "; CONFIG_BLOCK_START\n; belt_printer = 1\n; belt_slice_rotation_angle = -45\n; CONFIG_BLOCK_END\n"; + CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(45., 1e-6)); +} + +TEST_CASE("Non-belt start G-code moves keep the first-layer Z in the processor", "[GCodeProcessor]") +{ + // The belt path (GCodeWriter tests: "start-gcode prepare-stage moves keep their real Z") + // stores the real Z of a move inside the start G-code. Every other printer must keep + // the historical behaviour: a prepare-stage move is pinned to the first-layer height + // so the preview does not draw the start sequence's travel. The gate is the belt + // header, so a file without one, whatever its config block says, takes this path. + struct BBLPrinterGuard { + bool prev = GCodeProcessor::s_IsBBLPrinter; + BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = false; } + ~BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = prev; } + } bbl_guard; + + const std::string gcode = + "G90\n" + "G21\n" + "M83\n" + ";TYPE:Custom\n" + "G1 E-1.5 F2100\n" + "G1 X45 Y0.3 Z50 F12000\n" // prepare-stage travel to a high Z + "G1 E1.5 F1800\n" + ";TYPE:Outer wall\n" + "G1 X46 Y0.3 Z50 E0.05\n" + "; CONFIG_BLOCK_START\n; belt_printer = 0\n; belt_slice_rotation_angle = 45\n; CONFIG_BLOCK_END\n"; + + GCodeProcessor processor; + FullPrintConfig config; + config.initial_layer_print_height.value = 0.3; + processor.apply_config(config); + processor.process_buffer(gcode); + const GCodeProcessorResult &result = processor.get_result(); + REQUIRE_THAT(result.belt_tilt_angle, WithinAbs(0., 1e-6)); + + size_t first_extrude = result.moves.size(); + for (size_t i = 0; i < result.moves.size(); ++i) + if (result.moves[i].type == EMoveType::Extrude) { first_extrude = i; break; } + REQUIRE(first_extrude < result.moves.size()); + REQUIRE(first_extrude > 0); + + // The extrusion keeps its real Z; the prepare-stage move before it is pinned to the + // first-layer height. + CHECK_THAT(result.moves[first_extrude].position.z(), WithinAbs(50., 1e-3)); + CHECK_THAT(result.moves[first_extrude - 1].position.z(), WithinAbs(0.3, 1e-3)); +} diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index d3d73c222b..3dbb3a986b 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -8,6 +8,7 @@ #include "libslic3r/Point.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/Extruder.hpp" +#include "libslic3r/Geometry.hpp" #include "libslic3r/libslic3r.h" #include #include @@ -36,11 +37,52 @@ #include #include "test_helpers.hpp" +#include +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include +#include +#include "libslic3r/GCode/BeltKinematics.hpp" +#include "libslic3r/BeltTransform.hpp" +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/PrintConfig.hpp" #include "libslic3r/Arrange.hpp" using namespace Slic3r; using namespace Slic3r::Test; +TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCodeWriter][belt]") +{ + PrintConfig config; + config.belt_printer.value = true; + config.belt_slice_rotation.value = BeltRotationAxis::X; + config.belt_slice_rotation_angle.value = 30.; + config.gcode_remap_x.value = RemapAxis::PosX; + config.gcode_remap_y.value = RemapAxis::PosZ; + config.gcode_remap_z.value = RemapAxis::PosY; + + GCodeWriter writer; + install_belt_kinematics(writer, config); + writer.set_axis_remap(int(config.gcode_remap_x.value), + int(config.gcode_remap_y.value), + int(config.gcode_remap_z.value)); + + // Start with a point in the unrotated model frame, then feed the writer the + // same rotated coordinate produced by the pre-slice mesh transform. The + // back-transform must recover the model point before the axis swap and + // machine-frame shear/scale are applied. + const Vec3d model(4., 10., 3.); + Transform3d forward = BeltTransformPipeline::build_forward_transform(config); + const Vec3d machine = writer.kinematics().to_machine(forward * model); + + // The conventional X-tilt remap produces (x, z, y). At 30 degrees the + // gantry coordinate is z/sin(30) and belt travel is y + z*cot(30). + // The complementary tan/inv-cos formulas accidentally used by the unified + // transform are indistinguishable at 45 degrees, but fail this case. + REQUIRE_THAT(machine.x(), Catch::Matchers::WithinAbs(4., 1e-9)); + REQUIRE_THAT(machine.y(), Catch::Matchers::WithinAbs(3. / std::sin(Geometry::deg2rad(30.)), 1e-9)); + REQUIRE_THAT(machine.z(), Catch::Matchers::WithinAbs(10. + 3. / std::tan(Geometry::deg2rad(30.)), 1e-9)); +} + // Arrange on a 500x500 bed, which keeps coordinates small while still covering large printers. static void arrange_objects_on_test_bed(Model &model, const DynamicPrintConfig &config) { @@ -966,6 +1008,440 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos); } +// Regression test for the belt-printer "illegal gantry move at print start" bug. +// +// On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed +// by the first travel_to_xyz, whose NormalLift branch lifts in place via +// _travel_to_z(). On a normal printer _travel_to_z emits a Z-only move, but in +// belt mode Z is coupled to Y/X, so _travel_to_z re-emits the current m_pos +// through the belt shear. At print start (and after custom gcode) +// is_current_position_clear() is false and m_pos.xy is still the uninitialised +// origin (0,0), which shears into machine (X=bed_max, Y=layer_z) — a move far up +// the gantry, e.g. "G1 X95 Y168.19 Z237.857". The fix guards that lift on +// is_current_position_clear(), mirroring the SlopeLift branch. +SCENARIO("Belt: the first travel does not lift through the uninitialised origin", "[GCodeWriter][belt]") +{ + GIVEN("A fresh belt-kinematics GCodeWriter configured for an X-tilt 45 degree belt") { + // Machine-frame + slicer->world back-transform config (X tilt, 45 deg). + PrintConfig belt_config; + belt_config.belt_printer.value = true; + belt_config.belt_slice_rotation.value = BeltRotationAxis::X; + belt_config.belt_slice_rotation_angle.value = 45.0; + belt_config.belt_frame_tilt_decouple.value = false; + belt_config.belt_frame_tilt_angle.value = 45.0; + + GCodeWriter writer; + install_belt_kinematics(writer, belt_config); + + std::vector extruder_ids { 0 }; + writer.set_extruders(extruder_ids); + writer.set_extruder(0); + // travel_speed became per-extruder (ConfigOptionFloatsNullable) upstream. + writer.config.travel_speed.values = { 100.0 }; + writer.config.z_hop.values = { 0.4 }; + writer.config.retract_lift_above.values = { 0.0 }; + writer.config.retract_lift_below.values = { 0.0 }; + + // A fresh writer has not established its planar position yet — this is the + // precondition that made the origin leak into the first move. + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a layer-change z-hop is pending and we travel to the first object point") { + // Defer a z-hop, exactly as a retract on layer change leaves it. + writer.lazy_lift(LiftType::NormalLift); + + // First object point in slicing coordinates: a near-belt point (y ~= -z) + // so its transformed gantry Y is small (~1mm). The bogus origin lift, in + // contrast, would shear to machine Y ~= nominal_z. + const double nominal_z = 100.0; + std::string gcode = writer.travel_to_xyz(Vec3d(10.0, -(nominal_z - 1.0), nominal_z)); + + THEN("no emitted move flies up the gantry; machine Y stays near the part") { + double max_y = std::numeric_limits::lowest(); + GCodeReader reader; + reader.parse_buffer(gcode, [&max_y](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1") && line.has(Y)) + max_y = std::max(max_y, double(line.y())); + }); + // The destination shears to machine Y ~= 1mm. The old origin-lift bug + // produced a separate move at machine Y ~= nominal_z (100mm), so any + // Y well above the part means the origin leaked into a move. + REQUIRE(max_y > 0.0); // the destination move was emitted and parsed + REQUIRE(max_y < 10.0); // ... and nothing flew up the gantry + } + } + } +} + +// Regression test for the belt-printer "phantom extrusion line from Y=0" bug. +// +// GCodeProcessor::store_move_vertex pins a move's stored Z to the first-layer +// height while m_processing_start_custom_gcode is set (the start G-code "prepare" +// stage), because on a normal printer the toolhead Z there is not yet a real print +// height. On a belt printer that override is wrong: Z is written explicitly and the +// designed-view back-transform couples machine Z into the rendered model Y (the +// belt tilt mixes the height and belt-feed axes). Overriding it back-transforms the +// last prepare-stage move (the unretract right before the first extrusion) to +// model Y ~= 0, and libvgcode then draws a phantom extrusion segment from Y ~= 0 to +// the first real toolpath — rendered in the first extrusion role's color. The fix +// keeps the real Z for belt printers (gated on belt_tilt_angle). Here we assert the +// prepare-stage move keeps its real Z so it can no longer leak to Y ~= 0. +SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][belt]") +{ + // Belt printers are non-Bambu, so the G-code uses the "compatible" reserved + // tags ("TYPE:" for the extrusion role). The processor selects the tag table + // from the static s_IsBBLPrinter flag, so mirror the belt-printer setting here + // (saved/restored so test ordering stays unaffected). + struct BBLPrinterGuard { + bool prev = GCodeProcessor::s_IsBBLPrinter; + BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = false; } + ~BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = prev; } + } bbl_guard; + + GIVEN("A belt G-code whose start sequence travels to a high machine Z before the first extrusion") { + // The leading "; belt_slice_rotation_angle = 45" header sets belt_tilt_angle + // (parsed before the body), enabling the belt code path. ;TYPE:Custom before + // any G1 turns on the prepare stage; ;TYPE:Outer wall turns it off, exactly + // as a sliced belt print is laid out. + const std::string gcode = + "; belt_slice_rotation_angle = 45\n" + "G90\n" + "G21\n" + "M83\n" + ";TYPE:Custom\n" + "G1 E-1.5 F2100\n" // retract at the (0,0,0) origin + "G1 X45 Y0.3 Z50 F12000\n" // travel to the approach point (prepare stage) + "G1 E1.5 F1800\n" // unretract in place (prepare stage) + ";TYPE:Outer wall\n" + "G1 X46 Y0.3 Z50 E0.05\n"; // first extrusion, same Z as the approach + + GCodeProcessor processor; + processor.process_buffer(gcode); + const GCodeProcessorResult& result = processor.get_result(); + + THEN("the belt code path is active") { + REQUIRE_THAT(result.belt_tilt_angle, Catch::Matchers::WithinAbs(45.0, 1e-4)); + } + + WHEN("locating the first extrusion and the move that precedes it") { + size_t first_extrude = result.moves.size(); + for (size_t i = 0; i < result.moves.size(); ++i) + if (result.moves[i].type == EMoveType::Extrude) { first_extrude = i; break; } + + THEN("an extrusion and a preceding move exist") { + REQUIRE(first_extrude < result.moves.size()); + REQUIRE(first_extrude > 0); + } + + THEN("the preceding prepare-stage move shares the extrusion's real Z (no leak to Y=0)") { + const float extrude_z = result.moves[first_extrude].position.z(); + const float prev_z = result.moves[first_extrude - 1].position.z(); + // The first extrusion is at the real Z=50; before the fix the + // prepare-stage move's Z was pinned to the first-layer height + // (0 here) instead, which back-transforms to model Y ~= 0 and + // produces the phantom extrusion segment. + REQUIRE_THAT(extrude_z, Catch::Matchers::WithinAbs(50.0, 1e-3)); + REQUIRE_THAT(prev_z, Catch::Matchers::WithinAbs(50.0, 1e-3)); + } + } + } +} + +// --------------------------------------------------------------------------- +// Regression tests for the two latent bugs the MachineKinematics refactor +// preserved deliberately and the follow-up commit fixed. +// --------------------------------------------------------------------------- + +// Bug 1. _travel_to_z() emits full XYZ whenever the mapping must emit every +// axis, and it builds that point from m_pos. While the position is unknown, +// m_pos.xy is the uninitialised origin, which a reverse remap maps to the far +// corner of the bed. Belt kinematics guarded this; a Cartesian writer with an +// axis remap did not, and would command a rapid across the whole bed. +static void configure_lift_writer(GCodeWriter &writer) +{ + std::vector extruder_ids { 0 }; + writer.set_extruders(extruder_ids); + writer.set_extruder(0); + writer.config.travel_speed.values = { 100.0 }; + writer.config.travel_speed_z.values = { 100.0 }; + writer.config.z_hop.values = { 0.4 }; + writer.config.retract_lift_above.values = { 0.0 }; + writer.config.retract_lift_below.values = { 0.0 }; +} + +// Largest X word in a chunk of emitted G-code, or lowest() if none. +static double max_emitted_x(const std::string &gcode) +{ + double max_x = std::numeric_limits::lowest(); + GCodeReader reader; + reader.parse_buffer(gcode, [&max_x](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1") && line.has(X)) + max_x = std::max(max_x, double(line.x())); + }); + return max_x; +} + +static size_t count_g1(const std::string &gcode) +{ + size_t n = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&n](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1")) ++n; + }); + return n; +} + +SCENARIO("Axis remap: no lift is commanded through the uninitialised origin", "[GCodeWriter][remap]") +{ + // Reverse X: machine X = build_vol_max.x - logical X, so the uninitialised + // origin maps to the far edge of the bed and is unmistakable in the output. + const double bed_x = 250.0; + + GIVEN("a writer with a reverse-X remap and an unknown current position") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0)); + REQUIRE(writer.kinematics().must_emit_all_axes()); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel to the first point") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0)); + + THEN("nothing is commanded at the image of the origin") { + // The destination maps to machine X = 250 - 10 = 240; the bogus + // origin lift would have mapped to machine X = 250. + REQUIRE(max_emitted_x(gcode) < bed_x - 1.0); + } + THEN("only the destination move is emitted") { + REQUIRE(count_g1(gcode) == 1); + } + } + } + + GIVEN("the same writer once its position is known") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0)); + writer.travel_to_xyz(Vec3d(20.0, 20.0, 5.0)); + REQUIRE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel again") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(30.0, 30.0, 5.0)); + + THEN("the separate lift move is still emitted") { + // Suppression must be pinned to the unknown position, not to the + // presence of a remap. + REQUIRE(count_g1(gcode) == 2); + } + } + } + + GIVEN("an identity-mapping writer with an unknown position") { + GCodeWriter writer; + configure_lift_writer(writer); + REQUIRE_FALSE(writer.kinematics().must_emit_all_axes()); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel to the first point") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0)); + + THEN("behaviour is unchanged: the lift is still emitted") { + // Three moves, not two: with no remap and an unknown position the + // destination is emitted as a separate XY move followed by its own + // Z move, on top of the lift. That split is the pre-existing + // identity-mapping path and must not change. + REQUIRE(count_g1(gcode) == 3); + } + } + } +} + +SCENARIO("Axis remap: eager_lift does not lift, or record a lift, at an unknown position", + "[GCodeWriter][remap]") +{ + GIVEN("a writer with a reverse-X remap and an unknown current position") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(250.0, 250.0, 250.0)); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("an eager lift is requested") { + const std::string lift = writer.eager_lift(LiftType::NormalLift); + + THEN("no move is emitted") { + REQUIRE(lift.empty()); + } + THEN("no lift is recorded, so unlift does not descend from it") { + // If m_lifted had been set while nothing was commanded, unlift() + // would emit a descent from a height the machine never reached. + REQUIRE(writer.unlift().empty()); + } + } + } + + GIVEN("an identity-mapping writer with an unknown position") { + GCodeWriter writer; + configure_lift_writer(writer); + + WHEN("an eager lift is requested") { + const std::string lift = writer.eager_lift(LiftType::NormalLift); + + THEN("behaviour is unchanged: the lift is emitted and can be undone") { + REQUIRE_FALSE(lift.empty()); + REQUIRE_FALSE(writer.unlift().empty()); + } + } + } +} + +// Bug 2. extrude_arc_to_xy() emits G2/G3 with logical X/Y and I/J and never +// consulted the mapping. An arc is only representable when logical X and Y reach +// the machine unchanged -- which is a narrower question than "is the remap the +// identity", because a mapping that only touches Z leaves every emitted word alone. +SCENARIO("Arc support is decided by whether the mapping leaves X and Y alone", "[GCodeWriter][remap]") +{ + GIVEN("a Cartesian writer") { + GCodeWriter writer; + + THEN("the identity mapping supports arcs") { + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("a Z-only negation still supports arcs") { + // (+X, +Y, -Z): non-identity, but X, Y, I and J are all untouched. + writer.set_axis_remap(0, 1, 5); + REQUIRE(writer.kinematics().must_emit_all_axes()); + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("a Z-only reversal still supports arcs") { + writer.set_axis_remap(0, 1, 8); + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("swapping X and Y does not support arcs") { + writer.set_axis_remap(1, 0, 2); + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + THEN("the X-tilt style (x, z, y) remap does not support arcs") { + writer.set_axis_remap(0, 2, 1); + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + } + + GIVEN("a belt writer") { + PrintConfig belt_config; + belt_config.belt_printer.value = true; + belt_config.belt_slice_rotation.value = BeltRotationAxis::X; + belt_config.belt_slice_rotation_angle.value = 45.0; + + GCodeWriter writer; + install_belt_kinematics(writer, belt_config); + + THEN("arcs are never supported, because the frame shears") { + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + } +} + +SCENARIO("An unrepresentable arc degrades to its chord rather than emitting a wrong G2/G3", + "[GCodeWriter][remap]") +{ + auto emitted_commands = [](const std::string &gcode) { + std::vector cmds; + GCodeReader reader; + reader.parse_buffer(gcode, [&cmds](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (! line.cmd().empty()) cmds.emplace_back(line.cmd()); + }); + return cmds; + }; + + GIVEN("an identity-mapping writer") { + GCodeWriter writer; + configure_lift_writer(writer); + + WHEN("an arc is extruded") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true); + + THEN("it is still a G3") { + const auto cmds = emitted_commands(gcode); + REQUIRE(cmds.size() == 1); + REQUIRE(cmds.front() == "G3"); + } + } + } + + GIVEN("a writer whose mapping swaps X and Y") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(1, 0, 2); + + WHEN("an arc is extruded") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true); + + THEN("no arc is emitted; it is approximated with linear moves") { + const auto cmds = emitted_commands(gcode); + REQUIRE(! cmds.empty()); + for (const auto &c : cmds) + REQUIRE(c == "G1"); + } + } + } + + // The first version of this test used dE = 0 with force_no_extrusion, which + // hid a real bug: the capability check sat AFTER filament()->extrude(dE), so + // the fallback into extrude_to_xy() advanced E twice. Extrusion accounting has + // to be asserted with a positive dE. + GIVEN("a writer whose mapping cannot express arcs, extruding a real amount") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(1, 0, 2); + const double dE = 1.5; + // used_filament() accumulates across moves; E() is reset per line in + // relative-E mode, so it would only show the last segment. + const double used_before = writer.filament()->used_filament(); + + WHEN("an arc carrying that extrusion is emitted") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false); + + THEN("exactly dE is accounted for, not twice dE") { + REQUIRE_THAT(writer.filament()->used_filament() - used_before, + Catch::Matchers::WithinAbs(dE, 1e-6)); + } + THEN("no G2/G3 survives") { + REQUIRE(gcode.find("G2") == std::string::npos); + REQUIRE(gcode.find("G3") == std::string::npos); + } + } + } + + GIVEN("a writer whose mapping CAN express arcs, extruding a real amount") { + GCodeWriter writer; + configure_lift_writer(writer); + const double dE = 1.5; + // used_filament() accumulates across moves; E() is reset per line in + // relative-E mode, so it would only show the last segment. + const double used_before = writer.filament()->used_filament(); + + WHEN("an arc carrying that extrusion is emitted") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false); + + THEN("it is still a single arc and accounts for dE once") { + REQUIRE(emitted_commands(gcode).size() == 1); + REQUIRE_THAT(writer.filament()->used_filament() - used_before, + Catch::Matchers::WithinAbs(dE, 1e-6)); + } + } + } +} + TEST_CASE("Percent accelerations resolve against the option they are a percentage of", "[GCodeWriter]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); diff --git a/tests/fff_print/test_helpers.cpp b/tests/fff_print/test_helpers.cpp index 14365a5860..15bf6ce143 100644 --- a/tests/fff_print/test_helpers.cpp +++ b/tests/fff_print/test_helpers.cpp @@ -457,7 +457,10 @@ int role_passes(const std::string &gcode, const std::string &role) bool in_role = false; GCodeReader reader; reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { - if (! line.extruding(self)) return; + // E-only unretraction moves have positive E but do not lay down material. Ignoring + // them keeps a role pass contiguous across travel/retraction bookkeeping. + if (! line.extruding(self) || (line.dist_XY(self) <= EPSILON && std::abs(line.dist_Z(self)) <= EPSILON)) + return; const bool is_role = line.comment().find(role) != std::string_view::npos; if (is_role && ! in_role) ++passes; in_role = is_role; diff --git a/tests/fff_print/test_precise_seam.cpp b/tests/fff_print/test_precise_seam.cpp index 170c867b95..b5cb913904 100644 --- a/tests/fff_print/test_precise_seam.cpp +++ b/tests/fff_print/test_precise_seam.cpp @@ -23,9 +23,11 @@ #include "libslic3r/GCode/SeamPlacer.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/ExPolygon.hpp" #include #include #include +#include using namespace Slic3r; @@ -1427,3 +1429,44 @@ TEST_CASE("Full containment warns for Blocked but not for Enforced", "[PreciseSe else CHECK(fixture.warning().empty()); } + +TEST_CASE("Belt printers slice Precise Seam modifiers in the frame the object was sliced in", "[PreciseSeam][belt]") +{ + Model model; + Print print; + Test::init_print({Test::cube(20)}, print, model, { + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + // Keep the first layer's islands the raw slice, like the modifier's. + { "elefant_foot_compensation", 0 }, + }); + print.process(); + const PrintObject *object = print.get_object(0); + REQUIRE(object->layer_count() > 0); + + // A modifier with the object's own mesh and placement must slice to the object's own islands + // on every layer. Sliced without the belt rotation it would give 20 mm squares on the lower + // layers and nothing above 20 mm, while the tilted cube reaches about 28 mm. + Model modifiers; + ModelVolume *modifier = modifiers.add_object()->add_volume(*model.objects.front()->volumes.front()); + modifier->set_type(ModelVolumeType::PRECISE_SEAM_BLOCKED); + const std::vector slices = object->slice_single_volume_regions(modifier); + REQUIRE(slices.size() == object->layer_count()); + + const double tolerance = scale_(0.05) * scale_(20.); + for (size_t i = 0; i < slices.size(); ++i) { + CAPTURE(i, object->get_layer(int(i))->slice_z); + const ExPolygons &islands = object->get_layer(int(i))->lslices; + double object_area = 0., modifier_area = 0.; + for (const ExPolygon &island : islands) + object_area += island.area(); + for (const ExPolygon ®ion : slices[i]) + modifier_area += region.area(); + CHECK(std::abs(modifier_area - object_area) < tolerance); + CHECK(get_extents(slices[i]).inflated(scale_(0.05)).contains(get_extents(islands))); + } +} diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 40f6343d95..58f8d12e39 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -14,6 +14,7 @@ #include #include #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/libslic3r.h" #include #include "libslic3r/Surface.hpp" #include "libslic3r/Config.hpp" @@ -26,6 +27,17 @@ #include "libslic3r/Print.hpp" #include "libslic3r/Layer.hpp" +#include "libslic3r/BuildVolume.hpp" +#include "libslic3r/Support/TreeModelVolumes.hpp" +#include "libslic3r/Support/TreeSupportCommon.hpp" +#include "libslic3r/Support/BeltFloorContext.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/Polyline.hpp" +#include +#include +#include +#include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -36,6 +48,10 @@ #include "test_utils.hpp" #include +#include +#include +#include +#include #include #include #include @@ -451,6 +467,88 @@ TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate CHECK(err.string.empty()); } +TEST_CASE("Purge tower selection keeps ordinary printers on the classic path", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 0 }, + { "enable_prime_tower", 1 }, + { "enable_belt_purge_tower", 1 } + }); + config.set_key_value("timelapse_type", new ConfigOptionEnum(TimelapseType::tlSmooth)); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + + CHECK(print.has_wipe_tower()); + CHECK_FALSE(print.has_belt_purge_tower()); +} + +TEST_CASE("Belt purge planning requires its managed purge object", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 } + }); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + CHECK_FALSE(print.has_belt_purge_tower()); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(print.has_belt_purge_tower()); + CHECK_FALSE(print.has_wipe_tower()); +} + +// The GUI creates the purge tower object; a project sliced without one (the CLI) must say +// that its filament changes go unpurged. +TEST_CASE("Belt purge tower enabled without a tower object warns", "[Print][PurgeTower][belt]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 }, + { "layer_change_gcode", "G92 E0\n" } + }); + auto purge_warnings = [](Print &print) { + std::vector warnings; + print.validate(&warnings); + return std::count_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_belt_purge_tower"; + }); + }; + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/2, /*overlap=*/false); + model.objects[1]->config.set_key_value("extruder", new ConfigOptionInt(2)); + print.apply(model, config); + REQUIRE(print.extruders().size() > 1); + CHECK(purge_warnings(print) == 1); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(purge_warnings(print) == 0); +} + +TEST_CASE("Belt purge rejects multiple managed purge objects", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 } + }); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/2, /*overlap=*/false); + for (ModelObject *object : model.objects) + object->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + + CHECK_FALSE(print.validate().string.empty()); +} + TEST_CASE("A default slice emits perimeter, infill, and skirt", "[Print]") { const std::string gcode = slice({ cube(20) }, { @@ -661,6 +759,199 @@ TEST_CASE("Sequential printing publishes the nozzle group result", "[Print][Mult } } +// A scarf joint starts one layer height below the layer and ramps up along the +// wall. On a tilted belt that start is a step backwards along the belt axis, into +// the previous layer's wall at the seam: 0.283 mm per 0.2 mm layer at 45 degrees. +// With an aligned seam the nozzle rams the same spot on every layer (field report +// from a BabyBelt Pro: the belt "jumped backwards" and knocked the part loose). +// Belt printers therefore never get a scarf, whatever the process preset says. +TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][belt][Seam]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "wall_loops", 2 }, + { "seam_position", "back" }, + { "seam_slope_type", "external" }, + { "seam_slope_inner_walls", 1 }, + { "seam_slope_start_height", 0 }, + // No z-hop: on a belt a lift is a move along the belt axis (0.4 mm / sin 45 = 0.57 mm) + // and its return would read as a back-step. The shipped belt profiles print without one. + { "z_hop", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The belt axis is machine Z. Within a layer it only drifts by the frame + // coupling (well under 0.1 mm across a 20 mm cube); a scarf start is a full + // layer pitch (0.283 mm) backwards. + double last_z = std::numeric_limits::lowest(); + double worst_backstep = 0.; + GCodeReader parser; + parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (! line.cmd_is("G1") || ! line.has_z()) + return; + const double z = line.z(); + if (last_z != std::numeric_limits::lowest()) + worst_backstep = std::max(worst_backstep, last_z - z); + last_z = z; + }); + CHECK(worst_backstep < 0.2); +} + +// printable_height on a belt printer is the clearance under the gantry, so an object taller +// than that is refused whatever the machine-frame transform does to the emitted coordinates. +TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[Print][belt]") +{ + auto belt_config = [](double printable_height) { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "printable_height", printable_height }, + { "skirt_loops", 0 }, + { "layer_change_gcode", "G92 E0\n" }, + }); + return config; + }; + + SECTION("a 20 mm cube fits under 50 mm of clearance") { + Print print; + Model model; + init_print({ cube(20) }, print, model, belt_config(50)); + CHECK(print.validate().string.empty()); + } + SECTION("a 60 mm cube does not") { + Print print; + Model model; + init_print({ cube(60) }, print, model, belt_config(50)); + CHECK(print.validate().string.find("height") != std::string::npos); + } +} + +// On a belt every tilted layer starts on the belt, so "the first layers" the fan stays off +// for are a band along the belt, not the first slicing layers. The generator marks where +// each extrusion segment enters and leaves that band and the cooling buffer keeps the fan +// off inside it, on every layer. +TEST_CASE("Belt printers keep the part fan off within the band above the belt", "[Print][belt][Cooling]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + // Three layers, 0.6 mm: the lowest wall of each tilted layer is centred about 0.3 mm + // above the belt (half a line width in from the contact edge). + { "close_fan_the_first_x_layers", 3 }, + { "full_fan_speed_layer", 0 }, + { "fan_min_speed", 100 }, + { "fan_max_speed", 100 }, + { "slow_down_layer_time", 1000 }, + { "fan_cooling_layer_time", 1001 }, + { "reduce_fan_stop_start_freq", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The markers are consumed by the cooling buffer and never reach the file. + CHECK(gcode.find(";_BELT_BAND") == std::string::npos); + + // With this axis mapping machine Y is the height above the belt along the gantry. Walk + // the moves with the fan state: extrusions that stay within 0.45 mm of the belt are well + // inside the band and must print with the fan off; extrusions that stay 5 mm clear of it + // must print with it on. The first three slicing layers have the fan off altogether. + size_t in_band = 0, in_band_fan_on = 0, clear = 0, clear_fan_off = 0; + int layer = -1; + bool fan_on = false; + double y = 0.; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";LAYER_CHANGE")) { + ++ layer; + } else if (boost::starts_with(line, "M107")) { + fan_on = false; + } else if (boost::starts_with(line, "M106")) { + const size_t s = line.find('S'); + fan_on = s != std::string::npos && std::atof(line.c_str() + s + 1) > 0.; + } else if (boost::starts_with(line, "G1 ")) { + const size_t comment = line.find(';'); + const std::string cmd = line.substr(0, comment); + const size_t ypos = cmd.find(" Y"), epos = cmd.find(" E"); + if (ypos == std::string::npos) + continue; + const double y_new = std::atof(cmd.c_str() + ypos + 2); + const bool extruding = epos != std::string::npos && std::atof(cmd.c_str() + epos + 2) > 0.; + if (extruding && layer >= 3) { + if (std::max(y, y_new) < 0.45) { + ++ in_band; + in_band_fan_on += fan_on; + } else if (std::min(y, y_new) > 5.) { + ++ clear; + clear_fan_off += ! fan_on; + } + } + y = y_new; + } + } + CHECK(in_band > 20); + CHECK(in_band_fan_on == 0); + CHECK(clear > 20); + CHECK(clear_fan_off == 0); +} + +// Organic supports under an overhang on a belt printer reach below the object's first layer, +// where the virtual belt raft layers sit at negative Z. The lowest of them used to get a +// negative height and abort slicing with a negative flow error. +TEST_CASE("Belt printers slice organic tree supports that reach the belt", "[Print][belt][Support]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + std::string gcode; + REQUIRE_NOTHROW(gcode = slice({ TestMesh::overhang }, config)); + CHECK(! gcode.empty()); +} + TEST_CASE("Slicing errors are reported per object with the object's name", "[Print]") { Print print; @@ -686,3 +977,546 @@ TEST_CASE("Slicing errors are reported per object with the object's name", "[Pri CHECK(message.rfind("floating cube: ", 0) == 0); CHECK(message.find("empty first layer") != std::string::npos); } + +// --------------------------------------------------------------------------- +// Belt mode must be invisible when it is off, and must not leave traces behind. +// --------------------------------------------------------------------------- + +// Everything the slicer decided, without the lines that legitimately differ between +// two exports of the same print: comments (the config block lists every key, the +// header carries the export time) and the thumbnail blocks. +static std::string gcode_body(const std::string &gcode) +{ + std::string body; + std::istringstream in(gcode); + for (std::string line; std::getline(in, line); ) { + line.erase(std::min(line.size(), line.find(';'))); + while (! line.empty() && line.back() == ' ') + line.pop_back(); + if (! line.empty()) + body += line + '\n'; + } + return body; +} + +static DynamicPrintConfig belt_test_config() +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + return config; +} + +TEST_CASE("Belt-only keys at non-default values leave non-belt G-code unchanged", "[Print][belt][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "z_hop", 0 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "sparse_infill_pattern", "adaptivecubic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string reference = gcode_body(slice({ TestMesh::overhang }, config)); + REQUIRE(! reference.empty()); + + // Every belt key a profile can carry, at a value that would change a belt print. + // belt_printer stays off, so none of them may reach the G-code: the axis remaps are + // gated on belt mode, the rest is only read on belt printers. build_plate_tilt_x/y + // is a feature of its own on a flat bed and is left alone here; "leading_edge_only" + // prints as an outer brim by design. + config.set_deserialize_strict({ + { "belt_printer", 0 }, + { "belt_printer_infinite_y", 0 }, + { "belt_slice_rotation", "y" }, + { "belt_slice_rotation_angle", 30 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "belt_frame_tilt_decouple", 1 }, + { "belt_frame_tilt_angle", 30 }, + { "belt_support_floor_offset", -5 }, + { "enable_belt_purge_tower", 1 }, + { "belt_purge_tower_width", 10 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 5 }, + }); + CHECK(gcode_body(slice({ TestMesh::overhang }, config)) == reference); +} + +TEST_CASE("Switching a sliced project from belt to non-belt matches a fresh slice", "[Print][belt][Regression]") +{ + // The organic support layers and the adaptive infill octree are placed with the + // belt global Z offset, and the mesh with the belt min-Z lift. Both are only + // written while belt mode slices, so they used to survive a switch away from it. + DynamicPrintConfig flat = DynamicPrintConfig::full_print_config(); + flat.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "sparse_infill_pattern", "adaptivecubic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + DynamicPrintConfig belt = belt_test_config(); + belt.set_deserialize_strict({ + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "sparse_infill_pattern", "adaptivecubic" }, + }); + + // Both prints are placed with the belt config, so only the slicing history differs. + auto fresh_slice = [&](const DynamicPrintConfig &target) { + Print print; + Model model; + init_print({ TestMesh::overhang }, print, model, belt); + print.apply(model, target); + const std::string out = gcode(print); + return gcode_body(out); + }; + auto resliced = [&](const DynamicPrintConfig &target) { + Print print; + Model model; + init_print({ TestMesh::overhang }, print, model, belt); + REQUIRE(! gcode(print).empty()); + print.apply(model, target); + const std::string out = gcode(print); + return gcode_body(out); + }; + SECTION("belt printer to a flat-bed printer") { + CHECK(resliced(flat) == fresh_slice(flat)); + } + SECTION("belt tilt axis set to None") { + DynamicPrintConfig untilted = belt; + untilted.set_deserialize_strict({ { "belt_slice_rotation", "none" } }); + CHECK(resliced(untilted) == fresh_slice(untilted)); + } +} + +TEST_CASE("A support-only change on a belt purge print matches a fresh slice", "[Print][belt][PurgeTower][Regression]") +{ + // Snapping the purge prism onto the parts' layer grid shifts every object's layers by + // up to half a layer. A support-only change reruns support generation without + // reslicing, so the cached belt floor and the global Z offset have to carry the + // snap too, or the supports land on the pre-snap grid. + auto make_config = [](bool support) { + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_belt_purge_tower", 1 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + config.set_deserialize_strict({ + { "enable_support", support ? 1 : 0 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + }); + return config; + }; + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + auto build = [&](Print &print, Model &model, const DynamicPrintConfig &config) { + init_print(std::vector{ mesh(TestMesh::overhang), cube(20) }, print, model, config, &overrides); + model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + REQUIRE(print.has_belt_purge_tower()); + }; + + std::string fresh; + { + Print print; + Model model; + build(print, model, make_config(true)); + fresh = gcode_body(gcode(print)); + } + REQUIRE(! fresh.empty()); + + Print print; + Model model; + build(print, model, make_config(false)); + REQUIRE(! gcode(print).empty()); + // Support only: posSlice stays valid, posSupportMaterial reruns. + print.apply(model, make_config(true)); + CHECK(gcode_body(gcode(print)) == fresh); +} + +TEST_CASE("Organic tree supports place a support blocker at its own height above a raft", "[Print][Support][Regression]") +{ + // TreeModelVolumes consumes the support blockers in the same index space as the + // layer outlines, where object layer i sits at num_raft_layers + i, but + // slice_support_blockers() returns them in object-layer space. Without the shift + // every blocker lands num_raft_layers too low, so branches are kept out of the + // wrong layers and may pass through the blocked ones. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "raft_layers", 3 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + // A blocker floating beside the cube, 8 mm to 12 mm above the bed, so a collision at + // its centre can only come from the blocker itself (the part keeps its mesh + // coordinates in object space, hence the offset relative to the part). + ModelObject *object = model.objects.front(); + ModelVolume *blocker = object->add_volume(TriangleMesh(its_make_cube(6., 6., 4.))); + blocker->set_type(ModelVolumeType::SUPPORT_BLOCKER); + const Vec3d part_offset = object->volumes.front()->get_offset(); + blocker->set_offset(Vec3d(part_offset.x() + 20., part_offset.y(), 10.)); + print.apply(model, config); + print.set_status_silent(); + print.process(); + + const PrintObject &print_object = *print.objects().front(); + const std::vector bed = { { 0., 0. }, { 200., 0. }, { 200., 200. }, { 0., 200. } }; + const BuildVolume build_volume{ bed, print.config().printable_height.value, {}, {} }; + TreeSupport3D::TreeModelVolumes volumes{ print_object, build_volume, scaled(1.), scaled(0.5), 0, {} }; + + // The generator's raft layer count: the raft itself plus the gap layers up to the object. + const size_t num_raft = TreeSupport3D::TreeSupportSettings(TreeSupport3D::TreeSupportMeshGroupSettings(print_object), + print_object.slicing_parameters()).raft_layers.size(); + REQUIRE(num_raft >= 3); + // Object layers the blocker was sliced into (object-layer space, as the generator + // receives them). + const std::vector blockers = print_object.slice_support_blockers(); + size_t first = 0, last = 0; + bool found = false; + for (size_t i = 0; i < blockers.size(); ++ i) + if (! blockers[i].empty()) { + if (! found) { first = i; found = true; } + last = i; + } + REQUIRE(found); + REQUIRE(last - first > num_raft); + // The blocker's centre in the slicing frame (add_volume centred its mesh on its offset). + const Vec3d centre3 = print_object.trafo_sliced() * blocker->get_offset(); + const Point centre = Point::new_scale(centre3.x(), centre3.y()); + auto collides = [&](size_t tree_layer) { + for (const Slic3r::Polygon &poly : volumes.getCollision(0, TreeSupport3D::LayerIndex(tree_layer), false)) + if (poly.contains(centre)) + return true; + return false; + }; + // In TreeModelVolumes' index space the blocker lives at num_raft + object layer. + CHECK(collides(num_raft + first)); + CHECK(collides(num_raft + last)); + // The layers just below it, where an unshifted blocker would land, are free; the + // layers just above the unshifted range, which the blocker does occupy, are not. + CHECK_FALSE(collides(first)); + CHECK_FALSE(collides(first + num_raft - 1)); + CHECK(collides(last + 1)); + CHECK(collides(last + num_raft)); +} + +// organic_draw_branches() trims every branch slice against the collision volume (the +// part grown by the support XY distance), the bed and, on a belt, the belt plane before +// it becomes support, so a branch never runs into the part it supports. Not a belt +// feature: this is the generator every printer uses. +TEST_CASE("Organic tree supports keep their distance from the part", "[Print][Support]") +{ + // A 20 mm cube carrying a 60 x 60 mm plate: a 20 mm wide ceiling all around the + // cube, 16 mm above the bed, with the cube's four corners in the way of the branches + // that drop from it. The plate reaches into the cube so the two shells overlap + // instead of sharing a face. + indexed_triangle_set its = its_make_cube(20., 20., 20.); + indexed_triangle_set plate = its_make_cube(60., 60., 4.); + its_translate(its, Vec3f(20.f, 20.f, 0.f)); + its_translate(plate, Vec3f(0.f, 0.f, 16.f)); + its_merge(its, plate); + TriangleMesh mesh(std::move(its)); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "support_threshold_angle", 30 }, + }); + Print print; + Model model; + init_print({ mesh }, print, model, config); + // On the bed, not at its corner (the fixture leaves the object at the origin). + model.objects.front()->instances.front()->set_offset(Vec3d(100., 100., 0.)); + print.apply(model, config); + print.set_status_silent(); + print.process(); + + const PrintObject &object = *print.objects().front(); + INFO("object layers " << object.layers().size() << ", support layers " << object.support_layers().size()); + REQUIRE(! object.support_layers().empty()); + // Support exists under the plate at all. + size_t support_layers_with_fills = 0; + for (const SupportLayer *layer : object.support_layers()) + if (! layer->support_fills.empty()) + ++ support_layers_with_fills; + INFO("support layers with extrusions " << support_layers_with_fills); + CHECK(support_layers_with_fills > 20); + + // Object layers by print_z, to look up the part's slice at a support layer's height. + std::map object_layers; + for (const Layer *layer : object.layers()) + object_layers[scaled(layer->print_z)] = layer; + auto contains = [](const ExPolygons &expolys, const Point &pt) { + for (const ExPolygon &ex : expolys) + if (ex.contains(pt)) + return true; + return false; + }; + // No support extrusion may run closer to the part's slice than half a line width: + // the generator keeps the support XY distance (0.35 mm by default) plus the line's + // own half width away from it. + const float min_gap = scaled(0.2); + size_t too_close = 0, points = 0, layers_checked = 0, layers_unmatched = 0; + for (const SupportLayer *layer : object.support_layers()) { + if (layer->support_fills.empty()) + continue; + // The object layer whose slab spans this support layer's height. + auto it = object_layers.lower_bound(scaled(layer->print_z - EPSILON)); + if (it == object_layers.end()) { + ++ layers_unmatched; + continue; + } + ++ layers_checked; + const ExPolygons grown = offset_ex(it->second->lslices, min_gap); + for (const ExtrusionEntity *entity : layer->support_fills.flatten().entities) + for (const Slic3r::Polyline &pl : entity->as_polylines()) + for (size_t i = 0; i < pl.points.size(); ++ i) { + // The vertices and the midpoints of the segments between them. + ++ points; + if (contains(grown, pl.points[i])) + ++ too_close; + if (i + 1 < pl.points.size() && contains(grown, (pl.points[i] + pl.points[i + 1]) / 2)) + ++ too_close; + } + } + INFO("support layers checked " << layers_checked << " (unmatched " << layers_unmatched << "), support points " << points + << ", within 0.2 mm of the part " << too_close); + CHECK(layers_checked > 20); + CHECK(layers_unmatched == 0); + REQUIRE(points > 0); + CHECK(too_close == 0); +} + +// Two parts along the belt: the second part's slicing frame starts at the belt +// below its leading end, so its first layers are empty and interleave with the +// first part's printing layers. Those must not reach the G-code as layer changes +// that print nothing: the preview numbers its layers from the moves it sees, and +// a gap folded every later layer into the one before it. +TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + // Both export paths drop the empty layers and count the layers the same way. + SECTION("by layer") { config.set_deserialize_strict({{ "print_sequence", "by layer" }}); } + SECTION("by object") { config.set_deserialize_strict({{ "print_sequence", "by object" }}); } + Print print; + Model model; + TriangleMesh cube_a(its_make_cube(20., 20., 20.)); + TriangleMesh cube_b(its_make_cube(20., 20., 20.)); + init_print({ cube_a, cube_b }, print, model, config); + // 60 mm apart along the belt: the second cube's lead-in layers fall among the + // first cube's layers. + model.objects[0]->instances.front()->set_offset(Vec3d(50., 40., 0.)); + model.objects[1]->instances.front()->set_offset(Vec3d(50., 100., 0.)); + print.apply(model, config); + print.set_status_silent(); + const std::string gc = gcode(print); + REQUIRE(! gc.empty()); + + size_t layers = 0, empty = 0, total_header = 0, total_count = 0; + bool extruded = true; // before the first layer change + auto close_layer = [&]() { if (! extruded) ++ empty; }; + GCodeReader reader; + reader.apply_config(config); + reader.parse_buffer(gc, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + const std::string &raw = line.raw(); + if (raw.rfind(";LAYER_CHANGE", 0) == 0) { + close_layer(); + ++ layers; + extruded = false; + } else if (raw.rfind("; total layer number: ", 0) == 0) { + // Counted by the G-code processor from the layer changes it saw. + total_header = size_t(std::atoi(raw.c_str() + 22)); + } else if (raw.rfind("; total layers count = ", 0) == 0) { + // GCode::m_layer_count, counted up front from the objects' layers; it also + // drives the M73 progress and the total_layer_count placeholder. + total_count = size_t(std::atoi(raw.c_str() + 23)); + } else if (! extruded && line.extruding(self) && line.dist_XY(self) > EPSILON) { + // Material laid down along a move: a wipe or an unretraction does not count. + extruded = true; + } + }); + close_layer(); + INFO("layers " << layers << ", header " << total_header << ", count " << total_count + << ", layers without extrusion " << empty); + CHECK(layers > 150); // both cubes, 141 layers each, overlapping along the belt + CHECK(empty == 0); + CHECK(total_header == layers); + CHECK(total_count == layers); +} + +// A part with an overhang on its LEADING side (the end that prints first) needs +// supports below the object's own lowest slicing layer: the belt under that overhang +// is reached before the object's first contact with it, so the support layers sit at +// a lower slicing Z than any object layer. A generator that stops at the object's +// first layer, or at global Z = 0, leaves those supports floating above the belt. +TEST_CASE("Belt supports reach the belt under a leading overhang", "[Print][belt][Support][Regression]") +{ + // default resolves to organic for tree support; tree_hybrid is the classic tree. + const char *support_type = GENERATE("normal(auto)", "tree(auto)"); + const char *support_style = GENERATE("default", "organic", "tree_hybrid"); + if (std::string(support_type) == "normal(auto)" && std::string(support_style) != "default") + return; // organic and tree_hybrid are tree styles + DYNAMIC_SECTION(support_type << " / " << support_style) { + // A 20 mm cube with a 2 mm thick fin that leaves its top edge and reaches + // 20 mm toward -Y, the end of the part that prints first, climbing at 45 deg + // as it goes (from z = 18 at the cube to z = 38 at the tip). With the layers + // leaning toward -Y at 45 deg the fin's underside is parallel to the layers: + // a ceiling 20 x 28 mm in one layer, with nothing but air between it and the + // belt, which lies up to 41 mm (of slicing Z) below the object's own lowest + // point. Support has to span all of it. + indexed_triangle_set its = its_make_cube(20., 20., 20.); + indexed_triangle_set fin = its_make_cube(20., 20., 2.); + Transform3d shear = Transform3d::Identity(); + shear.matrix() << 1., 0., 0., 0., + 0., 1., 0., -20., + 0., -1., 1., 38., + 0., 0., 0., 1.; + its_transform(fin, shear); + its_merge(its, fin); + TriangleMesh mesh(std::move(its)); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_support", 1 }, + { "support_type", support_type }, + { "support_style", support_style }, + { "support_threshold_angle", 30 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + Print print; + Model model; + init_print({ mesh }, print, model, config); + // On the bed, not at its corner: organic tree support clips its branches to + // the bed outline, and the fixture leaves the object at the origin. + model.objects.front()->instances.front()->set_offset(Vec3d(100., 100., 0.)); + print.apply(model, config); + print.set_status_silent(); + print.process(); + + const PrintObject &object = *print.objects().front(); + REQUIRE(! object.layers().empty()); + // The whole part is sliced: the layers lean at 45 deg, so the part spans + // (y + z) / sqrt(2) of slicing Z, and every layer in that span has geometry. + { + double lo = std::numeric_limits::max(), hi = std::numeric_limits::lowest(); + for (const stl_vertex &v : mesh.its.vertices) { + lo = std::min(lo, v.y() + v.z()); + hi = std::max(hi, v.y() + v.z()); + } + const double span = (hi - lo) / std::sqrt(2.); + size_t nonempty = 0; + for (const Layer *layer : object.layers()) + if (! layer->lslices.empty()) + ++ nonempty; + INFO("non-empty object layers " << nonempty << ", slicing span " << span << " mm"); + CHECK(double(nonempty) * 0.2 > span - 0.6); + } + BeltFloorContext floor; + REQUIRE(floor.init(object.slicing_parameters(), print.config())); + + // The lowest support layer that prints anything, and the belt floor beneath it. + const SupportLayer *lowest = nullptr; + for (const SupportLayer *layer : object.support_layers()) + if (! layer->support_fills.empty() && (lowest == nullptr || layer->print_z < lowest->print_z)) + lowest = layer; + REQUIRE(lowest != nullptr); + double floor_under_lowest = std::numeric_limits::max(); + for (const ExtrusionEntity *entity : lowest->support_fills.flatten().entities) + for (const Slic3r::Polyline &pl : entity->as_polylines()) + for (const Point &pt : pl.points) + floor_under_lowest = std::min(floor_under_lowest, floor.floor_print_z(pt)); + // The object's lowest geometry. The slicing frame starts at the lowest + // belt-floor point under the footprint, so the layers below the leading + // tip of the overhang are empty. + double first_object_z = std::numeric_limits::max(); + for (const Layer *layer : object.layers()) + if (! layer->lslices.empty()) { first_object_z = layer->print_z; break; } + REQUIRE(first_object_z < std::numeric_limits::max()); + INFO("lowest support z " << lowest->print_z << ", floor under it " << floor_under_lowest + << ", first object layer " << first_object_z); + // Well below the object's own lowest layer (the belt under the tip of the fin + // is ~41 mm of slicing Z below the cube's leading edge, which rests on it)... + CHECK(lowest->print_z < first_object_z - 5.); + // ...and resting on the belt: within a few layers of the floor beneath its own lines. + CHECK(lowest->print_z - floor_under_lowest < 4. * 0.2 + EPSILON); + CHECK(lowest->print_z - floor_under_lowest > -0.2 - EPSILON); + } +} diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 34a1260b7c..c51dd5ebc6 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -6,13 +6,15 @@ #include #include #include +#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/GCodeReader.hpp" +#include "libslic3r/Layer.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" -#include "libslic3r/Layer.hpp" #include +#include #include #include #include "libslic3r/Polygon.hpp" @@ -24,6 +26,13 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/libslic3r.h" #include +#include +#include +#include +#include +#include +#include +#include #include "test_helpers.hpp" // get access to init_print, etc #include "libslic3r/BoundingBox.hpp" @@ -32,6 +41,10 @@ #include "libslic3r/ExtrusionEntityCollection.hpp" #include "libslic3r/ObjectID.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/BeltBrim.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/TriangleMesh.hpp" using namespace Slic3r::Test; using namespace Slic3r; @@ -612,3 +625,787 @@ SCENARIO("Skirt and brim generation", "[SkirtBrim]") { } } } + +// Belt printers --------------------------------------------------------------- +// +// On a tilted belt the brim is laid onto the belt PLANE rather than into the Z=0 +// bed plane, so it is spread across many layers instead of living on the first +// one. The discriminating measurement is the number of contiguous brim runs in +// the G-code: a flat plate brim gives a single run, a belt brim gives one per +// layer that carries a band. Distinct Z values are useless here, because the +// machine-frame transform couples Y into Z so every belt move has its own Z. +static DynamicPrintConfig belt_brim_config() +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + return config; +} + +// Same belt as belt_brim_config(), but with `filaments` distinct filaments so the brim's +// tool selection can be observed. Kept separate from belt_brim_config() so the existing +// single-filament belt tests are untouched. +static DynamicPrintConfig belt_brim_multifilament_config(unsigned int filaments, + std::initializer_list extra = {}) +{ + DynamicPrintConfig config = multifilament_config(filaments); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + if (extra.size() > 0) + config.set_deserialize_strict(extra); + return config; +} + +// 0-based tool indices used by extrusions whose role comment contains `role` (needs +// gcode_comments). Mirrors tools_for_role in test_multifilament.cpp; statics do not cross +// translation units, so it is repeated here. +static std::set belt_tools_for_role(const std::string &gcode, const std::string &role) +{ + std::set tools; + int current_tool = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + const std::string cmd(line.cmd()); + if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char) cmd[1])) + current_tool = std::stoi(cmd.substr(1)); + else if (line.extruding(self) && std::string(line.comment()).find(role) != std::string::npos) + tools.insert(current_tool); + }); + return tools; +} + +// Machine Z of the first extruding move whose role comment contains `role`, in file order; +// numeric_limits::max() when the role never extrudes. +static double first_role_z(const std::string &gcode, const std::string &role) +{ + double z = std::numeric_limits::max(); + GCodeReader parser; + parser.parse_buffer(gcode, [&z, &role](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (line.extruding(self) && line.comment().find(role) != std::string_view::npos) { + z = self.z(); + self.quit_parsing(); + } + }); + return z; +} + +// Number of object layers that carry a belt brim band. Every band prints at its own +// layer Z, so this equals role_layers(gcode, "brim") (plus any apron bands below the +// first object layer). It is not a pass count: the bands on the empty lead-in layers +// ahead of the object's first contact print back to back, so they fold into one pass. +static int nonempty_belt_brim_layers(const PrintObject &object) +{ + int n = 0; + for (const ExtrusionEntityCollection &band : object.belt_brim_by_layer()) + if (! band.empty()) + ++ n; + return n; +} + +// Number of distinct Z heights at which `role` extrudes: one per layer that prints it. +static int role_layers(const std::string &gcode, const std::string &role) +{ + std::set zs; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (! line.extruding(self) || line.dist_XY(self) <= EPSILON) + return; + if (line.comment().find(role) != std::string_view::npos) + zs.insert(std::lround(self.z() * 1000.)); + }); + return int(zs.size()); +} + +// Apron bands below the object's first layer that print something. +static int belt_brim_apron_bands(const PrintObject &object) +{ + int n = 0; + for (const BeltBrimBand &band : object.belt_brim_prologue()) + if (! band.fills.empty()) + ++ n; + return n; +} + +// For each active tool, the ordinal (1-based, over extruding moves) of the FIRST move whose +// role comment contains `role`. Lets a per-object ordering check key off the object's +// unique wall filament. +static std::map first_move_by_tool(const std::string &gcode, const std::string &role) +{ + std::map first; + int tool = 0; + long idx = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + const std::string cmd(line.cmd()); + if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char) cmd[1])) { + tool = std::stoi(cmd.substr(1)); + return; + } + if (! line.extruding(self)) + return; + ++ idx; + if (std::string(line.comment()).find(role) != std::string::npos && ! first.count(tool)) + first[tool] = idx; + }); + return first; +} + +// C - the band coincident with the object's FIRST contact with the belt must not be dropped: +// a belt brim has to appear at or below the object's first perimeter. On the unfixed feature +// the first-contact band is dropped and the first brim then appears only at a later (higher) +// layer. Machine Z is meaningful and shared between roles under the belt remap, so the first +// brim's Z must not exceed the first perimeter's. Both with and without support. +TEST_CASE("Belt brim is laid at the object's first belt contact", "[SkirtBrim][belt]") +{ + const bool support = GENERATE(false, true); + DYNAMIC_SECTION("enable_support=" << support) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "leading_brim_length", 0 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + { "enable_support", support ? 1 : 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + + const double brim_z = first_role_z(gcode, "brim"); + const double peri_z = first_role_z(gcode, "perimeter"); + REQUIRE(brim_z < std::numeric_limits::max()); + REQUIRE(peri_z < std::numeric_limits::max()); + CHECK(brim_z <= peri_z + EPSILON); + } +} + +// C control - when the band's own object layer has extrusion (any interior layer of a solid +// cube), the band takes the ordinary process_layer() path and must be drawn immediately +// before that layer's perimeters, and exactly once: never dropped, never double-emitted. +TEST_CASE("Belt brim on an object layer precedes its perimeters, once", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + const std::string gc = gcode(print); + + // Ordering: the first thing extruded is brim, then perimeter. + const std::vector seq = role_sequence(gc, { "brim", "perimeter" }); + REQUIRE(seq.size() >= 2); + CHECK(seq[0] == "brim"); + CHECK(seq[1] == "perimeter"); + + // Exactly once: every band prints at its own layer Z, so the number of Z heights with + // brim equals the number of bands - not fewer, which a dropped band would give. (A + // double emission would print twice at one Z: the pass count below catches that for + // the bands that sit on layers with perimeters.) + const PrintObject &object = *print.objects().front(); + const int bands = nonempty_belt_brim_layers(object); + REQUIRE(bands > 0); + CHECK(role_layers(gc, "brim") == bands + belt_brim_apron_bands(object)); + CHECK(role_passes(gc, "brim") <= bands); +} + +// B - single extruder (filament id 1). Every band must survive the 1-based -> 0-based +// filament-id conversion the apron path performs: a wrong conversion drops all single-extruder +// bands, so the pass count would collapse. The expected count is derived from the sliced +// layers, not a ratio. +TEST_CASE("Belt brim on a single extruder emits every band once", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + const std::string gc = gcode(print); + + const PrintObject &object = *print.objects().front(); + const int expected = nonempty_belt_brim_layers(object) + belt_brim_apron_bands(object); + REQUIRE(expected > 0); + CHECK(role_layers(gc, "brim") == expected); + CHECK(belt_tools_for_role(gc, "brim") == std::set{ 0 }); // filament 1 -> tool 0 +} + +// Number of brim segments the belt brim generator produced for `object`: the lattice +// lines of every per-layer band plus the apron prologue. Each segment is written as one +// extruding move, so this is what a G-code count has to match. A pass count cannot see a +// band emitted twice back to back (two copies of the same band merge into one pass). +static long belt_brim_segments(const PrintObject &object) +{ + auto segments = [](const ExtrusionEntityCollection &fills) { + long n = 0; + for (const ExtrusionEntity *entity : fills.flatten().entities) + for (const Polyline &pl : entity->as_polylines()) + n += long(pl.size()) - 1; + return n; + }; + long n = 0; + for (const ExtrusionEntityCollection &band : object.belt_brim_by_layer()) + n += segments(band); + for (const BeltBrimBand &band : object.belt_brim_prologue()) + n += segments(band.fills); + return n; +} + +// Number of extruding moves in the G-code whose role is `role`. +static long role_segments(const std::string &gcode, const std::string &role) +{ + long n = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (line.extruding(self) && line.dist_XY(self) > EPSILON && line.comment().find(role) != std::string_view::npos) + ++ n; + }); + return n; +} + +TEST_CASE("Belt brim writes every generated segment exactly once", "[SkirtBrim][belt]") +{ + // The pass count above cannot tell one band from the same band twice in a row; the + // segment count can, so a brim band emitted twice back to back fails here. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "leading_brim_length", 6 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + const std::string gc = gcode(print); + + const long expected = belt_brim_segments(*print.objects().front()); + REQUIRE(expected > 100); + CHECK(role_segments(gc, "brim") == expected); +} + +// B - multi extruder (wall filament id 2). Every belt-brim line must print on the object's +// wall filament (index 2 -> tool 1), and the total number of passes must equal the +// single-extruder baseline: no per-filament doubling. +TEST_CASE("Belt brim on a multi-extruder object uses the wall filament, no doubling", "[SkirtBrim][belt]") +{ + // Single-extruder baseline built the same way (same nozzle/flow), so the band geometry - + // and thus the band count - is identical and only the filament assignment differs. + DynamicPrintConfig base = belt_brim_multifilament_config(1, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + const int baseline = role_passes(slice({ cube(20) }, base), "brim"); + REQUIRE(baseline > 0); + + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "outer_wall_filament_id", 2 }, + { "inner_wall_filament_id", 2 }, + }); + const std::string gc = slice({ cube(20) }, config); + + CHECK(belt_tools_for_role(gc, "brim") == std::set{ 1 }); // filament 2 -> tool 1 + CHECK(role_passes(gc, "brim") == baseline); +} + +// B - two objects offset ALONG the belt (Y, since the tilt is about X), each with its own +// wall filament. Each object's brim/apron must print on that object's filament AND before +// that object's own perimeters. The object is identified by its unique tool. +TEST_CASE("Belt brim of each object precedes its perimeters on its own filament", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + }); + + std::vector meshes; + meshes.emplace_back(cube(20)); + TriangleMesh second = cube(20); + second.translate(0.f, 40.f, 0.f); // offset along the belt so it lands well after the first + meshes.emplace_back(std::move(second)); + + const std::vector> overrides { + { { "outer_wall_filament_id", 1 }, { "inner_wall_filament_id", 1 } }, + { { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 } }, + }; + Print print; + Model model; + init_print(std::move(meshes), print, model, config, &overrides, /*arrange=*/false); + print.process(); + const std::string gc = gcode(print); + + // Both brims appear, each on its object's wall filament (1 -> T0, 2 -> T1). + CHECK(belt_tools_for_role(gc, "brim") == std::set{ 0, 1 }); + + const std::map brim_first = first_move_by_tool(gc, "brim"); + const std::map peri_first = first_move_by_tool(gc, "perimeter"); + for (int tool : { 0, 1 }) { + REQUIRE(brim_first.count(tool) == 1); + REQUIRE(peri_first.count(tool) == 1); + CHECK(brim_first.at(tool) < peri_first.at(tool)); + } +} + +// D - the belt-brim predicate must not fire on a request that produces no belt brim. +// leading_brim_length / extra_brim_width only feed the OUTER ring, so inner_only with zero +// brim_width yields nothing and must not claim the layers the prime tower / spiral vase need. +TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spiral vase", "[SkirtBrim][belt]") +{ + auto inner_leading = [](std::initializer_list extra) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "inner_only" }, + { "brim_width", 0 }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + }); + config.set_deserialize_strict(extra); + return config; + }; + auto init_inner_leading_with_prime_tower = [](Print &print, Model &model, double brim_width) { + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "inner_only" }, + { "brim_width", brim_width }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + { "enable_prime_tower", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + }; + + SECTION("prime tower is left alone") { + Print print; + Model model; + init_inner_leading_with_prime_tower(print, model, 0); + CHECK_FALSE(print.objects().front()->has_belt_brim()); + CHECK(print.validate().string.empty()); + } + SECTION("spiral vase is left alone") { + Print print; + Model model; + init_print({ cube(20) }, print, model, inner_leading({ { "spiral_mode", 1 } })); + CHECK_FALSE(print.objects().front()->has_belt_brim()); + CHECK(print.validate().string.empty()); + } + // enable_prime_tower stays on for any multi-filament project, but a belt printer never + // prints the classic tower, so the setting alone must not cost the print its brim. + SECTION("a real inner brim is accepted with the prime tower setting on") { + Print print; + Model model; + init_inner_leading_with_prime_tower(print, model, 4); + CHECK(print.objects().front()->has_belt_brim()); + CHECK(print.validate().string.empty()); + CHECK_FALSE(gcode(print).empty()); + } + // A purge tower object is accepted too: the purge plan moves every object, apron + // bands included, onto one layer grid. + SECTION("a brim is accepted next to a belt purge tower object") { + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "enable_belt_purge_tower", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + Print print; + Model model; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + REQUIRE(print.has_belt_purge_tower()); + CHECK(print.validate().string.empty()); + CHECK_FALSE(gcode(print).empty()); + } +} + +TEST_CASE("Belt brim spans many layers instead of one", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 5 }, + }); + const std::string gcode = slice({ cube(20) }, config); + // A plate-brim implementation would score 1 here. + CHECK(role_passes(gcode, "brim") > 10); +} + +TEST_CASE("Belt brim is absent when both widths are zero", "[SkirtBrim][belt]") +{ + // The "no effect when disabled" guard: brim_type Auto is the shipped default and + // reports has_brim() even at width 0, so this also pins the gate that keeps the + // flat plate brim from running on a tilted belt. + const char *brim_type = GENERATE("auto_brim", "outer_only", "no_brim"); + DYNAMIC_SECTION("brim_type " << brim_type) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", brim_type }, + { "brim_width", 0 }, + { "leading_brim_length", 0 }, + { "extra_brim_width", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + CHECK(role_passes(gcode, "brim") == 0); + } +} + +TEST_CASE("Leading brim length alone produces a belt brim", "[SkirtBrim][belt]") +{ + // Exercises the leading_brim_length-only enablement path and the downhill sweep. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 0 }, + { "leading_brim_length", 5 }, + { "brim_object_gap", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + CHECK(role_passes(gcode, "brim") > 0); +} + +TEST_CASE("Leading brim length reaches further ahead of the object", "[SkirtBrim][belt]") +{ + // Compared between two runs rather than against an absolute coordinate, so the + // assertion survives any change of origin or axis remap. + auto brim_extent = [](double extra) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 3 }, + { "leading_brim_length", extra }, + { "brim_object_gap", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + // The apron prints before the object reaches the belt, so it shows up as brim + // extrusion at the lowest machine Z of any brim move. + double min_z = std::numeric_limits::max(); + GCodeReader parser; + parser.parse_buffer(gcode, [&min_z](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (line.extruding(self) && line.comment().find("brim") != std::string_view::npos) + min_z = std::min(min_z, static_cast(self.z())); + }); + return min_z; + }; + const double without = brim_extent(0.); + const double with = brim_extent(10.); + REQUIRE(without < std::numeric_limits::max()); + REQUIRE(with < std::numeric_limits::max()); + CHECK(with < without); +} + +TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") +{ + // Auto / Mouse ear / Painted collapse to outer-only rather than crashing or + // silently producing nothing. + const char *brim_type = GENERATE("auto_brim", "brim_ears", "painted", "outer_only", + "inner_only", "outer_and_inner", "leading_edge_only", "no_brim"); + DYNAMIC_SECTION("brim_type " << brim_type) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", brim_type }, + { "brim_width", 5 }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + if (std::string(brim_type) == "no_brim") + CHECK(role_passes(gcode, "brim") == 0); + else if (std::string(brim_type) != "inner_only") + // A solid cube has no holes, so inner_only legitimately yields nothing. + CHECK(role_passes(gcode, "brim") > 0); + } +} + +// The leading-edge-only brim is the outer brim cut down to the part's first contact +// with the belt. The cut has to be taken at the first layer that touches the belt: the +// slicing frame starts at the belt below the footprint, so layers().front() is an empty +// lead-in layer whose contact lies ahead of the part, and a cut taken there left no brim +// at all. +TEST_CASE("Leading-edge-only brim is laid at the first contact and nowhere else", "[SkirtBrim][belt][Regression]") +{ + auto brim_gcode = [](const char *brim_type) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", brim_type }, + { "brim_width", 5 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + }); + return slice({ cube(20) }, config); + }; + const std::string leading = brim_gcode("leading_edge_only"); + const std::string outer = brim_gcode("outer_only"); + + const double brim_z = first_role_z(leading, "brim"); + const double peri_z = first_role_z(leading, "perimeter"); + REQUIRE(brim_z < std::numeric_limits::max()); + REQUIRE(peri_z < std::numeric_limits::max()); + // At the first contact: the brim starts no later than the part does... + CHECK(brim_z <= peri_z + EPSILON); + // ...and stops there, while the outer brim keeps following the footprint. + const int leading_layers = role_layers(leading, "brim"); + const int outer_layers = role_layers(outer, "brim"); + INFO("brim layers: leading-edge " << leading_layers << ", outer " << outer_layers); + CHECK(leading_layers > 0); + CHECK(leading_layers < outer_layers); +} + +// An overhang on the leading side is sliced before the part reaches the belt, so the +// first layer with geometry is the overhang's tip, above the belt. A leading-edge cut +// taken there lies ahead of the part: the brim shrank to a sliver well ahead of it, or +// vanished once the overhang reached further forward than the brim. The overhang does +// not touch the belt, so it must not change the brim at all. +TEST_CASE("Leading-edge-only brim ignores an overhang ahead of the part", "[SkirtBrim][belt][Regression]") +{ + const double fin_length = GENERATE(30., 40.); + CAPTURE(fin_length); + // A 20 mm cube, with or without a 2 mm thick fin leaving its top edge and reaching + // `fin` toward -Y, the end of the part that prints first. The fin overlaps the cube + // by 1 mm so the two shells merge instead of sharing a face. + auto brim_layers = [](double fin) { + indexed_triangle_set its = its_make_cube(20., 20., 20.); + if (fin > 0.) { + indexed_triangle_set fin_its = its_make_cube(20., fin + 1., 2.); + its_translate(fin_its, Vec3f(0.f, float(-fin), 18.f)); + its_merge(its, fin_its); + } + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "leading_edge_only" }, + { "brim_width", 5 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + }); + return role_layers(slice({ TriangleMesh(std::move(its)) }, config), "brim"); + }; + const int plain = brim_layers(0.); + const int with_fin = brim_layers(fin_length); + INFO("leading-edge brim layers: plain cube " << plain << ", with the fin " << with_fin); + REQUIRE(plain > 0); + // One layer of slack: the fin widens the part's footprint on the plate, which can + // move the layer grid by a fraction of a layer. + CHECK(std::abs(with_fin - plain) <= 1); +} + +TEST_CASE("An untilted belt printer gets no brim", "[SkirtBrim][belt]") +{ + // Belt brim needs a tilt to have a belt plane to lie on, and the flat plate brim + // cannot reach the G-code on any belt printer: it is emitted out of + // skirt_brim_groups(), which _make_skirt() builds, and that returns early for every + // belt printer. So an untilted belt printer gets nothing - unchanged by this + // feature. Making the flat brim work here would mean reopening the belt skirt gate, + // which is a separate change; Print::validate() warns instead. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "belt_slice_rotation", "none" }, + { "brim_type", "outer_only" }, + { "brim_width", 5 }, + }); + const std::string gcode = slice({ cube(20) }, config); + CHECK(role_passes(gcode, "brim") == 0); +} + +TEST_CASE("Belt brim does not resurrect the skirt", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 5 }, + { "skirt_loops", 2 }, + }); + const std::string gcode = slice({ cube(20) }, config); + CHECK(role_passes(gcode, "skirt") == 0); +} + +TEST_CASE("Belt brim lines all have the same width", "[SkirtBrim][belt]") +{ + // Each brim line's extrusion volume comes from its nozzle-to-belt clearance. Anchoring + // every line to a fixed fraction of its own band gives them all the same clearance, so + // they all come out the same width. The nominal-spacing lattice this replaced let each + // line land wherever it fell inside its band, so the clearance - and the width with it - + // varied by 2x, which showed up as visibly ragged brim. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 5 }, + { "brim_object_gap", 0 }, + }); + Print print; + init_and_process_print({ cube(20) }, print, config); + const PrintObject *obj = print.objects().front(); + + std::vector widths; + auto collect = [&widths](const ExtrusionEntityCollection &coll) { + for (const ExtrusionEntity *ee : coll.entities) + if (const auto *path = dynamic_cast(ee)) + widths.push_back(path->width); + }; + for (const ExtrusionEntityCollection &band : obj->belt_brim_by_layer()) + collect(band); + for (const BeltBrimBand &band : obj->belt_brim_prologue()) + collect(band.fills); + + REQUIRE(widths.size() > 10); + const float lo = *std::min_element(widths.begin(), widths.end()); + const float hi = *std::max_element(widths.begin(), widths.end()); + CHECK_THAT(hi, Catch::Matchers::WithinRel(lo, 1e-4f)); +} + +TEST_CASE("Belt apron survives another object printing at the same Z", "[SkirtBrim][belt]") +{ + // An apron band prints below its OWN object's first layer, but with two objects on the + // belt the second one is already printing at that print_z. The layer then has an + // object layer and takes the ordinary process_layer() path rather than the brim-only + // branch, so the band must be emitted from both or it is silently dropped. A + // single-object print cannot exercise this. + auto brim_passes = [](int object_count) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 3 }, + { "leading_brim_length", 8 }, + { "brim_object_gap", 0 }, + }); + std::vector meshes; + for (int i = 0; i < object_count; ++ i) { + TriangleMesh m = cube(20); + // Offset along the belt so the second object starts well after the first. + m.translate(0.f, float(40 * i), 0.f); + meshes.emplace_back(std::move(m)); + } + Print print; + Model model; + init_print(std::move(meshes), print, model, config); + print.process(); + return role_passes(gcode(print), "brim"); + }; + + const int one = brim_passes(1); + const int two = brim_passes(2); + REQUIRE(one > 0); + // Two identical objects should carry twice the brim. Merely asserting `two > one` + // would not be decisive: the FIRST object's apron survives the bug, because nothing + // else is printing that early, so only the second object's apron goes missing. + // Requiring close to 2x is what actually detects the dropped bands. + CHECK(two >= 1.8 * one); +} + +TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]") +{ + // Supports put extra layers into the same z stream as the apron bands, which is what + // the three-way merge in collect_layers_to_print() exists to handle: a band sharing a + // print_z with a support layer of the SAME object used to overwrite it in the + // print-wide merge. A smoke test - it cannot prove the collision occurred - but it + // does exercise the merge with all three streams populated. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "leading_brim_length", 6 }, + { "brim_object_gap", 0 }, + { "enable_support", 1 }, + }); + const std::string gc = slice({ TestMesh::overhang }, config); + REQUIRE(! gc.empty()); + CHECK(role_passes(gc, "brim") > 0); +} + +// With a 0.3 mm first layer at 45 degrees the brim band on the belt is wider than one bead, +// so its lines go on the nominal lattice instead of at a fixed fraction of the band. A +// lattice line can then land where the belt is almost at the band's print_z; it must be +// moved uphill to the same 0.75 fraction the single-line case uses, not laid scraping the +// belt with its flow clamped to half a layer. +TEST_CASE("Belt brim lattice lines keep their clearance above the belt", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "layer_height", 0.3 }, + { "initial_layer_print_height", 0.3 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + + // Heights of the brim extrusions, from the ;HEIGHT: tags inside ;TYPE:Brim sections. + std::vector brim_heights; + bool in_brim = false; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";TYPE:")) + in_brim = boost::starts_with(line, ";TYPE:Brim"); + else if (in_brim && boost::starts_with(line, ";HEIGHT:")) + brim_heights.push_back(std::stod(line.substr(8))); + } + REQUIRE(! brim_heights.empty()); + for (const double h : brim_heights) { + CHECK(h >= 0.75 * 0.3 - 1e-3); + CHECK(h <= 0.3 + 1e-3); + } +} + +// The brim prints in the object's outer wall filament even when every extrusion of the object +// is offered to purging (flush_into_objects): the tool ordering registers the brim filament +// itself, so the writer always knows it. +TEST_CASE("Belt brim slices when every object is a flush target", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "flush_into_objects", 1 }, + { "flush_into_infill", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + Print print; + Model model; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + REQUIRE(print.validate().string.empty()); + const std::string out = gcode(print); + CHECK(out.find(";TYPE:Brim") != std::string::npos); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index ce1185ac80..cf9a69e5d6 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable(${_TEST_NAME}_tests test_arrange.cpp test_assemble_list.cpp test_bambu_networking.cpp + test_belt_brim.cpp test_buildvolume.cpp test_calib.cpp test_clipper_offset.cpp diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index 7d3c2fb324..f4dad02808 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "libslic3r/Arrange.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/ClipperUtils.hpp" @@ -274,6 +275,101 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]") require_no_overlap(items); } +// A belt printer starts its parts at the leading end of the belt (best_object_pos 0.5, 0.05). +// Centring a pile on a point that close to the edge pushed everything longer than the room +// around it off the bed: four 90 mm parts on a 95 x 500 mm belt ended with one across the +// edge and one outside, with 290 mm of belt free behind them. The pile stops at the edge. +TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange][belt]") +{ + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(4, 90.); + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + params.is_belt = true; + params.belt_axis = 1; + params.belt_tilt_slope = 1.f; + + arrange(items, belt, params); + + coord_t lowest = std::numeric_limits::max(); + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); + CHECK(belt.contains(bb)); + lowest = std::min(lowest, bb.min.y()); + } + // Snapped to the edge it was aimed at, less the spacing margin, not re-centred. + CHECK(lowest < scaled(10.)); + require_no_overlap(items); +} + +// The clamp is a belt feature. Printers whose best_object_pos is off-centre (the A1 mini +// and the H2 family) keep their final alignment: the pile is centred on that point, even +// when that puts part of it outside the bed. +TEST_CASE("Arrange leaves the final alignment of a flat bed unclamped", "[Arrange]") +{ + const BoundingBox bed_ = bed(95, 500); + ArrangePolygons items = squares(4, 90.); + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + + arrange(items, bed_, params); + + BoundingBox pile; + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + pile.merge(ap.transformed_poly().contour.bounding_box()); + } + // Centred on the 5% mark of the bed's length, not pushed inside it. + CHECK_THAT(unscaled(pile.center().y()), Catch::Matchers::WithinAbs(0.05 * 500., 15.)); + CHECK(pile.min.y() < 0); + require_no_overlap(items); +} + +// On a belt the parts print in belt order, so two colours that alternate along the +// belt, or sit side by side, cost a filament change on every shared layer. Arrange +// keeps each colour together: no part shares belt length with a part of another +// colour, counting the tilted layers that run cot(angle) * height past its far edge, +// whichever end of the belt prints first. +TEST_CASE("Arrange groups the colours of a belt print along the belt", "[Arrange][belt]") +{ + const bool reversed = GENERATE(false, true); + CAPTURE(reversed); + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(6, 30., 20.); + for (size_t i = 0; i < items.size(); ++i) + items[i].extrude_ids = { int(i % 3) + 1 }; // three colours, two parts each + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + params.is_belt = true; + params.belt_axis = 1; + params.belt_reversed = reversed; + params.belt_tilt_slope = 1.f; // 45 degrees + + arrange(items, belt, params); + require_no_overlap(items); + + // Belt position in print order, so the same check serves both directions. + const coord_t dir = reversed ? -1 : 1; + auto start = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.max.y() : bb.min.y()); }; + auto end = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.min.y() : bb.max.y()) + scaled(ap.height * params.belt_tilt_slope); }; + + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + CHECK(belt.contains(ap.transformed_poly().contour.bounding_box())); + } + for (const ArrangePolygon &a : items) + for (const ArrangePolygon &b : items) { + if (a.extrude_ids == b.extrude_ids) + continue; + // The part printed later starts after the earlier one has finished. + const coord_t earlier_end = start(a) <= start(b) ? end(a) : end(b); + const coord_t later_start = std::max(start(a), start(b)); + INFO("colour " << a.extrude_ids.front() << " vs " << b.extrude_ids.front()); + CHECK(earlier_end <= later_start); + } +} + TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]") { // The only place sequential-print clearance is enforced. The arrange menu offers diff --git a/tests/libslic3r/test_belt_brim.cpp b/tests/libslic3r/test_belt_brim.cpp new file mode 100644 index 0000000000..f6d794f939 --- /dev/null +++ b/tests/libslic3r/test_belt_brim.cpp @@ -0,0 +1,435 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/BeltBrim.hpp" +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/libslic3r.h" + +using namespace Slic3r; + +// Pure-geometry tests for the belt brim. No Print, no slicing: everything here is +// a property of the tilted-belt mapping and the sweep/lattice helpers, which is +// exactly the part that has to be right before any G-code is worth looking at. + +static ExPolygon make_box(coord_t x0, coord_t y0, coord_t x1, coord_t y1) +{ + ExPolygon out; + out.contour.points = { Point(x0, y0), Point(x1, y0), Point(x1, y1), Point(x0, y1) }; + return out; +} + +static void add_hole(ExPolygon &ex, coord_t x0, coord_t y0, coord_t x1, coord_t y1) +{ + Polygon hole; + // Holes run clockwise, opposite the contour. + hole.points = { Point(x0, y0), Point(x0, y1), Point(x1, y1), Point(x1, y0) }; + ex.holes.emplace_back(std::move(hole)); +} + +SCENARIO("sweep_ex sweeps a box", "[BeltBrim]") { + const coord_t mm = scale_(1.); + GIVEN("a 10x10 mm box") { + const ExPolygons src { make_box(0, 0, 10 * mm, 10 * mm) }; + + WHEN("swept 5 mm along -Y") { + const ExPolygons out = sweep_ex(src, Point(0, -5 * mm)); + THEN("it becomes one 10x15 mm box") { + REQUIRE(out.size() == 1); + REQUIRE(out.front().holes.empty()); + const BoundingBox bb = get_extents(out); + CHECK(bb.min.y() == -5 * mm); + CHECK(bb.max.y() == 10 * mm); + CHECK(bb.min.x() == 0); + CHECK(bb.max.x() == 10 * mm); + CHECK_THAT(unscale(unscale(out.front().area())), + Catch::Matchers::WithinRel(150., 1e-6)); + } + } + + WHEN("swept by a zero vector") { + THEN("it is unchanged") { + const ExPolygons out = sweep_ex(src, Point(0, 0)); + REQUIRE(out.size() == 1); + CHECK(out.front().area() == src.front().area()); + } + } + + WHEN("swept diagonally") { + // For a convex P, area(P + [0,t]) == area(P) + |t| * width of P + // perpendicular to t. For a square swept along (1,1)/sqrt2 the + // perpendicular width is the diagonal, 10*sqrt2. + const ExPolygons out = sweep_ex(src, Point(3 * mm, 3 * mm)); + THEN("area grows by |t| times the perpendicular width") { + const double expected = 100. + std::sqrt(2. * 9.) * (10. * std::sqrt(2.)); + CHECK_THAT(unscale(unscale(out.front().area())), + Catch::Matchers::WithinRel(expected, 1e-6)); + } + } + } +} + +SCENARIO("sweep_ex closes holes narrower than the sweep", "[BeltBrim]") { + const coord_t mm = scale_(1.); + // This is the assertion that catches the two likeliest implementation bugs: + // omitting hole boundaries from the parallelogram set, or using the wrong + // Clipper fill rule. Note hole survival depends on the hole's extent ALONG + // the sweep direction, not on its narrowest dimension. + GIVEN("a 20x20 mm box with a hole 2 mm tall in the sweep direction") { + ExPolygon ex = make_box(0, 0, 20 * mm, 20 * mm); + add_hole(ex, 5 * mm, 9 * mm, 15 * mm, 11 * mm); + WHEN("swept 5 mm along -Y") { + const ExPolygons out = sweep_ex(ExPolygons{ ex }, Point(0, -5 * mm)); + THEN("the hole is filled in") { + REQUIRE(out.size() == 1); + CHECK(out.front().holes.empty()); + } + } + } + GIVEN("a 20x20 mm box with a hole 12 mm tall in the sweep direction") { + ExPolygon ex = make_box(0, 0, 20 * mm, 20 * mm); + add_hole(ex, 5 * mm, 4 * mm, 15 * mm, 16 * mm); + WHEN("swept 5 mm along -Y") { + const ExPolygons out = sweep_ex(ExPolygons{ ex }, Point(0, -5 * mm)); + THEN("the hole survives, shrunk by the sweep") { + REQUIRE(out.size() == 1); + REQUIRE(out.front().holes.size() == 1); + const BoundingBox hb = get_extents(out.front().holes.front()); + CHECK(hb.max.y() - hb.min.y() == 7 * mm); + } + } + } + GIVEN("a box whose edges are parallel to the sweep vector") { + // Degenerate parallelograms; Clipper must simply discard them. + const ExPolygons src { make_box(0, 0, 10 * mm, 10 * mm) }; + WHEN("swept along +X") { + const ExPolygons out = sweep_ex(src, Point(4 * mm, 0)); + THEN("the result is the expected rectangle") { + REQUIRE(out.size() == 1); + const BoundingBox bb = get_extents(out); + CHECK(bb.min.x() == 0); + CHECK(bb.max.x() == 14 * mm); + } + } + } + GIVEN("a reversed (clockwise) contour") { + ExPolygon ex = make_box(0, 0, 10 * mm, 10 * mm); + ex.contour.reverse(); + WHEN("swept") { + const ExPolygons out = sweep_ex(ExPolygons{ ex }, Point(0, -5 * mm)); + THEN("material is still produced") { + REQUIRE(! out.empty()); + CHECK(get_extents(out).min.y() == -5 * mm); + } + } + } +} + +SCENARIO("Belt flattening round-trips and rescales only the shear axis", "[BeltBrim]") { + const coord_t mm = scale_(1.); + const double shear = GENERATE(0.1, 0.5, 1.0, 3.0); + const int from_axis = GENERATE(0, 1); + DYNAMIC_SECTION("shear " << shear << " axis " << from_axis) { + const BeltBrimFrame frame { shear, from_axis }; + + // An L shape with a hole, so contours and holes are both exercised. + ExPolygon ex; + ex.contour.points = { Point(0, 0), Point(20 * mm, 0), Point(20 * mm, 6 * mm), + Point(6 * mm, 6 * mm), Point(6 * mm, 20 * mm), Point(0, 20 * mm) }; + add_hole(ex, 2 * mm, 2 * mm, 4 * mm, 4 * mm); + const ExPolygons src { ex }; + + const ExPolygons round_tripped = belt_unflatten(belt_flatten(src, frame), frame); + REQUIRE(round_tripped.size() == src.size()); + REQUIRE(round_tripped.front().holes.size() == src.front().holes.size()); + for (size_t c = 0; c < src.front().num_contours(); ++ c) { + const Points &a = src.front().contour_or_hole(c).points; + const Points &b = round_tripped.front().contour_or_hole(c).points; + REQUIRE(a.size() == b.size()); + for (size_t i = 0; i < a.size(); ++ i) { + // Rounding, not truncation, so the round trip stays within a + // couple of coordinate units. + CHECK(std::abs(a[i].x() - b[i].x()) <= 2); + CHECK(std::abs(a[i].y() - b[i].y()) <= 2); + // The axis that is not stretched must come back untouched. + if (from_axis == 0) + CHECK(a[i].y() == b[i].y()); + else + CHECK(a[i].x() == b[i].x()); + } + } + } +} + +SCENARIO("Flattening makes shear-axis distances true on-belt distances", "[BeltBrim]") { + // The property the whole design rests on: an in-plane distance w projects to + // dw = w * cos(tilt) along the shear axis, so stretching that axis by + // 1/cos(tilt) makes ordinary Clipper offsets measure real on-belt distance. + const coord_t mm = scale_(1.); + const double shear = GENERATE(0.1, 0.5, 1.0, 3.0); + const int from_axis = GENERATE(0, 1); + DYNAMIC_SECTION("shear " << shear << " axis " << from_axis) { + const BeltBrimFrame frame { shear, from_axis }; + const double stretch = std::sqrt(1. + shear * shear); + CHECK_THAT(frame.u_stretch(), Catch::Matchers::WithinRel(stretch, 1e-12)); + CHECK_THAT(frame.cos_tilt() * frame.u_stretch(), Catch::Matchers::WithinRel(1., 1e-12)); + + // Two points 1 mm apart along the shear axis are stretch mm apart once + // flattened. + ExPolygon seg = make_box(0, 0, 1 * mm, 1 * mm); + const BoundingBox flat = get_extents(belt_flatten(ExPolygons{ seg }, frame)); + const coord_t span_u = from_axis == 0 ? flat.max.x() - flat.min.x() + : flat.max.y() - flat.min.y(); + CHECK_THAT(unscale(span_u), Catch::Matchers::WithinRel(stretch, 1e-5)); + } +} + +SCENARIO("belt_brim_line_positions walks an exact lattice", "[BeltBrim]") { + const coord_t pitch = 420; // arbitrary units; the point is exactness + const coord_t anchor = 1000; + + GIVEN("a band narrower than the pitch containing no lattice point") { + // Between anchor+0 and anchor+pitch, pick a window that misses both. + const std::vector us = belt_brim_line_positions(anchor + 100, anchor + 300, pitch, anchor); + THEN("nothing is emitted") { CHECK(us.empty()); } + } + GIVEN("a band containing exactly one lattice point") { + const std::vector us = belt_brim_line_positions(anchor - 10, anchor + 10, pitch, anchor); + THEN("that point is emitted") { + REQUIRE(us.size() == 1); + CHECK(us.front() == anchor); + } + } + GIVEN("a wide band, as at a shallow belt tilt") { + const std::vector us = belt_brim_line_positions(anchor, anchor + 5 * pitch, pitch, anchor); + THEN("several lines are emitted at exactly the pitch") { + REQUIRE(us.size() == 5); + for (size_t i = 1; i < us.size(); ++ i) + CHECK(us[i] - us[i - 1] == pitch); + } + } + GIVEN("two adjacent bands sharing a boundary") { + // Half-open ownership: a lattice point landing on the shared bound belongs + // to the upper band only, so no line is duplicated or dropped. + const coord_t bound = anchor + 2 * pitch; + const std::vector lower = belt_brim_line_positions(anchor, bound, pitch, anchor); + const std::vector upper = belt_brim_line_positions(bound, bound + 2 * pitch, pitch, anchor); + THEN("the boundary point appears exactly once, in the upper band") { + CHECK(std::count(lower.begin(), lower.end(), bound) == 0); + CHECK(std::count(upper.begin(), upper.end(), bound) == 1); + CHECK(lower.size() == 2); + CHECK(upper.size() == 2); + } + } + GIVEN("a lattice anchored below zero") { + THEN("negative lattice points are handled") { + const std::vector us = belt_brim_line_positions(-3 * pitch, -pitch, pitch, 0); + REQUIRE(us.size() == 2); + CHECK(us.front() == -3 * pitch); + CHECK(us.back() == -2 * pitch); + } + } + GIVEN("a degenerate pitch or band") { + THEN("nothing is emitted rather than looping forever") { + CHECK(belt_brim_line_positions(0, 1000, 0, 0).empty()); + CHECK(belt_brim_line_positions(1000, 1000, pitch, 0).empty()); + CHECK(belt_brim_line_positions(1000, 500, pitch, 0).empty()); + } + } +} + +SCENARIO("belt_brim_region reduces to the plate brim without an apron", "[BeltBrim]") { + const coord_t mm = scale_(1.); + const BeltBrimFrame frame { 1.0, 1 }; + const ExPolygons footprint { make_box(0, 0, 20 * mm, 20 * mm) }; + const coord_t width = 3 * mm; + const coord_t gap = 1 * mm; + + GIVEN("outer brim, no apron") { + const ExPolygons region = belt_brim_region(footprint, true, false, width, gap, 0, 0, frame); + THEN("it matches the plate brim ring built from the same offsets") { + const ExPolygons inner = offset_ex(Polygons{ footprint.front().contour }, float(gap), jtRound, SCALED_RESOLUTION); + const ExPolygons outer = offset_ex(inner, float(width), jtRound, SCALED_RESOLUTION); + const ExPolygons expect = diff_ex(outer, inner); + // Not exact: the region is offset from the CLOSED footprint, and closing a + // single convex island is a geometric no-op but still round-trips every + // vertex through a dilate/erode, which perturbs the area in the 8th + // significant figure. + CHECK_THAT(unscale(unscale(area(region))), + Catch::Matchers::WithinRel(unscale(unscale(area(expect))), 1e-6)); + } + } + GIVEN("no outer and no inner brim") { + THEN("the region is empty") { + CHECK(belt_brim_region(footprint, false, false, width, gap, 5 * mm, 0, frame).empty()); + } + } + GIVEN("an apron but no brim width") { + const ExPolygons region = belt_brim_region(footprint, true, false, 0, 0, 5 * mm, 0, frame); + THEN("brim appears only downhill of the footprint") { + REQUIRE(! region.empty()); + const BoundingBox rb = get_extents(region); + // shear > 0 means downhill is -u, and from_axis 1 means u is Y. + CHECK(rb.min.y() < 0); + CHECK(rb.max.y() <= 0 + 2); // nothing above the footprint's own base + } + } +} + +SCENARIO("belt_brim_region builds an inner ring inside a hole", "[BeltBrim]") { + // Holed prisms (a washer) are the only footprints an inner brim has anything to grab. + // The inner path offsets the hole boundary inward and keeps the ring between the two + // offsets, clipped back inside the hole - it must be non-empty and live in the hole, + // never spill out onto the plate. No apron is applied to the inner ring. + const coord_t mm = scale_(1.); + const BeltBrimFrame frame { 1.0, 1 }; + const coord_t width = 3 * mm; + const coord_t gap = 1 * mm; + + GIVEN("a 40x40 mm washer with a 20 mm square hole") { + ExPolygon washer = make_box(0, 0, 40 * mm, 40 * mm); + add_hole(washer, 10 * mm, 10 * mm, 30 * mm, 30 * mm); + const ExPolygons footprint { washer }; + const BoundingBox hole_bb = get_extents(washer.holes.front()); + + WHEN("an inner-only brim is requested") { + const ExPolygons region = belt_brim_region(footprint, false, true, width, gap, 0, 0, frame); + THEN("a non-empty ring is produced strictly inside the hole") { + REQUIRE(! region.empty()); + CHECK(area(region) > 0); + const BoundingBox rb = get_extents(region); + CHECK(rb.min.x() >= hole_bb.min.x()); + CHECK(rb.min.y() >= hole_bb.min.y()); + CHECK(rb.max.x() <= hole_bb.max.x()); + CHECK(rb.max.y() <= hole_bb.max.y()); + } + } + WHEN("no inner brim is requested") { + THEN("the hole contributes nothing") { + CHECK(belt_brim_region(footprint, false, false, width, gap, 0, 0, frame).empty()); + } + } + } +} + +SCENARIO("Leading-edge-only retains the downhill half of the brim region", "[BeltBrim]") { + // The production clip, belt_brim_clip_leading_edge(), keeps what lies at or downhill + // of the first-contact cut. downhill_sign() pins the convention for both tilt signs: + // low_side = shear > 0, i.e. downhill is -u. + const coord_t mm = scale_(1.); + const double shear = GENERATE(1.0, -1.0); + DYNAMIC_SECTION("shear " << shear) { + const BeltBrimFrame frame { shear, 1 }; // from_axis 1 => u is Y + CHECK((frame.downhill_sign() < 0) == (frame.shear > 0.)); + + const ExPolygons region { make_box(0, 0, 20 * mm, 20 * mm) }; // straddles the cut + const coordf_t u_cut = 8.; // mm + + const ExPolygons kept = belt_brim_clip_leading_edge(region, frame, u_cut); + + REQUIRE(! kept.empty()); + const BoundingBox kb = get_extents(kept); + if (frame.shear > 0.) { + // downhill is -u: nothing above the cut survives. + CHECK(kb.max.y() <= 8 * mm + 2); + CHECK(kb.min.y() < 8 * mm); + } else { + // downhill is +u: nothing below the cut survives. + CHECK(kb.min.y() >= 8 * mm - 2); + CHECK(kb.max.y() > 8 * mm); + } + // Half of the box is kept either way, and the full width across the belt. + CHECK_THAT(area(kept), Catch::Matchers::WithinRel(area(region) * (frame.shear > 0. ? 8. / 20. : 12. / 20.), 0.01)); + CHECK(kb.min.x() == 0); + CHECK(kb.max.x() == 20 * mm); + } + WHEN("the cut lies beyond the region") { + const BeltBrimFrame frame { 1.0, 1 }; + const ExPolygons region { make_box(0, 0, 20 * mm, 20 * mm) }; + THEN("a cut past the uphill end keeps everything") { + CHECK_THAT(area(belt_brim_clip_leading_edge(region, frame, 30.)), Catch::Matchers::WithinRel(area(region), 0.001)); + } + THEN("a cut before the downhill end keeps nothing") { + CHECK(belt_brim_clip_leading_edge(region, frame, -5.).empty()); + } + THEN("an empty region stays empty") { + CHECK(belt_brim_clip_leading_edge(ExPolygons{}, frame, 8.).empty()); + } + } +} + +SCENARIO("The apron follows the sign of the shear", "[BeltBrim]") { + // Guards the one sign convention that is easiest to get backwards: which way + // is downhill, i.e. which way the belt carries the part. + const coord_t mm = scale_(1.); + const ExPolygons footprint { make_box(0, 0, 20 * mm, 20 * mm) }; + + const ExPolygons pos = belt_brim_region(footprint, true, false, 0, 0, 5 * mm, 0, BeltBrimFrame{ 1.0, 1 }); + const ExPolygons neg = belt_brim_region(footprint, true, false, 0, 0, 5 * mm, 0, BeltBrimFrame{ -1.0, 1 }); + REQUIRE(! pos.empty()); + REQUIRE(! neg.empty()); + CHECK(get_extents(pos).min.y() < 0); + CHECK(get_extents(neg).max.y() > 20 * mm); +} + +SCENARIO("Outer belt brim does not fill the space between contact islands", "[BeltBrim]") { + // A belt contact patch is often a narrow, broken-up strip. Offsetting each island + // outwards by brim_width merges the rings of any two islands closer than + // 2 x brim_width and fills the space between them - and on a belt that space is + // UNDERNEATH the part, which is not what "outer brim only" means. Closing the + // footprint before the outward offset is what prevents it. + const coord_t mm = scale_(1.); + const BeltBrimFrame frame { 1.0, 1 }; + const coord_t width = 3 * mm; + + GIVEN("two contact islands 4 mm apart, closer than 2 x brim width") { + const ExPolygons footprint { + make_box(0, 0, 10 * mm, 10 * mm), + make_box(14 * mm, 0, 24 * mm, 10 * mm), + }; + const ExPolygons region = belt_brim_region(footprint, true, false, width, 0, 0, 0, frame); + THEN("no brim is placed in the gap between them") { + REQUIRE(! region.empty()); + // Midpoint of the gap, and a point just inside either edge of it. + for (const coord_t x : { 12 * mm, coord_t(10.5 * mm), coord_t(13.5 * mm) }) { + const Point probe(x, 5 * mm); + bool covered = false; + for (const ExPolygon &ex : region) + if (ex.contains(probe)) { covered = true; break; } + CHECK(! covered); + } + } + THEN("brim is still placed outside the pair") { + bool outside_covered = false; + const Point probe(-1 * mm, 5 * mm); // 1 mm left of the left island + for (const ExPolygon &ex : region) + if (ex.contains(probe)) { outside_covered = true; break; } + CHECK(outside_covered); + } + } + + GIVEN("an apron on a fragmented footprint") { + const ExPolygons footprint { + make_box(0, 0, 10 * mm, 10 * mm), + make_box(14 * mm, 0, 24 * mm, 10 * mm), + }; + // Closing fills concavities only, so an outward protrusion such as the apron must + // survive it untouched. + const ExPolygons region = belt_brim_region(footprint, true, false, width, 0, 5 * mm, 0, frame); + THEN("the apron still reaches downhill") { + REQUIRE(! region.empty()); + CHECK(get_extents(region).min.y() <= -5 * mm); + } + } +} diff --git a/tests/libslic3r/test_filament_mixer.cpp b/tests/libslic3r/test_filament_mixer.cpp index f43a8e7588..7c01b26d87 100644 --- a/tests/libslic3r/test_filament_mixer.cpp +++ b/tests/libslic3r/test_filament_mixer.cpp @@ -65,6 +65,50 @@ TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", " } } +TEST_CASE("belt purge tower island count ignores virtual mixed slots", "[FilamentMixer][belt]") +{ + // Regression for the "extra purge tower" on a belt printer with a mixed + // filament (MCTEST5). The belt purge prism is sized as + // n_islands = used_filaments.size() - 1 + // and GUI::ensure_belt_purge_tower() collected those filaments straight off + // the model objects' extruder assignments. A mixed slot is VIRTUAL -- no + // nozzle carries it, and ToolOrdering::resolve_mixed_filaments() replaces it + // with its components before any G-code is emitted -- so counting it as a + // filament of its own provisions one island that can never be reached. + // + // MCTEST5: five cubes on extruders 1..5, where filament 5 is a 50/50 blend of + // filaments 2 and 4. The G-code uses only T0..T3 and reports + // "filament used [g] = 53.35, 141.11, 40.84, 107.23, 0.00" -- filament 5 + // consumes nothing, exactly as a virtual slot should. + const std::vector is_mixed = {0, 0, 0, 0, 1}; + const std::vector comp_strs = {"", "", "", "", "2,4"}; + + // The set the sizer used to see: slots 0..4 (filaments 1..5). + const std::vector assigned = {0, 1, 2, 3, 4}; + const auto physical = expand_mixed_filaments(assigned, is_mixed, comp_strs); + + // Slot 4 dissolves into 1 and 3, which are already present. + REQUIRE(physical == std::vector({0, 1, 2, 3})); + + // Four physical filaments => three transitions => three islands, not four. + REQUIRE(int(physical.size()) - 1 == 3); + REQUIRE(int(assigned.size()) - 1 == 4); // what it produced before the fix + + SECTION("A mixed slot whose components are otherwise unused still counts them") { + // Only the mixed slot is assigned: it must still yield its two components, + // i.e. one island, rather than collapsing to zero. + const auto only_mixed = expand_mixed_filaments({4}, is_mixed, comp_strs); + REQUIRE(only_mixed == std::vector({1, 3})); + REQUIRE(int(only_mixed.size()) - 1 == 1); + } + + SECTION("No mixed filaments anywhere leaves the set untouched") { + const std::vector none_mixed = {0, 0, 0, 0, 0}; + REQUIRE_FALSE(has_any_mixed_filament(none_mixed)); + REQUIRE(expand_mixed_filaments(assigned, none_mixed, {"", "", "", "", ""}) == assigned); + } +} + TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]") { const std::vector is_mixed = {0, 0, 1};