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 2f9eeefd34..ba1ddc14f4 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,9 +1,13 @@ { "name": "Custom Printer", - "version": "02.04.00.00", + "version": "02.04.00.03", "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" @@ -262,18 +266,38 @@ "name": "MyKlipper 0.8 nozzle", "sub_path": "machine/MyKlipper 0.8 nozzle.json" }, + { + "name": "fdm_belt_common", + "sub_path": "machine/fdm_belt_common.json" + }, { "name": "fdm_toolchanger_common", "sub_path": "machine/fdm_toolchanger_common.json" }, - { - "name": "MyRepetier 0.4 nozzle", - "sub_path": "machine/MyRepetier 0.4 nozzle.json" - }, { "name": "MyRRF 0.4 nozzle", "sub_path": "machine/MyRRF 0.4 nozzle.json" }, + { + "name": "MyBeltPrinter 0.2 nozzle", + "sub_path": "machine/MyBeltPrinter 0.2 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.4 nozzle", + "sub_path": "machine/MyBeltPrinter 0.4 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.6 nozzle", + "sub_path": "machine/MyBeltPrinter 0.6 nozzle.json" + }, + { + "name": "MyBeltPrinter 0.8 nozzle", + "sub_path": "machine/MyBeltPrinter 0.8 nozzle.json" + }, + { + "name": "MyRepetier 0.4 nozzle", + "sub_path": "machine/MyRepetier 0.4 nozzle.json" + }, { "name": "MyToolChanger 0.2 nozzle", "sub_path": "machine/MyToolChanger 0.2 nozzle.json" 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..5e7655fbe1 --- /dev/null +++ b/resources/profiles/Custom/machine/MyBeltPrinter 0.2 nozzle.json @@ -0,0 +1,26 @@ +{ + "type": "machine", + "name": "MyBeltPrinter 0.2 nozzle", + "inherits": "fdm_belt_common", + "from": "system", + "setting_id": "GM_BELT_001", + "instantiation": "true", + "printer_model": "Generic Belt Printer", + "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..73be69ba61 --- /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": "GM_BELT_002", + "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..49646db06d --- /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": "GM_BELT_003", + "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..412291754e --- /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": "GM_BELT_004", + "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..b010742d78 --- /dev/null +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -0,0 +1,99 @@ +{ + "type": "machine", + "name": "fdm_belt_common", + "inherits": "fdm_klipper_common", + "from": "system", + "instantiation": "false", + "gcode_flavor": "klipper", + "single_extruder_multi_material": "0", + "default_filament_profile": [ + "Generic PLA @System" + ], + "default_print_profile": "0.20mm Standard @System", + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "deretraction_speed": [ + "30" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "long_retractions_when_cut": [ + "0" + ], + "nozzle_diameter": [ + "0.4" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_length_toolchange": [ + "2" + ], + "retract_lift_above": [ + "0" + ], + "retract_lift_below": [ + "0" + ], + "retract_lift_enforce": [ + "All Surfaces" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_distances_when_cut": [ + "18" + ], + "retraction_length": [ + "0.8" + ], + "retraction_minimum_travel": [ + "1" + ], + "retraction_speed": [ + "30" + ], + "travel_slope": [ + "3" + ], + "wipe": [ + "1" + ], + "wipe_distance": [ + "1" + ], + "z_hop": [ + "0.4" + ], + "z_hop_types": [ + "Normal Lift" + ], + "gcode_remap_x": "rev_x", + "gcode_remap_y": "pos_z", + "gcode_remap_z": "pos_y", + "printer_extruder_id": [ + "1" + ], + "belt_printer": "1", + "belt_slice_rotation": "x", + "belt_slice_rotation_angle": "45", + "belt_slice_rotation_global": "1", + "build_plate_tilt_x": "45", + "purge_in_prime_tower": "0", + "scan_first_layer": "0", + "auxiliary_fan": "0" +} diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json new file mode 100644 index 0000000000..a021e7c164 --- /dev/null +++ b/resources/profiles/IdeaFormer.json @@ -0,0 +1,54 @@ +{ + "name": "IdeaFormer", + "version": "02.00.00.02", + "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..c51b20abbc --- /dev/null +++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json @@ -0,0 +1,112 @@ +{ + "type": "filament", + "name": "Generic PETG @IdeaFormer IR3 V2", + "inherits": "Generic PETG @System", + "from": "system", + "instantiation": "true", + "compatible_printers": [ + "IdeaFormer IR3 V2 0.4 nozzle" + ], + "filament_type": [ + "PETG" + ], + "filament_vendor": [ + "Generic" + ], + "filament_settings_id": [ + "Generic PETG @IdeaFormer IR3 V2" + ], + "filament_diameter": [ + "1.75" + ], + "filament_density": [ + "1.27" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_cost": [ + "25" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "nozzle_temperature": [ + "240" + ], + "nozzle_temperature_initial_layer": [ + "245" + ], + "nozzle_temperature_range_low": [ + "220" + ], + "nozzle_temperature_range_high": [ + "260" + ], + "temperature_vitrification": [ + "70" + ], + "hot_plate_temp": [ + "80" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "fan_min_speed": [ + "40" + ], + "fan_max_speed": [ + "60" + ], + "overhang_fan_threshold": [ + "25%" + ], + "overhang_fan_speed": [ + "80" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "full_fan_speed_layer": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "slow_down_layer_time": [ + "4" + ], + "fan_cooling_layer_time": [ + "100" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "filament_retraction_length": [ + "2" + ], + "filament_retraction_speed": [ + "40" + ], + "filament_deretraction_speed": [ + "40" + ], + "filament_z_hop": [ + "0.4" + ], + "filament_start_gcode": [ + "; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C" + ] +} 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..c86ba651da --- /dev/null +++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json @@ -0,0 +1,112 @@ +{ + "type": "filament", + "name": "Generic PLA @IdeaFormer IR3 V2", + "inherits": "Generic PLA @System", + "from": "system", + "instantiation": "true", + "compatible_printers": [ + "IdeaFormer IR3 V2 0.4 nozzle" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Generic" + ], + "filament_settings_id": [ + "Generic PLA @IdeaFormer IR3 V2" + ], + "filament_diameter": [ + "1.75" + ], + "filament_density": [ + "1.24" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_cost": [ + "20" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "nozzle_temperature": [ + "215" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "temperature_vitrification": [ + "45" + ], + "hot_plate_temp": [ + "75" + ], + "hot_plate_temp_initial_layer": [ + "75" + ], + "cool_plate_temp": [ + "75" + ], + "cool_plate_temp_initial_layer": [ + "75" + ], + "textured_plate_temp": [ + "75" + ], + "textured_plate_temp_initial_layer": [ + "75" + ], + "fan_min_speed": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "overhang_fan_speed": [ + "100" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "full_fan_speed_layer": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "slow_down_layer_time": [ + "4" + ], + "fan_cooling_layer_time": [ + "100" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "filament_retraction_length": [ + "1.5" + ], + "filament_retraction_speed": [ + "35" + ], + "filament_deretraction_speed": [ + "30" + ], + "filament_z_hop": [ + "0.4" + ], + "filament_start_gcode": [ + "; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C" + ] +} 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..d34131842a --- /dev/null +++ b/resources/profiles/IdeaFormer/filament/eSUN PLA @IdeaFormer IR3 V2.json @@ -0,0 +1,34 @@ +{ + "type": "filament", + "name": "eSUN PLA @IdeaFormer IR3 V2", + "inherits": "Generic PLA @IdeaFormer IR3 V2", + "from": "system", + "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..b6f128ca86 --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -0,0 +1,94 @@ +{ + "type": "machine", + "name": "IdeaFormer IR3 V2 0.4 nozzle", + "inherits": "fdm_belt_common", + "from": "system", + "setting_id": "GMIF001", + "instantiation": "true", + "printer_model": "IdeaFormer IR3 V2", + "printer_variant": "0.4", + "nozzle_diameter": [ + "0.4" + ], + "printable_area": [ + "0x0", + "250x0", + "250x2000", + "0x2000" + ], + "printable_height": "250", + "belt_printer_infinite_y": "1", + "thumbnails": [ + "48x48/PNG", + "300x300/PNG" + ], + "default_filament_profile": [ + "Generic PLA @IdeaFormer IR3 V2" + ], + "default_print_profile": "0.20mm Standard @IdeaFormer IR3 V2", + "use_relative_e_distances": "1", + "machine_max_acceleration_e": [ + "5000" + ], + "machine_max_acceleration_extruding": [ + "5000" + ], + "machine_max_acceleration_retracting": [ + "1000" + ], + "machine_max_acceleration_travel": [ + "9000" + ], + "machine_max_acceleration_x": [ + "5000" + ], + "machine_max_acceleration_y": [ + "5000" + ], + "machine_max_acceleration_z": [ + "100" + ], + "machine_max_jerk_e": [ + "2.5" + ], + "machine_max_jerk_x": [ + "10" + ], + "machine_max_jerk_y": [ + "10" + ], + "machine_max_jerk_z": [ + "0.4" + ], + "machine_max_speed_e": [ + "60" + ], + "machine_max_speed_x": [ + "500" + ], + "machine_max_speed_y": [ + "500" + ], + "machine_max_speed_z": [ + "20" + ], + "retraction_length": [ + "2" + ], + "retraction_speed": [ + "40" + ], + "deretraction_speed": [ + "40" + ], + "z_hop": [ + "0.4" + ], + "retract_lift_below": [ + "300" + ], + "machine_start_gcode": "; === IdeaFormer IR3 V2 Belt Printer Start ===\n; Axes: X=lateral, Y=gantry height (probe), Z=belt\nG90 ; absolute positioning\nM82 ; absolute extruder\nG21 ; millimeters\nG28 ; home all axes\nG1 Y20 F500 ; lift nozzle 20mm from belt\n; Bed + hotend temps come from the active filament profile. Belt PLA requires 75 C bed — use Generic/eSun PLA @IdeaFormer IR3 V2 filament presets to get it automatically.\nM140 S[hot_plate_temp_initial_layer] ; set bed temp\nM104 S[nozzle_temperature_initial_layer] ; hotend temp\nM109 S[nozzle_temperature_initial_layer] ; wait hotend\nM190 S[hot_plate_temp_initial_layer] ; wait bed\n; --- Purge blob ---\nG92 E0 ; zero extruder\nG1 Y.1 ; nozzle 0.1mm above belt\nG1 E15 F1000 ; purge 15mm blob\nG1 Z20 E25 F800 ; belt advance 20mm + extrude\nG1 E23 ; retract 2mm\nG28 Y ; re-probe belt surface\nG1 E25 ; de-retract\n; --- Prime lines (full 250mm bed width) ---\nFMS_on ; filament motion sensor\nG1 X250 E50 F2000 ; prime line 1\nG92 Z0 ; reset belt origin\nG1 Z.4 ; belt advance 0.4mm\nG1 X0 E75 ; prime line 2\nG1 F1000 ; default feedrate\nG92 E0 Z0 ; zero extruder + belt = print origin\n", + "machine_end_gcode": "; === IdeaFormer IR3 V2 Belt Printer End ===\nM400 ; wait for moves to finish\nM104 S0 ; heater off\nM140 S0 ; bed off\nG92 E0 ; zero extruder\nG1 E-5 F300 ; retract 5mm\nG4 P5000 ; wait for ooze\nG91 ; relative mode - keep every end move relative on a belt\nG1 Y20 F1000 ; raise gantry 20mm for clearance over the part\nG1 Z676 F3000 ; advance belt one full machine-depth to eject the part and clean the belt\nG90 ; back to absolute\nG28 X ; home X only - NEVER 'G28' all: that homes Z/belt and reverses the whole print back into the gantry\nFMS_off ; filament motion sensor off\nBED_MESH_CLEAR\nM84 ; disable motors\n", + "machine_pause_gcode": "PAUSE", + "layer_change_gcode": "G92 E0 ; belt: reset extruder at layer change (relative E)" +} 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..b010742d78 --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -0,0 +1,99 @@ +{ + "type": "machine", + "name": "fdm_belt_common", + "inherits": "fdm_klipper_common", + "from": "system", + "instantiation": "false", + "gcode_flavor": "klipper", + "single_extruder_multi_material": "0", + "default_filament_profile": [ + "Generic PLA @System" + ], + "default_print_profile": "0.20mm Standard @System", + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "deretraction_speed": [ + "30" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "long_retractions_when_cut": [ + "0" + ], + "nozzle_diameter": [ + "0.4" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_length_toolchange": [ + "2" + ], + "retract_lift_above": [ + "0" + ], + "retract_lift_below": [ + "0" + ], + "retract_lift_enforce": [ + "All Surfaces" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_distances_when_cut": [ + "18" + ], + "retraction_length": [ + "0.8" + ], + "retraction_minimum_travel": [ + "1" + ], + "retraction_speed": [ + "30" + ], + "travel_slope": [ + "3" + ], + "wipe": [ + "1" + ], + "wipe_distance": [ + "1" + ], + "z_hop": [ + "0.4" + ], + "z_hop_types": [ + "Normal Lift" + ], + "gcode_remap_x": "rev_x", + "gcode_remap_y": "pos_z", + "gcode_remap_z": "pos_y", + "printer_extruder_id": [ + "1" + ], + "belt_printer": "1", + "belt_slice_rotation": "x", + "belt_slice_rotation_angle": "45", + "belt_slice_rotation_global": "1", + "build_plate_tilt_x": "45", + "purge_in_prime_tower": "0", + "scan_first_layer": "0", + "auxiliary_fan": "0" +} 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..36f3fe13c6 --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/fdm_klipper_common.json @@ -0,0 +1,141 @@ +{ + "type": "machine", + "name": "fdm_klipper_common", + "inherits": "fdm_machine_common", + "from": "system", + "instantiation": "false", + "gcode_flavor": "klipper", + "machine_max_acceleration_e": [ + "5000", + "5000" + ], + "machine_max_acceleration_extruding": [ + "20000", + "20000" + ], + "machine_max_acceleration_retracting": [ + "5000", + "5000" + ], + "machine_max_acceleration_travel": [ + "20000", + "20000" + ], + "machine_max_acceleration_x": [ + "20000", + "20000" + ], + "machine_max_acceleration_y": [ + "20000", + "20000" + ], + "machine_max_acceleration_z": [ + "500", + "200" + ], + "machine_max_speed_e": [ + "25", + "25" + ], + "machine_max_speed_x": [ + "500", + "200" + ], + "machine_max_speed_y": [ + "500", + "200" + ], + "machine_max_speed_z": [ + "12", + "12" + ], + "machine_max_jerk_e": [ + "2.5", + "2.5" + ], + "machine_max_jerk_x": [ + "9", + "9" + ], + "machine_max_jerk_y": [ + "9", + "9" + ], + "machine_max_jerk_z": [ + "0.2", + "0.4" + ], + "machine_min_extruding_rate": [ + "0", + "0" + ], + "machine_min_travel_rate": [ + "0", + "0" + ], + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "printable_height": "250", + "extruder_clearance_radius": "65", + "extruder_clearance_height_to_rod": "36", + "extruder_clearance_height_to_lid": "140", + "printer_settings_id": "", + "printer_technology": "FFF", + "printer_variant": "0.4", + "retraction_minimum_travel": [ + "1" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_length": [ + "0.8" + ], + "retract_length_toolchange": [ + "2" + ], + "z_hop": [ + "0.4" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retraction_speed": [ + "30" + ], + "deretraction_speed": [ + "30" + ], + "z_hop_types": "Normal Lift", + "silent_mode": "0", + "single_extruder_multi_material": "1", + "change_filament_gcode": "", + "wipe": [ + "1" + ], + "default_filament_profile": [ + "Generic PLA @System" + ], + "default_print_profile": "0.20mm Standard @MyKlipper", + "bed_exclude_area": [ + "0x0" + ], + "machine_start_gcode": "M190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nPRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer_single]\n", + "machine_end_gcode": "PRINT_END", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n", + "machine_pause_gcode": "PAUSE", + "scan_first_layer": "0", + "nozzle_type": "undefine", + "auxiliary_fan": "0" +} 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..963f39c9d8 --- /dev/null +++ b/resources/profiles/IdeaFormer/machine/fdm_machine_common.json @@ -0,0 +1,119 @@ +{ + "type": "machine", + "name": "fdm_machine_common", + "from": "system", + "instantiation": "false", + "printer_technology": "FFF", + "deretraction_speed": [ + "40" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "gcode_flavor": "marlin", + "silent_mode": "0", + "machine_max_acceleration_e": [ + "5000" + ], + "machine_max_acceleration_extruding": [ + "10000" + ], + "machine_max_acceleration_retracting": [ + "1000" + ], + "machine_max_acceleration_x": [ + "10000" + ], + "machine_max_acceleration_y": [ + "10000" + ], + "machine_max_acceleration_z": [ + "500" + ], + "machine_max_speed_e": [ + "60" + ], + "machine_max_speed_x": [ + "500" + ], + "machine_max_speed_y": [ + "500" + ], + "machine_max_speed_z": [ + "10" + ], + "machine_max_jerk_e": [ + "5" + ], + "machine_max_jerk_x": [ + "8" + ], + "machine_max_jerk_y": [ + "8" + ], + "machine_max_jerk_z": [ + "0.4" + ], + "machine_min_extruding_rate": [ + "0" + ], + "machine_min_travel_rate": [ + "0" + ], + "max_layer_height": [ + "0.32" + ], + "min_layer_height": [ + "0.08" + ], + "printable_height": "250", + "extruder_clearance_radius": "65", + "extruder_clearance_height_to_rod": "36", + "extruder_clearance_height_to_lid": "140", + "nozzle_diameter": [ + "0.4" + ], + "printer_settings_id": "", + "printer_variant": "0.4", + "retraction_minimum_travel": [ + "2" + ], + "retract_before_wipe": [ + "70%" + ], + "retract_when_changing_layer": [ + "1" + ], + "retraction_length": [ + "1" + ], + "retract_length_toolchange": [ + "1" + ], + "z_hop": [ + "0" + ], + "retract_restart_extra": [ + "0" + ], + "retract_restart_extra_toolchange": [ + "0" + ], + "retraction_speed": [ + "60" + ], + "single_extruder_multi_material": "1", + "change_filament_gcode": "", + "wipe": [ + "1" + ], + "default_print_profile": "", + "machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up", + "machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n", + "machine_pause_gcode": "M601" +} 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..b06b2e2192 --- /dev/null +++ b/resources/profiles/IdeaFormer/process/0.20mm Standard @IdeaFormer IR3 V2.json @@ -0,0 +1,22 @@ +{ + "type": "process", + "name": "0.20mm Standard @IdeaFormer IR3 V2", + "inherits": "fdm_process_common", + "from": "system", + "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..b8e882770a --- /dev/null +++ b/resources/profiles/IdeaFormer/process/fdm_process_common.json @@ -0,0 +1,108 @@ +{ + "type": "process", + "name": "fdm_process_common", + "from": "system", + "instantiation": "false", + "adaptive_layer_height": "0", + "reduce_crossing_wall": "0", + "max_travel_detour_distance": "0", + "bottom_surface_pattern": "monotonic", + "bottom_shell_thickness": "0", + "bridge_speed": "50", + "brim_width": "5", + "brim_object_gap": "0.1", + "compatible_printers": [], + "compatible_printers_condition": "", + "print_sequence": "by layer", + "default_acceleration": "1000", + "initial_layer_acceleration": "500", + "top_surface_acceleration": "1000", + "travel_acceleration": "1000", + "inner_wall_acceleration": "1000", + "outer_wall_acceleration": "700", + "bridge_no_support": "0", + "draft_shield": "disabled", + "elefant_foot_compensation": "0", + "enable_arc_fitting": "0", + "wall_infill_order": "inner wall/outer wall/infill", + "infill_direction": "45", + "sparse_infill_density": "15%", + "sparse_infill_pattern": "crosshatch", + "initial_layer_print_height": "0.2", + "infill_combination": "0", + "infill_wall_overlap": "25%", + "interface_shells": "0", + "ironing_flow": "10%", + "ironing_spacing": "0.15", + "ironing_speed": "30", + "ironing_type": "no ironing", + "reduce_infill_retraction": "1", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[initial_tool]}_{printer_model}_{print_time}.gcode", + "detect_overhang_wall": "1", + "slowdown_for_curled_perimeters": "1", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "50", + "overhang_3_4_speed": "30", + "overhang_4_4_speed": "10", + "line_width": "110%", + "inner_wall_line_width": "110%", + "outer_wall_line_width": "100%", + "top_surface_line_width": "93.75%", + "sparse_infill_line_width": "110%", + "initial_layer_line_width": "120%", + "internal_solid_infill_line_width": "120%", + "support_line_width": "96%", + "wall_loops": "3", + "print_settings_id": "", + "raft_layers": "0", + "seam_position": "aligned", + "skirt_distance": "2", + "skirt_height": "3", + "min_skirt_length": "4", + "skirt_loops": "0", + "minimum_sparse_infill_area": "15", + "spiral_mode": "0", + "standby_temperature_delta": "-5", + "enable_support": "0", + "resolution": "0.012", + "support_type": "normal(auto)", + "support_on_build_plate_only": "0", + "support_top_z_distance": "0.2", + "support_bottom_z_distance": "0.2", + "support_filament": "0", + "support_interface_loop_pattern": "0", + "support_interface_filament": "0", + "support_interface_top_layers": "2", + "support_interface_bottom_layers": "2", + "support_interface_spacing": "0.5", + "support_interface_speed": "80", + "support_base_pattern": "default", + "support_base_pattern_spacing": "2.5", + "support_speed": "150", + "support_threshold_angle": "30", + "support_object_xy_distance": "0.35", + "tree_support_branch_angle": "30", + "tree_support_wall_count": "0", + "tree_support_with_infill": "0", + "detect_thin_wall": "0", + "top_surface_pattern": "monotonicline", + "top_shell_thickness": "0.8", + "enable_prime_tower": "1", + "wipe_tower_no_sparse_layers": "0", + "prime_tower_width": "60", + "xy_hole_compensation": "0", + "xy_contour_compensation": "0", + "layer_height": "0.2", + "bottom_shell_layers": "3", + "top_shell_layers": "4", + "bridge_flow": "1", + "initial_layer_speed": "45", + "initial_layer_infill_speed": "45", + "outer_wall_speed": "45", + "inner_wall_speed": "80", + "sparse_infill_speed": "150", + "internal_solid_infill_speed": "150", + "top_surface_speed": "50", + "gap_infill_speed": "30", + "travel_speed": "200" +} diff --git a/resources/shaders/110/gouraud.fs b/resources/shaders/110/gouraud.fs index e602d6067d..12f4feca3e 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 ff3a190c79..e2de2b63e5 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; @@ -71,8 +72,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; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. 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/140/gouraud.fs b/resources/shaders/140/gouraud.fs index bbfb76f7a1..0d9fa021fe 100644 --- a/resources/shaders/140/gouraud.fs +++ b/resources/shaders/140/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/140/gouraud.vs b/resources/shaders/140/gouraud.vs index 72dd4ff1bf..c69dcd11df 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; @@ -71,8 +72,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; // Fill in the scalars for fragment shader clipping. Fragments with any of these components lower than zero are discarded. 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/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp new file mode 100644 index 0000000000..3ba2f79e55 --- /dev/null +++ b/src/libslic3r/BeltGCode.cpp @@ -0,0 +1,76 @@ +#include "BeltGCode.hpp" +#include "BeltGCodeWriter.hpp" +#include "BeltTransform.hpp" +#include "Print.hpp" + +namespace Slic3r { + +void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) +{ + if (!print.config().belt_printer.value) + return; + + auto belt_writer = std::make_unique(); + belt_writer->set_is_bbl_machine(is_bbl_printers); + // Axis remap and build volume max are set by base GCode after init_belt_writer returns. + belt_writer->set_belt_back_transform(print.config()); + belt_writer->set_machine_frame_transform(print.config()); + m_writer = std::move(belt_writer); +} + +void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) +{ + if (!print.config().belt_printer.value) + return; + + const auto &full_cfg = print.full_print_config(); + // Slicing rotation: the belt tilt (axis + angle) and the single source of truth + // for the physical tilt the G-code viewer uses to enable belt view. + file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str()); + file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value); + file.write_format("; belt_slice_rotation_global = %d\n", print.config().belt_slice_rotation_global.value ? 1 : 0); + // Pre-slice remap configs + file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str()); + file.write_format("; preslice_remap_y = %s\n", full_cfg.opt_serialize("preslice_remap_y").c_str()); + file.write_format("; preslice_remap_z = %s\n", full_cfg.opt_serialize("preslice_remap_z").c_str()); + file.write_format("; preslice_remap_global = %d\n", print.config().preslice_remap_global.value ? 1 : 0); + file.write_format("; belt_preslice_global = %d\n", print.config().belt_preslice_global.value ? 1 : 0); + // Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt + // tilt angle (or belt_frame_tilt_angle when decoupled). + file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0); + file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value); +} + +void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/) +{ + // Global pre-slice mode: adjust origin using computed correction. + // Transform the origin through the belt pipeline so that + // back_transform(T * origin) = origin (correct machine position). + // + // Flags that trigger this path: + // belt_preslice_global — full pipeline (rotation * remap) is global + // preslice_remap_global — only the pre-slice remap is global + // belt_slice_rotation_global — slicing rotation treated as global (matches + // the per-instance Z-offset added in PrintObjectSlice.cpp) + // The XY origin adjustment uses the FULL forward transform, because the + // back_transform applied during G-code emission is always the inverse of + // the full pipeline. + bool use_global = m_config.belt_preslice_global.value + || (m_config.preslice_remap_global.value + && BeltTransformPipeline::has_preslice_remap(m_config)) + || (m_config.belt_slice_rotation_global.value + && m_config.belt_slice_rotation.value != BeltRotationAxis::None + && std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON); + if (!use_global || !m_config.belt_printer.value) + return; + + // Adjust origin: transform through belt forward pipeline so that + // the back-transform correctly recovers model-space positions. + Transform3d T = BeltTransformPipeline::build_forward_transform(m_config); + Vec2d cur_origin = this->origin(); + Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.); + Vec3d adjusted = T.linear() * origin3d; + this->set_origin(Vec2d(adjusted.x(), adjusted.y())); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp new file mode 100644 index 0000000000..2a3718b49f --- /dev/null +++ b/src/libslic3r/BeltGCode.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "GCode.hpp" + +namespace Slic3r { + +// Belt-printer-specific GCode export. +// +// Inherits from GCode and overrides virtual hooks to: +// - Create a BeltGCodeWriter instead of a plain GCodeWriter +// - Write belt configuration to the G-code header +// - Adjust the origin for global pre-slice transforms when switching instances +// - Disable arc fitting (G2/G3 not supported on belt printers) +class BeltGCode : public GCode +{ +protected: + void init_belt_writer(Print &print, bool is_bbl_printers) override; + void write_belt_header(GCodeOutputStream &file, const Print &print) override; + void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; + bool should_disable_arc_fitting() const override { return true; } +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp new file mode 100644 index 0000000000..7c645edf43 --- /dev/null +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -0,0 +1,264 @@ +#include "BeltGCodeWriter.hpp" +#include "FirstLayerPlane.hpp" +#include "Geometry.hpp" +#include + +namespace Slic3r { + +namespace { + +// Decide whether a particular destination point gets first-layer treatment. +// When the plane evaluator is active, distance from the plane wins; otherwise +// fall back to the layer-coarse m_is_first_layer flag set by the caller. +inline bool belt_point_on_first_layer( + const FirstLayerPlane *plane, + double first_layer_thickness_mm, + bool layer_first_flag, + const Vec3d &point_slicing_mm) +{ + if (plane && plane->is_active()) + return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm); + return layer_first_flag; +} + +} // namespace + +// ---- Belt configuration --------------------------------------------------- + +void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) +{ + m_belt_back_transform.init_from_config(config); +} + +void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config) +{ + m_machine_frame_transform.init_from_config(config); +} + +Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const +{ + // Step 1+2: To Cartesian (back_transform + axis_remap). + // In world-coordinates mode (PA line / PA pattern calibration) the input + // already describes a point relative to the belt surface, so the + // slicer->world back-transform is skipped and only the machine kinematics + // (axis remap + frame shear/scale) are applied. + Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos); + Vec3d result = apply_axis_remap(after_back); + Vec3d after_remap = result; + // Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts + // as a global linear transform on the placed coords. + Vec3d final = m_machine_frame_transform.apply(result); + + // [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z + // crosses an integer mm boundary) to keep the log volume manageable while + // still capturing one sample per ~5 layers. Shows the full pipeline so + // Case A vs Case B can be compared step-by-step. + static thread_local int s_last_logged_z = std::numeric_limits::min(); + int z_bucket = static_cast(std::floor(pos.z() * 5.0)); // every 0.2mm + if (z_bucket != s_last_logged_z) { + s_last_logged_z = z_bucket; + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords" + << " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")" + << " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")" + << " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")" + << " final=(" << final.x() << "," << final.y() << "," << final.z() << ")" + << " mft_active=" << m_machine_frame_transform.is_active() + << " back_active=" << m_belt_back_transform.is_active(); + } + return final; +} + +// ---- Overridden movement methods ------------------------------------------ + +std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) +{ + m_pos(0) = point(0); + m_pos(1) = point(1); + + this->set_current_position_clear(true); + Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; + + // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + + GCodeG1Formatter w; + w.emit_xyz(machine); + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, + Vec3d(point.x(), point.y(), m_pos.z())); + auto speed = first_layer_for_point + ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) +{ + // Belt printer: force NormalLift since SpiralLift and SlopeLift compute + // slope angles that don't account for the YZ coordinate rotation. + return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase); +} + +std::string BeltGCodeWriter::eager_lift(const LiftType type) +{ + // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). + return GCodeWriter::eager_lift(LiftType::NormalLift); +} + +std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) +{ + m_pos(2) = z; + + double speed = this->config.travel_speed_z.value; + if (speed == 0.) { + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, + Vec3d(m_pos.x(), m_pos.y(), z)); + speed = first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; + } + + // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. + Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); + + GCodeG1Formatter w; + w.emit_xyz(machine); + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + m_pos(0) = point(0); + m_pos(1) = point(1); + if (std::abs(dE) <= std::numeric_limits::epsilon()) + force_no_extrusion = true; + + if (!force_no_extrusion) + filament()->extrude(dE); + + Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; + + // Belt printer: transform and emit XYZ (Y and Z are coupled) + Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); + + GCodeG1Formatter w; + w.emit_xyz(machine); + if (!force_no_extrusion) + w.emit_e(filament()->E()); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) +{ + m_pos = point; + m_lifted = 0; + if (!force_no_extrusion) + filament()->extrude(dE); + + Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; + point_on_plate = to_machine_coords(point_on_plate); + + GCodeG1Formatter w; + w.emit_xyz(point_on_plate); + if (!force_no_extrusion) + w.emit_e(filament()->E()); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + return w.string(); +} + +std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z) +{ + // Belt-specific override of travel_to_xyz. + // Key differences from base: + // 1. All coordinates go through to_machine_coords() + // 2. Always emit full XYZ (can't split XY and Z due to coupling) + // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) + + Vec3d dest_point = point; + const bool first_layer_for_point = belt_point_on_first_layer( + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); + auto travel_speed = + first_layer_for_point ? this->config.get_abs_value("initial_layer_travel_speed") + : this->config.travel_speed.value; + + // Handle pending z_hop + if (std::abs(m_to_lift) > EPSILON) { + assert(std::abs(m_lifted) < EPSILON); + if ((!this->is_current_position_clear() || m_pos != dest_point) && + m_to_lift + m_pos(2) > point(2)) { + m_lifted = m_to_lift + m_pos(2) - point(2); + dest_point(2) = m_to_lift + m_pos(2); + } + m_to_lift = 0.; + + std::string slop_move; + Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) }; + Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + Vec3d delta = target - source; + Vec2d delta_no_z = { delta(0), delta(1) }; + + if (delta(2) > 0 && delta_no_z.norm() != 0.0f) { + // Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift), + // but handle NormalLift and fallthrough. + if (m_to_lift_type == LiftType::SlopeLift && + this->is_current_position_clear() && + atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) { + Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); + Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; + slope_top_point = to_machine_coords(slope_top_point); + GCodeG1Formatter w0; + w0.emit_xyz(slope_top_point); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + slop_move = w0.string(); + } + else if (m_to_lift_type == LiftType::NormalLift) { + slop_move = _travel_to_z(target.z(), "normal lift Z"); + } + } + + std::string xy_z_move; + { + Vec3d emit_target = to_machine_coords(target); + GCodeG1Formatter w0; + // Belt mode: always emit full XYZ since Y and Z are coupled + w0.emit_xyz(emit_target); + w0.emit_f(travel_speed * 60.0); + w0.emit_comment(GCodeWriter::full_gcode_comment, comment); + xy_z_move = w0.string(); + } + m_pos = dest_point; + this->set_current_position_clear(true); + return slop_move + xy_z_move; + } + else if (!force_z && !this->will_move_z(point(2))) { + double nominal_z = m_pos(2) - m_lifted; + m_lifted -= (point(2) - nominal_z); + if (std::abs(m_lifted) < EPSILON) + m_lifted = 0.; + this->set_current_position_clear(true); + return this->travel_to_xy(to_2d(point)); + } + else { + m_lifted = 0; + } + + Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; + point_on_plate = to_machine_coords(point_on_plate); + + // Belt mode: always emit full XYZ + GCodeG1Formatter w; + w.emit_xyz(point_on_plate); + w.emit_f(this->config.travel_speed.value * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + + m_pos = dest_point; + this->set_current_position_clear(true); + return w.string(); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp new file mode 100644 index 0000000000..ff7428d062 --- /dev/null +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "GCodeWriter.hpp" +#include "GCode/BeltBackTransform.hpp" +#include "GCode/MachineFrameTransform.hpp" + +namespace Slic3r { + +class FirstLayerPlane; + +// Belt-printer-specific GCode writer. +// +// Inherits from GCodeWriter and overrides movement methods to apply +// coordinate transformation (back-transform, axis remap, machine-frame +// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). +class BeltGCodeWriter : public GCodeWriter +{ +public: + BeltGCodeWriter() : GCodeWriter() {} + + // Belt configuration (axis remap is inherited from GCodeWriter) + void set_belt_back_transform(const PrintConfig &config); + void set_machine_frame_transform(const PrintConfig &config); + Vec3d to_machine_coords(const Vec3d &pos) const; + + // World-coordinates mode: incoming coordinates are treated as points + // relative to the physical belt surface (X across, Y along the belt, + // Z height above it) instead of slicing-frame coordinates — the + // slicer->world back-transform is skipped. Used by the PA line / PA + // pattern calibration generators, whose logical bed coordinates describe + // first-layer drawings on the build surface. + void set_world_coordinates(bool enable) { m_world_coordinates = enable; } + + // First-layer plane: when set to a non-null active evaluator, travel + // speed selection consults the plane per-move and uses + // initial_layer_travel_speed for points within first_layer_height_mm + // of the plane (regardless of slicing layer index). + void set_first_layer_plane(const FirstLayerPlane *plane, + double first_layer_height_mm) { + m_first_layer_plane = plane; + m_first_layer_thickness_mm = first_layer_height_mm; + } + + // Overridden movement methods + std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; + std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; + std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; + std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; + std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override; + std::string eager_lift(const LiftType type) override; + +protected: + std::string _travel_to_z(double z, const std::string &comment) override; + +private: + BeltBackTransform m_belt_back_transform; + MachineFrameTransform m_machine_frame_transform; + bool m_world_coordinates = false; + // Borrowed pointer; lifetime owned by GCode. null = inactive. + const FirstLayerPlane *m_first_layer_plane = nullptr; + double m_first_layer_thickness_mm = 0.; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp new file mode 100644 index 0000000000..cdb6ba428f --- /dev/null +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -0,0 +1,143 @@ +#include "BeltSliceStrategy.hpp" +#include "Model.hpp" + +#include + +#include +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG +#include +#include +#include +#endif + +namespace Slic3r { + +void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, + const PrintConfig &config, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z) +{ + // 1. Standalone pre-slice axis remap (works without belt mode). + const bool has_remap = BeltTransformPipeline::has_preslice_remap(config); + if (has_remap) + trafo = BeltTransformPipeline::build_preslice_remap(config) * trafo; + + // 2. Belt rotation — the sole mesh-side belt transform (matching + // BeltTransformPipeline::build_forward_transform). Only active in + // belt-printer mode. + bool has_rotation = false; + if (config.belt_printer.value) { + const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation); + if (has_rotation) { + Transform3d belt_xform = Transform3d::Identity(); + belt_xform.linear() = rot; + trafo = belt_xform * trafo; + } + } + + if (!has_remap && !has_rotation) + return; + + // 3. Z-shift — detect if the mesh clips below the build plate after the + // transforms and lift it. Each mesh vertex must be brought into object space + // via mv->get_matrix() before applying the full trafo (which is in object + // space). Missing this on assemblies (where per-volume get_matrix() positions + // each volume within the object) would compute min_z against mesh-local vertex + // coordinates rather than object-space coordinates, so volumes translated along + // the slicer's Z axis would be silently excluded from the bound check. +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + // Capture the incoming trafo for diagnostic logging. + // This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift. + const Transform3d trafo_pre_shift = trafo; + auto log_mat = [](const Matrix3d &m) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "]," + << "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "]," + << "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]"; + return ss.str(); + }; + auto log_vec3 = [](const Vec3d &v) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; + return ss.str(); + }; + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter" + << " has_rotation=" << has_rotation + << " has_remap=" << has_remap + << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) + << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) + << " volumes=" << model_volumes.size(); +#endif + + double min_z = std::numeric_limits::max(); +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + int vol_idx = 0; +#endif + for (const ModelVolume *mv : model_volumes) { +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + if (!mv->is_model_part()) { ++vol_idx; continue; } +#else + if (!mv->is_model_part()) continue; +#endif + Transform3d vol_trafo = trafo * mv->get_matrix(); + const auto &its = mv->mesh().its; +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + // Per-volume bbox in mesh-frame and post-trafo slicer-frame. + Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); + double vol_min_z = std::numeric_limits::max(); +#endif + for (const stl_vertex &v : its.vertices) { + Vec3d vm = v.cast(); + Vec3d pt = vol_trafo * vm; + min_z = std::min(min_z, pt.z()); +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + mesh_min = mesh_min.cwiseMin(vm); + mesh_max = mesh_max.cwiseMax(vm); + slicer_min = slicer_min.cwiseMin(pt); + slicer_max = slicer_max.cwiseMax(pt); + vol_min_z = std::min(vol_min_z, pt.z()); +#endif + } +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx + << "] id=" << mv->id().id << " name='" << mv->name << "'" + << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) + << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) + << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) + << " vol_min_z=" << vol_min_z; + ++vol_idx; +#endif + } + const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z + << " z_shift_val=" << z_shift_val; +#endif + if (z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = z_shift_val; + trafo = z_shift * trafo; + } + // out_belt_min_z is only meaningful in belt mode; the standalone-remap path + // never reported it. + if (out_belt_min_z && config.belt_printer.value) { + const double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() + << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; +#endif + *out_belt_min_z = new_val; + } +#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit" + << " final_trafo.linear=" << log_mat(trafo.linear()) + << " final_trafo.translation=" << log_vec3(trafo.translation()); +#endif +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp new file mode 100644 index 0000000000..33a7ed4979 --- /dev/null +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "libslic3r.h" +#include "Point.hpp" +#include "BeltTransform.hpp" +#include "PrintConfig.hpp" +#include "Model.hpp" + +namespace Slic3r { + +// Belt printer / pre-slice transform strategy. +// +// Composes, in order, the pre-slice mesh transforms applied before slicing: +// 1. Pre-slice axis remap (standalone — works without belt mode) +// 2. Belt rotation (the sole mesh-side belt transform; shear & scale are a +// g-code-side stage, see MachineFrameTransform) +// 3. Per-object Z-shift that lifts the mesh above the build plate +// +// Isolates this belt/remap-specific logic from the generic slicing pipeline in +// PrintObjectSlice.cpp. +class BeltSliceStrategy +{ +public: + // Apply the pre-slice remap + belt rotation + Z-shift to `trafo` in place. + // No-op when neither a remap nor a belt rotation is configured. + // + // out_belt_min_z (if non-null) receives the minimum mesh Z after the + // transforms, but only in belt-printer mode — the standalone-remap path + // never reported it. + static void apply_preslice_transforms(Transform3d &trafo, + const PrintConfig &config, + const ModelVolumePtrs &model_volumes, + double *out_belt_min_z = nullptr); +}; + +} // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp new file mode 100644 index 0000000000..db23e13417 --- /dev/null +++ b/src/libslic3r/BeltTransform.cpp @@ -0,0 +1,223 @@ +#include "BeltTransform.hpp" +#include "Model.hpp" + +#include + +namespace Slic3r { + +// ---- Matrix builders ------------------------------------------------------ + +Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &config) +{ + Transform3d pre_remap = Transform3d::Identity(); + if (!has_preslice_remap(config)) + return pre_remap; + + int pre_rx = int(config.preslice_remap_x.value); + int pre_ry = int(config.preslice_remap_y.value); + int pre_rz = int(config.preslice_remap_z.value); + + // Each remap value selects a source axis and sign. + auto remap_column = [](int r) -> Vec3d { + int axis = r % 3; + Vec3d col = Vec3d::Zero(); + if (r < 3) col[axis] = 1.0; // +axis + else if (r < 6) col[axis] = -1.0; // -axis + else col[axis] = -1.0; // Rev: max - pos = -(pos - max) + return col; + }; + + Matrix3d remap_lin; + remap_lin.col(0) = remap_column(pre_rx); + remap_lin.col(1) = remap_column(pre_ry); + remap_lin.col(2) = remap_column(pre_rz); + pre_remap.linear() = remap_lin; + + // Translation for Rev modes (needs build volume extents). + if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) { + BoundingBoxf bbox_bed(config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(), + config.printable_height.value); + Vec3d remap_trans = Vec3d::Zero(); + auto add_rev = [&](int r, int out) { + if (r >= 6) remap_trans[out] = vol_max[r % 3]; + }; + add_rev(pre_rx, 0); + add_rev(pre_ry, 1); + add_rev(pre_rz, 2); + pre_remap.translation() = remap_trans; + } + + return pre_remap; +} + +Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out) +{ + BeltRotationAxis axis = config.belt_slice_rotation.value; + double angle_deg = config.belt_slice_rotation_angle.value; + bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON; + if (has_rot_out) *has_rot_out = active; + if (!active) + return Matrix3d::Identity(); + double angle_rad = Geometry::deg2rad(angle_deg); + Vec3d unit_axis; + switch (axis) { + case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; + case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; + default: return Matrix3d::Identity(); + } + return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); +} + +Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config) +{ + // Mesh-side belt transform: rotation applied after the pre-slice axis remap. + // (Shear & scale are a g-code-side stage, not part of the mesh transform.) + Transform3d pre_remap = build_preslice_remap(config); + Matrix3d rot = build_rotation_matrix(config); + + Transform3d combined = Transform3d::Identity(); + combined.linear() = rot; + combined = combined * pre_remap; + return combined; +} + +// ---- Bounding box remap --------------------------------------------------- + +BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) +{ + int pre_rx = int(config.preslice_remap_x.value); + int pre_ry = int(config.preslice_remap_y.value); + int pre_rz = int(config.preslice_remap_z.value); + + if (pre_rx == int(RemapAxis::PosX) && + pre_ry == int(RemapAxis::PosY) && + pre_rz == int(RemapAxis::PosZ)) + return bb; // Identity remap. + + auto remap_coord = [](int r, const Vec3d &v) -> double { + int axis = r % 3; + if (r < 3) return v[axis]; + return -v[axis]; + }; + + Vec3d mn = bb.min.cast(), mx = bb.max.cast(); + BoundingBoxf3 rbb; + for (int i = 0; i < 8; ++i) { + Vec3d c((i & 1) ? mx.x() : mn.x(), + (i & 2) ? mx.y() : mn.y(), + (i & 4) ? mx.z() : mn.z()); + Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c)); + if (i == 0) rbb = BoundingBoxf3(rc, rc); + else rbb.merge(rc); + } + return rbb; +} + +BoundingBoxf3 BeltTransformPipeline::remap_bbox(const ModelObject &model_object, const PrintConfig &config) +{ + return remap_bbox(model_object.raw_bounding_box(), config); +} + +// ---- Belt floor parameters ------------------------------------------------ + +// Shared implementation for both PrintConfig and DynamicPrintConfig. +// Template avoids duplicating the math for the two config types. +namespace { + +template +BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( + const Config &config, const BoundingBoxf3 &bb, double original_height) +{ + BeltTransformPipeline::BeltHeightResult result; + result.object_height = original_height; + + // Extract the mesh rotation from config (the sole mesh-side belt transform). + BeltRotationAxis rot_axis; + double rot_angle; + + if constexpr (std::is_same_v) { + rot_axis = config.belt_slice_rotation.value; + rot_angle = config.belt_slice_rotation_angle.value; + } else { + // DynamicPrintConfig path + auto get_float = [&](const char *key) { + auto *opt = config.template option(key); + return opt ? opt->value : 0.0; + }; + auto get_rot_axis = [&](const char *key) { + auto *opt = config.template option>(key); + return opt ? opt->value : BeltRotationAxis::None; + }; + rot_axis = get_rot_axis("belt_slice_rotation"); + rot_angle = get_float("belt_slice_rotation_angle"); + } + + bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON; + if (!has_rotation) + return result; + + // Rotation path: sweep the 8 bbox corners through R to get the rotated height, + // then derive the belt floor (the image of machine-Z = 0 under R). + double angle_rad = Geometry::deg2rad(rot_angle); + Vec3d unit_axis; + switch (rot_axis) { + case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break; + case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break; + case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break; + default: unit_axis = Vec3d::UnitX(); break; + } + Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix(); + double min_rz = std::numeric_limits::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()); + double z = (R * c).z(); + min_rz = std::min(min_rz, z); + max_rz = std::max(max_rz, z); + } + result.object_height = max_rz - min_rz; + + // Belt floor in slicer-frame is the image of z_machine = 0 under R. + // R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s + // R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s + // R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor + double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad); + switch (rot_axis) { + case BeltRotationAxis::X: + result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.; + result.floor_params.from_axis = 1; // Y + break; + case BeltRotationAxis::Y: + result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.; + result.floor_params.from_axis = 0; // X + break; + case BeltRotationAxis::Z: + default: + result.floor_params.shear_factor = 0.0; + result.floor_params.from_axis = 1; + break; + } + result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); + + return result; +} + +} // anonymous namespace + +BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( + const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) +{ + return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); +} + +BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( + const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height) +{ + return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height); +} + +} // namespace Slic3r diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp new file mode 100644 index 0000000000..ad5b6d6ffd --- /dev/null +++ b/src/libslic3r/BeltTransform.hpp @@ -0,0 +1,152 @@ +#pragma once + +#include "libslic3r.h" +#include "Point.hpp" +#include "BoundingBox.hpp" +#include "PrintConfig.hpp" +#include "Geometry.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 * pre_remap * trafo_in +// +// Rotation is the sole mesh-side belt transform; shear & scale are applied +// to the g-code instead (see MachineFrameTransform). This class provides the +// building blocks so every call site uses the same implementation. z_shift is +// object-dependent (computed from mesh vertex bounds) and is NOT included in +// build_forward_transform(). The machine-frame shear/scale is derived directly +// from the tilt angle in MachineFrameTransform and no longer lives here. +// +// Design note: this mesh-rotation approach replaced an earlier pre-shear +// method (now removed). While that initial pre-shear method was instrumental +// in getting belt printer slicing off the ground in the first place, its place is +// in the past. A big thank you goes to the Unlayered3D team, who recommended +// switching to a pre-slice rotation stage instead. Doing so keeps the slicing +// operation isometric — no distortion of the sliced geometry — while the +// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale +// derived from the same tilt angle. +// +// This fixed a number of issues, including several issues noticed by hotcubcar +// regarding adaptive infills not working, gyroid becoming anisotropic, and more +// that were all mostly resolved as a result of the switch. +// +// This also means that the pre-slice rotation transform methodology can be used +// more cleanly on non-belt printers. +// - HarrierPigeon (Joseph Robertson) + +class BeltTransformPipeline +{ +public: + // ---- Identity checks -------------------------------------------------- + + static bool has_preslice_remap(const PrintConfig &config) + { + return int(config.preslice_remap_x.value) != int(RemapAxis::PosX) || + int(config.preslice_remap_y.value) != int(RemapAxis::PosY) || + int(config.preslice_remap_z.value) != int(RemapAxis::PosZ); + } + + // Overload accepting DynamicPrintConfig (used in static slicing_parameters). + static bool has_preslice_remap(const DynamicPrintConfig &config) + { + auto get_int = [&](const char *key) -> int { + auto *opt = config.option>(key); + return opt ? int(opt->value) : 0; + }; + return get_int("preslice_remap_x") != int(RemapAxis::PosX) || + get_int("preslice_remap_y") != int(RemapAxis::PosY) || + get_int("preslice_remap_z") != int(RemapAxis::PosZ); + } + + static bool has_rotation(const PrintConfig &config) + { + return config.belt_slice_rotation.value != BeltRotationAxis::None && + std::abs(config.belt_slice_rotation_angle.value) > EPSILON; + } + + // Physical belt tilt derived from the slicing rotation — the single source of + // truth for bed rendering, support gravity tilt and the bed-exclusion + // projection. Returns the tilt magnitude in degrees split onto the X and Y + // build-plate tilt axes according to the rotation axis: + // rotation about X → tilt_x = angle (gantry tilts in the YZ plane) + // rotation about Y → tilt_y = angle (gantry tilts in the XZ plane) + // rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt) + // The magnitude uses abs(angle) so a negative rotation still reports a positive + // physical tilt. + struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; }; + + static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg) + { + PhysicalTilt t; + double mag = std::abs(angle_deg); + switch (axis) { + case BeltRotationAxis::X: t.tilt_x_deg = mag; break; + case BeltRotationAxis::Y: t.tilt_y_deg = mag; break; + default: break; // Z / None: no physical tilt + } + return t; + } + + static PhysicalTilt physical_tilt(const PrintConfig &config) + { + return physical_tilt(config.belt_slice_rotation.value, + config.belt_slice_rotation_angle.value); + } + + // ---- Matrix builders -------------------------------------------------- + + // Build the pre-slice axis remap transform (includes Rev-mode translation). + static Transform3d build_preslice_remap(const PrintConfig &config); + + // Build the 3x3 rotation matrix from belt_slice_rotation* config. + // Returns Identity if rotation axis is None or angle is ~0. + // Also sets has_rot_out if non-null. + static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr); + + // Combined forward transform (rotation * pre_remap) — the mesh-side belt + // transform that BeltSliceStrategy applies and BeltBackTransform inverts. + // Does NOT include the per-object Z-shift. + static Transform3d build_forward_transform(const PrintConfig &config); + + // ---- Bounding box remap ----------------------------------------------- + + // Remap a bounding box through the pre-slice axis remap. + // Returns the original bbox if remap is identity. + static BoundingBoxf3 remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config); + static BoundingBoxf3 remap_bbox(const ModelObject &model_object, const PrintConfig &config); + + // ---- Belt floor parameters -------------------------------------------- + + struct BeltFloorParams { + double shear_factor = 0.0; + int from_axis = 1; + double z_shift = 0.0; + }; + + // Result of computing belt height + floor params. + struct BeltHeightResult { + double object_height; // Effective object height after shear/scale + BeltFloorParams floor_params; + }; + + // Compute effective object height and belt floor parameters from config + // and pre-remapped bounding box. original_height is the input height + // (bb.size().z() or model_object.max_z()). + static BeltHeightResult compute_belt_height_and_floor( + const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, + double original_height); + + // Overload for DynamicPrintConfig (used by static slicing_parameters). + static BeltHeightResult compute_belt_height_and_floor( + const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, + double original_height); +}; + +} // namespace Slic3r diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index 91d72faa70..0bbe8dad83 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -895,6 +895,10 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_ std::map* objectBrimAreasOut, std::map* supportBrimAreasOut) { + // Belt printer: brim is not compatible with belt printing. + if (print.config().belt_printer.value) + return; + std::map brim_width_map; std::map brimAreaMap; std::map supportBrimAreaMap; diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 1ac200d9c8..c9e779f9e7 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -176,6 +176,31 @@ BuildVolume::BuildVolume(const std::vector &printable_area, const double BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name(); } +void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y) +{ + m_is_belt_printer = enabled; + m_belt_angle = angle_deg; + m_belt_infinite_y = infinite_y; + + // Restart from the unmodified bbox each call. Without this, toggling + // belt mode off (or switching infinite_y true→false) would leave the + // extents inflated and break collision / object_state checks. + BoundingBoxf bboxf = get_extents(m_bed_shape); + m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) }; + + if (enabled) { + if (infinite_y) { + // Extend the Y bound to a very large value for infinite belt. + m_bboxf.max.y() = 100000.; + } + // Belt printer: the Z extent already equals printable_height (set above), which + // is the usable vertical clearance above the belt. The gantry's axis range is + // sized to reach height/cos(tilt), so no diagonal scaling is applied here — this + // keeps the live "outside build volume" highlight in agreement with Print::validate(). + (void) angle_deg; + } +} + #if 0 // Tests intersections of projected triangles, not just their vertices against a bounding box. // This test also correctly evaluates collision of a non-convex object with the bounding box. @@ -384,6 +409,11 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i build_volume.max.z() = std::numeric_limits::max(); if (ignore_bottom) build_volume.min.z() = -std::numeric_limits::max(); + // Belt printer: extend Y bounds for infinite Y. + if (m_is_belt_printer && m_belt_infinite_y) { + build_volume.min.y() = -std::numeric_limits::max(); + build_volume.max.y() = std::numeric_limits::max(); + } BoundingBox3Base build_volumef(build_volume.min.cast(), build_volume.max.cast()); // The following test correctly interprets intersection of a non-convex object with a rectangular build volume. //return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z()); diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 6f53fdd704..2b0d8c2746 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -57,6 +57,10 @@ public: // Initialize from PrintConfig::printable_area and PrintConfig::printable_height BuildVolume(const std::vector &printable_area, const double printable_height, const std::vector> &extruder_areas, const std::vector& extruder_printable_heights); + // Belt printer configuration. + void set_belt_printer(bool enabled, double angle_deg, bool infinite_y); + bool is_belt_printer() const { return m_is_belt_printer; } + // Source data, unscaled coordinates. const std::vector& printable_area() const { return m_bed_shape; } double printable_height() const { return m_max_print_height; } @@ -80,7 +84,7 @@ public: indexed_triangle_set bounding_mesh(bool scale=true) const; // Center of the print bed, unscaled. - Vec2d bed_center() const { return to_2d(m_bboxf.center()); } + Vec2d bed_center() const { return get_extents(m_bed_shape).center(); } // Convex hull of polygon(), scaled. const Polygon& convex_hull() const { return m_convex_hull; } // Smallest enclosing circle of polygon(), scaled. @@ -139,6 +143,10 @@ private: // Source definition of the print volume height (PrintConfig::printable_height) double m_max_print_height { 0.f }; std::vector m_extruder_printable_height; + // Belt printer state. + bool m_is_belt_printer { false }; + double m_belt_angle { 0. }; + bool m_belt_infinite_y { false }; // Derived values. BuildVolume_Type m_type { BuildVolume_Type::Invalid }; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 9b67d7ab50..3fd6559bc1 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -79,6 +79,16 @@ set(lisbslic3r_sources BoundingBox.hpp BridgeDetector.cpp BridgeDetector.hpp + BeltGCode.cpp + BeltGCode.hpp + BeltGCodeWriter.cpp + BeltGCodeWriter.hpp + BeltSliceStrategy.cpp + BeltSliceStrategy.hpp + BeltTransform.cpp + BeltTransform.hpp + FirstLayerPlane.cpp + FirstLayerPlane.hpp Brim.cpp BrimEarsPoint.hpp Brim.hpp @@ -209,6 +219,10 @@ set(lisbslic3r_sources GCode/AdaptivePAProcessor.hpp GCode/AvoidCrossingPerimeters.cpp GCode/AvoidCrossingPerimeters.hpp + GCode/BeltBackTransform.cpp + GCode/BeltBackTransform.hpp + GCode/MachineFrameTransform.cpp + GCode/MachineFrameTransform.hpp GCode/ConflictChecker.cpp GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp @@ -416,6 +430,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 ce7ebe892f..b4acca90ad 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -396,6 +396,11 @@ inline void translate(ExPolygons &expolys, const Point &p) { expoly.translate(p); } +inline void translate(Polygons &polys, const Point &p) { + for (Polygon &poly : polys) + poly.translate(p); +} + inline void polygons_append(Polygons &dst, const ExPolygon &src) { dst.reserve(dst.size() + src.holes.size() + 1); diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp new file mode 100644 index 0000000000..6a4feba886 --- /dev/null +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -0,0 +1,225 @@ +#include "FirstLayerPlane.hpp" +#include "BeltTransform.hpp" + +#include +#include +#include + +namespace Slic3r { + +namespace { + +// Build the row of the gcode-axis-remap matrix R that produces machine_Z, +// AS A FUNCTION OF a slicing-frame point in the GCode generator's coordinate +// space. Without back-transform this is just R.row(2). With back-transform +// the writer applies F^-1 before R, so the effective row is (R * F^-1).row(2). +// +// Returns a pair (gradient, constant) such that: +// machine_Z(p_slicing) = gradient.dot(p_slicing) + constant +struct MachineZAffine { + Vec3d gradient = Vec3d::UnitZ(); + double constant = 0.0; +}; + +MachineZAffine compute_machine_z_affine(const PrintConfig &config) +{ + MachineZAffine out; + + // R is the matrix form of GCodeWriter::apply_axis_remap. Each output axis + // i picks one slicing-frame component (with sign + optional Rev mode + // translation) based on m_remap_{x,y,z}. We only need row 2 (the z output) + // since machine_Z is what defines the first-layer plane. + int rz = int(config.gcode_remap_z.value); + int axis = rz % 3; + double sign; + double trans; + if (rz < int(RemapAxis::NegX)) { // 0..2 = PosX/Y/Z + sign = 1.0; + trans = 0.0; + } else if (rz < int(RemapAxis::RevX)) { // 3..5 = NegX/Y/Z + sign = -1.0; + trans = 0.0; + } else { // 6..8 = RevX/Y/Z + sign = -1.0; + BoundingBoxf bbox_bed(config.printable_area.values); + Vec3d vol_max(bbox_bed.max.x(), + bbox_bed.max.y(), + config.printable_height.value); + trans = vol_max[axis]; + } + + Vec3d r_row = Vec3d::Zero(); + r_row[axis] = sign; + + // Without back-transform, machine_Z(slicing) = r_row · slicing + trans. + out.gradient = r_row; + out.constant = trans; + + if (config.gcode_back_transform.value && config.belt_printer.value) { + // BeltGCodeWriter applies F^-1 before R when back-transform is on. + // So machine_Z(slicing) = r_row · (F^-1 · slicing) + trans + // = (r_row^T · F^-1) · slicing + trans + // We need to compose r_row with F^-1 from the LEFT (treating r_row as + // a row vector). Eigen makes this easy: it's just F^-1.transpose() * r_row. + Transform3d forward = BeltTransformPipeline::build_forward_transform(config); + Transform3d inverse = forward.inverse(); + // Note: forward.translation() is normally zero (per-print transforms + // don't add a translation; the per-object z_shift is added separately + // in PrintObjectSlice). We still incorporate inverse.translation() in + // case a Rev-mode preslice_remap puts a translation in F. + Vec3d composed_grad = inverse.linear().transpose() * r_row; + double composed_trans = + r_row.dot(inverse.translation()) + trans; + out.gradient = composed_grad; + out.constant = composed_trans; + } + + return out; +} + +} // namespace + +FirstLayerPlane::FirstLayerPlane(const PrintConfig &config) +{ + // -------- Resolve Auto ------------------------------------------------- + FirstLayerPlaneMode mode = config.first_layer_plane.value; + if (mode == FirstLayerPlaneMode::Auto) { + bool belt_affine_active = config.belt_printer.value && + config.belt_slice_rotation.value != BeltRotationAxis::None && + std::abs(config.belt_slice_rotation_angle.value) > EPSILON; + mode = belt_affine_active ? FirstLayerPlaneMode::BeltAffine + : FirstLayerPlaneMode::XY; + } + m_mode = mode; + + // -------- Band thickness ---------------------------------------------- + // Note: layer_height lives in PrintObjectConfig, not PrintConfig, so we + // can't fall back to it from here. initial_layer_print_height is in + // PrintConfig and is the right default anyway (the legacy first-layer + // semantics used initial_layer_print_height, not the regular one). + double thickness = config.first_layer_plane_thickness.value; + if (thickness <= 0.0) + thickness = config.initial_layer_print_height.value; + if (thickness <= 0.0) + thickness = 0.2; + m_thickness_mm = thickness; + + const double user_offset = config.first_layer_plane_offset.value; + + // -------- Build the plane --------------------------------------------- + auto set_axis_aligned = [&](const Vec3d &n_unit, double offset_along_n) { + m_normal = n_unit; + m_offset = offset_along_n; + }; + + switch (mode) { + case FirstLayerPlaneMode::XY: + // Legacy XY plane. Inactive: short-circuit to layer-index path. + set_axis_aligned(Vec3d::UnitZ(), user_offset); + m_active = false; + return; + + case FirstLayerPlaneMode::YZ: + set_axis_aligned(Vec3d::UnitX(), user_offset); + m_active = true; + return; + + case FirstLayerPlaneMode::XZ: + set_axis_aligned(Vec3d::UnitY(), user_offset); + m_active = true; + return; + + case FirstLayerPlaneMode::BeltAffine: { + // Compute the slicing-frame plane that maps to machine_Z = user_offset + // under the gcode axis remap (and optional back-transform). + MachineZAffine mz = compute_machine_z_affine(config); + double cmag = mz.gradient.norm(); + if (cmag < EPSILON) { + // Degenerate: slicing point doesn't affect machine_Z. Fall back. + set_axis_aligned(Vec3d::UnitZ(), user_offset); + m_active = false; + return; + } + // Plane equation: gradient · slicing = user_offset - constant + const double K = user_offset - mz.constant; + m_normal = mz.gradient / cmag; + m_offset = K / cmag; + m_active = true; + return; + } + + case FirstLayerPlaneMode::Auto: + // Should have been resolved above. + m_active = false; + return; + } + + m_active = false; +} + +double FirstLayerPlane::distance_from_plane(const Vec3d &point_slicing_mm) const +{ + return m_normal.dot(point_slicing_mm) - m_offset; +} + +bool FirstLayerPlane::is_first_layer(const Vec3d &point_slicing_mm, + double first_layer_height_mm) const +{ + if (!m_active) + return false; + return distance_from_plane(point_slicing_mm) < first_layer_height_mm; +} + +int FirstLayerPlane::effective_layer_index(const Vec3d &point_slicing_mm) const +{ + if (!m_active) + return INT_MAX / 2; // Effectively "way past first layer". + double d = distance_from_plane(point_slicing_mm); + if (d <= 0.0) + return 0; + return int(std::floor(d / m_thickness_mm)); +} + +int FirstLayerPlane::min_effective_index_for_xy_bbox( + const BoundingBoxf &xy_bbox_mm, double slicing_z_mm) const +{ + if (!m_active) + return INT_MAX / 2; + // For the rectangular bbox in (x, y) at fixed z, the smallest value of + // (n.x*x + n.y*y + n.z*z - offset) is achieved at one of the four + // corners, with the smaller component picked when the corresponding + // normal coefficient is positive. + const double x_for_min = (m_normal.x() >= 0.0) + ? xy_bbox_mm.min.x() : xy_bbox_mm.max.x(); + const double y_for_min = (m_normal.y() >= 0.0) + ? xy_bbox_mm.min.y() : xy_bbox_mm.max.y(); + const double dmin = m_normal.x() * x_for_min + + m_normal.y() * y_for_min + + m_normal.z() * slicing_z_mm + - m_offset; + if (dmin <= 0.0) + return 0; + return int(std::floor(dmin / m_thickness_mm)); +} + +int FirstLayerPlane::min_effective_index_for_bbox3( + const BoundingBoxf3 &bbox_mm) const +{ + if (!m_active) + return INT_MAX / 2; + const double x_for_min = (m_normal.x() >= 0.0) + ? bbox_mm.min.x() : bbox_mm.max.x(); + const double y_for_min = (m_normal.y() >= 0.0) + ? bbox_mm.min.y() : bbox_mm.max.y(); + const double z_for_min = (m_normal.z() >= 0.0) + ? bbox_mm.min.z() : bbox_mm.max.z(); + const double dmin = m_normal.x() * x_for_min + + m_normal.y() * y_for_min + + m_normal.z() * z_for_min + - m_offset; + if (dmin <= 0.0) + return 0; + return int(std::floor(dmin / m_thickness_mm)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/FirstLayerPlane.hpp b/src/libslic3r/FirstLayerPlane.hpp new file mode 100644 index 0000000000..35b8346c82 --- /dev/null +++ b/src/libslic3r/FirstLayerPlane.hpp @@ -0,0 +1,76 @@ +#ifndef slic3r_FirstLayerPlane_hpp_ +#define slic3r_FirstLayerPlane_hpp_ + +#include "libslic3r.h" +#include "Point.hpp" +#include "BoundingBox.hpp" +#include "PrintConfig.hpp" + +namespace Slic3r { + +// Decides which extrusions get "first layer" treatment (no fan, slow speed, +// initial-layer accel/jerk, deferred temperature drop) by reference to a +// configurable plane in slicing-frame coordinates rather than the slicing +// layer index. +// +// On a normal flat-bed printer the plane is XY at slicing_Z = 0 and the +// evaluator is INACTIVE — every call site short-circuits back to the legacy +// `Layer::id() == 0` test. On a belt printer with a Z-from-Y shear the +// belt surface (machine_Z = 0) maps to a plane in slicing-frame coordinates +// derived from the gcode axis remap, so layer-index-based detection no +// longer matches the physical first printed surface. +// +// Plane representation: unit normal `n` (slicing frame) and offset along +// the normal such that the plane equation is `n · p == offset`. Signed +// perpendicular distance is `d(p) = n · p - offset`. Positive distance +// means "away from the belt surface", negative means "below the plane". +class FirstLayerPlane +{ +public: + explicit FirstLayerPlane(const PrintConfig &config); + + // Inactive when the legacy XY layer-index path should be used. This + // covers all non-belt printers and any belt printer where the user + // explicitly picked XY mode. + bool is_active() const { return m_active; } + FirstLayerPlaneMode effective_mode() const{ return m_mode; } + double band_thickness_mm() const { return m_thickness_mm; } + const Vec3d & normal() const { return m_normal; } + double plane_offset() const { return m_offset; } + + // Signed perpendicular distance from a slicing-frame point to the plane. + double distance_from_plane(const Vec3d &point_slicing_mm) const; + + // True if perpendicular distance < first_layer_height_mm. When the + // evaluator is inactive this returns false (call sites should fall back + // to the legacy per-layer path before reaching this function). + bool is_first_layer(const Vec3d &point_slicing_mm, + double first_layer_height_mm) const; + + // floor((distance - 0) / band_thickness), clamped to [0, +inf). Used + // for "first N layers" thresholds (fan, slow_down_layers). Returns 0 + // for points within the band. Returns INT_MAX/2 when inactive. + int effective_layer_index(const Vec3d &point_slicing_mm) const; + + // Min effective index over a 2D bbox at a fixed slicing_Z. Used for + // layer-level decisions (e.g. temperature transition gate) where we + // don't want to walk every extrusion in the layer. For axis-aligned + // planes this is exact; for tilted planes it's a tight lower bound + // (the plane projection of the bbox's extreme corner). + int min_effective_index_for_xy_bbox(const BoundingBoxf &xy_bbox_mm, + double slicing_z_mm) const; + + // Same as above but the bbox spans a Z range too. + int min_effective_index_for_bbox3(const BoundingBoxf3 &bbox_mm) const; + +private: + bool m_active = false; + FirstLayerPlaneMode m_mode = FirstLayerPlaneMode::XY; + Vec3d m_normal = Vec3d::UnitZ(); // unit, slicing frame + double m_offset = 0.0; // n·p == m_offset + double m_thickness_mm = 0.0; +}; + +} // namespace Slic3r + +#endif // slic3r_FirstLayerPlane_hpp_ diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 3cf54d00dc..77064e4ee2 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -9,6 +9,8 @@ #include "Exception.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" @@ -24,6 +26,7 @@ #include "GCode/ExtrusionProcessor.hpp" #include #include +#include #include #include #include @@ -807,7 +810,7 @@ static std::vector get_path_of_change_filament(const Print& print) //BBS: if needed, write the gcode_label_objects_end then priming tower, if the retract, didn't did it. std::string object_end_label_temp; - gcodegen.m_writer.add_object_end_labels(object_end_label_temp); + gcodegen.m_writer->add_object_end_labels(object_end_label_temp); // Process the custom change_filament_gcode. If it is empty, provide a simple Tn command to change the filament. // Otherwise, leave control to the user completely. @@ -855,7 +858,7 @@ static std::vector get_path_of_change_filament(const Print& print) // config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); // BBS { - GCodeWriter& gcode_writer = gcodegen.m_writer; + GCodeWriter& gcode_writer = *gcodegen.m_writer; FullPrintConfig& full_config = gcodegen.m_config; // set volumetric speed of outer wall ,ignore per obejct,just use default setting @@ -1600,8 +1603,8 @@ static std::vector get_path_of_change_filament(const Print& print) const std::vector ColorPrintColors::Colors = { "#C0392B", "#E67E22", "#F1C40F", "#27AE60", "#1ABC9C", "#2980B9", "#9B59B6" }; -#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->extruder_id()) -#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->id()) +#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->extruder_id()) +#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer->filament()->id()) void GCode::PlaceholderParserIntegration::reset() { @@ -1714,7 +1717,7 @@ 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) +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()); @@ -1772,8 +1775,15 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // 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); } @@ -1804,14 +1814,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; @@ -1846,7 +1874,7 @@ 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; @@ -2044,7 +2072,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << boost::format("Will export G-code to %1% soon")%path; GCodeProcessor::s_IsBBLPrinter = print->is_BBL_printer(); - m_writer.set_is_bbl_machine(print->is_BBL_printer()); + m_writer->set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); // check if any custom gcode contains keywords used by the gcode processor to @@ -2141,7 +2169,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.result().support_traditional_timelapse = m_support_traditional_timelapse; bool activate_long_retraction_when_cut = false; - for (const auto& filament : m_writer.extruders()) + for (const auto& filament : m_writer->extruders()) activate_long_retraction_when_cut |= ( m_config.long_retractions_when_cut.get_at(filament.id()) && m_config.retraction_distances_when_cut.get_at(filament.id()) > 0 @@ -2172,7 +2200,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.finalize(true); // DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics); - DoExport::update_print_estimated_stats(m_processor, m_writer.extruders(), print->m_print_statistics, print->config()); + DoExport::update_print_estimated_stats(m_processor, m_writer->extruders(), print->m_print_statistics, print->config()); if (result != nullptr) { *result = std::move(m_processor.extract_result()); // set the filename to the correct value @@ -2462,7 +2490,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato PROFILE_FUNC(); m_print = &print; - m_timelapse_pos_picker.init(&print,m_writer.get_xy_offset().cast()); + m_timelapse_pos_picker.init(&print,m_writer->get_xy_offset().cast()); // modifies m_silent_time_estimator_enabled DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); @@ -2478,8 +2506,35 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_role_based_fan_marker_layer.fill(-1); m_fan_mover.release(); - - m_writer.set_is_bbl_machine(is_bbl_printers); + + m_writer->set_is_bbl_machine(is_bbl_printers); + + // Belt printer: initialize belt-specific writer via virtual hook. + this->init_belt_writer(print, is_bbl_printers); + + // Standalone axis remap (works with or without belt mode). + { + int rx = int(print.config().gcode_remap_x.value); + int ry = int(print.config().gcode_remap_y.value); + int rz = int(print.config().gcode_remap_z.value); + if (rx != 0 || ry != 1 || rz != 2) { + 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)); + } + } + + // Build the FirstLayerPlane evaluator. When inactive (non-belt printers + // and belt printers without Z shear), all per-path call sites short- + // circuit to the legacy Layer::id() == 0 path so g-code stays bit- + // identical to the pre-feature behavior. + m_first_layer_plane = std::make_unique(print.config()); + if (auto *belt_writer = dynamic_cast(m_writer.get())) { + belt_writer->set_first_layer_plane( + m_first_layer_plane.get(), + print.config().initial_layer_print_height.value); + } // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. @@ -2573,6 +2628,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); + // Belt printer: embed angle and transform configs in header via virtual hook. + this->write_belt_header(file, print); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -2819,13 +2876,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Disable fan. if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) { - file.write(m_writer.set_fan(0)); + file.write(m_writer->set_fan(0)); //BBS: disable additional fan - file.write(m_writer.set_additional_fan(0)); + file.write(m_writer->set_additional_fan(0)); } // Update output variables after the extruders were initialized. - m_placeholder_parser_integration.init(m_writer); + m_placeholder_parser_integration.init(*m_writer); // Let the start-up script prime the 1st printing tool. @@ -2904,7 +2961,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { BoundingBoxf bbox_bed(print.config().printable_area.values); - Vec2f plate_offset = m_writer.get_xy_offset(); + Vec2f plate_offset = m_writer->get_xy_offset(); this->placeholder_parser().set("print_bed_min", new ConfigOptionFloats({ bbox_bed.min.x(), bbox_bed.min.y()})); this->placeholder_parser().set("print_bed_max", new ConfigOptionFloats({ bbox_bed.max.x(), bbox_bed.max.y()})); this->placeholder_parser().set("print_bed_size", new ConfigOptionFloats({ bbox_bed.size().x(), bbox_bed.size().y() })); @@ -3006,7 +3063,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } bool activate_chamber_temp_control = false; auto max_chamber_temp = 0; - for (const auto &extruder : m_writer.extruders()) { + for (const auto &extruder : m_writer->extruders()) { activate_chamber_temp_control |= m_config.activate_chamber_temp_control.get_at(extruder.id()); max_chamber_temp = std::max(max_chamber_temp, m_config.chamber_temperature.get_at(extruder.id())); } @@ -3014,7 +3071,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato BedType curr_bed_type = m_config.curr_bed_type; int min_temperature_vitrification = std::numeric_limits::max(); - for (const auto& extruder : m_writer.extruders()) + for (const auto& extruder : m_writer->extruders()) min_temperature_vitrification = std::min(min_temperature_vitrification, m_config.temperature_vitrification.get_at(extruder.id())); @@ -3128,18 +3185,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (activate_chamber_temp_control && max_chamber_temp > 0){ int temp_out =0; if(!custom_gcode_sets_temperature(machine_start_gcode,141,191,false,temp_out)) - file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature + file.write(m_writer->set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature } // Write the custom start G-code file.writeln(machine_start_gcode); //BBS: gcode writer doesn't know where the real position of extruder is after inserting custom gcode - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); m_start_gcode_filament = GCodeProcessor::get_gcode_last_filament(machine_start_gcode); if (is_bbl_printers) { - m_writer.init_extruder(initial_non_support_extruder_id); + m_writer->init_extruder(initial_non_support_extruder_id); // add the missing filament start gcode in machine start gcode { DynamicConfig config; @@ -3152,7 +3209,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } // Orca: add missing PA settings for initial filament if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) { - file.write(m_writer.set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); + file.write(m_writer->set_pressure_advance(m_config.pressure_advance.get_at(initial_non_support_extruder_id))); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(initial_non_support_extruder_id)); @@ -3182,7 +3239,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato int during_print_exhaust_fan_speed = 0; // Orca: when activate_air_filtration is set on any extruder, find and set the highest during_print_exhaust_fan_speed - for (const auto &extruder : m_writer.extruders()) { + for (const auto &extruder : m_writer->extruders()) { if (m_config.activate_air_filtration.get_at(extruder.id()) && m_config.activate_air_filtration_during_print.get_at(extruder.id())) { activate_air_filtration_during_print = true; during_print_exhaust_fan_speed = std::max(during_print_exhaust_fan_speed, @@ -3191,7 +3248,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } if (activate_air_filtration_during_print) - file.write(m_writer.set_exhaust_fan(during_print_exhaust_fan_speed)); + file.write(m_writer->set_exhaust_fan(during_print_exhaust_fan_speed)); } print.throw_if_canceled(); @@ -3208,7 +3265,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_seam_placer.init(print, throw_if_canceled_func); // BBS: get path for change filament - if (m_writer.multiple_extruders) { + if (m_writer->multiple_extruders) { std::vector points = get_path_of_change_filament(print); if (points.size() == 3) { travel_point_1 = points[0]; @@ -3245,12 +3302,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato std::string gcode; gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; if ((print.default_object_config().outer_wall_acceleration.value > 0 && print.default_object_config().outer_wall_acceleration.value > 0)) { - gcode += m_writer.set_print_acceleration((unsigned int)floor(print.default_object_config().outer_wall_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int)floor(print.default_object_config().outer_wall_acceleration.value + 0.5)); } if (print.default_object_config().outer_wall_jerk.value > 0) { double jerk = print.default_object_config().outer_wall_jerk.value; - gcode += m_writer.set_jerk_xy(jerk); + gcode += m_writer->set_jerk_xy(jerk); } auto params = print.calib_params(); @@ -3264,7 +3321,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. + BeltGCodeWriter* belt_writer = dynamic_cast(m_writer.get()); + if (belt_writer != nullptr) + belt_writer->set_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_writer != nullptr) + belt_writer->set_world_coordinates(false); file.write(gcode); } else { @@ -3294,6 +3360,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; @@ -3303,7 +3371,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once(); // BBS. change tool before moving to origin point. - if (m_writer.need_toolchange(initial_extruder_id)) { + if (m_writer->need_toolchange(initial_extruder_id)) { const PrintObjectConfig& object_config = object.config(); coordf_t initial_layer_print_height = print.config().initial_layer_print_height.value; @@ -3316,7 +3384,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } else { file.write(this->retract()); } - file.write(m_writer.travel_to_z(m_max_layer_z + m_writer.config.z_hop.get_at(initial_extruder_id))); + file.write(m_writer->travel_to_z(m_max_layer_z + m_writer->config.z_hop.get_at(initial_extruder_id))); file.write(this->travel_to(Point(0, 0), erNone, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. @@ -3339,7 +3407,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // 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 @@ -3356,7 +3424,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { const auto plr_mode = print.config().enable_power_loss_recovery.value; if (m_second_layer_things_done && plr_mode == PowerLossRecoveryMode::Enable) { - file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } } ++ finished_objects; @@ -3377,7 +3445,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_wipe_tower->set_wipe_tower_bbx(print.get_wipe_tower_bbx()); m_wipe_tower->set_rib_offset(print.get_rib_offset()); //BBS - file.write(m_writer.travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); + file.write(m_writer->travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); if (wipe_tower_type == WipeTowerType::Type2 && print.config().single_extruder_multi_material_priming) { file.write(m_wipe_tower->prime(*this)); @@ -3436,7 +3504,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Orca: disable power loss recovery if (m_second_layer_things_done && print.config().enable_power_loss_recovery.value == PowerLossRecoveryMode::Enable) { - file.write(m_writer.enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); + file.write(m_writer->enable_power_loss_recovery(PowerLossRecoveryMode::Disable)); } if (m_wipe_tower) // Purge the extruder, pull out the active filament. @@ -3450,14 +3518,14 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // if needed, write the gcode_label_objects_end { std::string gcode; - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); file.write(gcode); } - file.write(m_writer.set_fan(0)); + file.write(m_writer->set_fan(0)); //BBS: make sure the additional fan is closed when end if(m_config.auxiliary_fan.value) - file.write(m_writer.set_additional_fan(0)); + file.write(m_writer->set_additional_fan(0)); if (is_bbl_printers) { //BBS: close spaghetti detector //Note: M981 is also used to tell xcam the last layer is finished, so we need always send it even if spaghetti option is disabled. @@ -3473,12 +3541,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); //BBS - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position()(2) - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position()(2) - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); if (print.config().single_extruder_multi_material) { // Process the filament_end_gcode for the active filament only. - int extruder_id = m_writer.filament()->id(); + int extruder_id = m_writer->filament()->id(); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); file.writeln(this->placeholder_parser_process("filament_end_gcode", print.config().filament_end_gcode.get_at(extruder_id), extruder_id, &config)); } else { @@ -3488,13 +3556,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.writeln(this->placeholder_parser_process("filament_end_gcode", end_gcode, extruder_id, &config)); } } - file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer.filament()->id(), &config)); + file.writeln(this->placeholder_parser_process("machine_end_gcode", print.config().machine_end_gcode, m_writer->filament()->id(), &config)); } - file.write(m_writer.update_progress(m_layer_count, m_layer_count, true)); // 100% - file.write(m_writer.postamble()); + file.write(m_writer->update_progress(m_layer_count, m_layer_count, true)); // 100% + file.write(m_writer->postamble()); if (activate_chamber_temp_control && max_chamber_temp > 0) - file.write(m_writer.set_chamber_temperature(0, false)); //close chamber_temperature + file.write(m_writer->set_chamber_temperature(0, false)); //close chamber_temperature // Orca: when air filtration is supported, check if it needs to be activated after print completion and set the exhaust fan speed accordingly if (m_config.support_air_filtration.value) { @@ -3502,7 +3570,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato int complete_print_exhaust_fan_speed = 0; // Orca: when activate_air_filtration is set on any extruder, find and set the highest complete_print_exhaust_fan_speed - for (const auto& extruder : m_writer.extruders()) { + for (const auto& extruder : m_writer->extruders()) { if (m_config.activate_air_filtration.get_at(extruder.id()) && m_config.activate_air_filtration_on_completion.get_at(extruder.id())) { activate_air_filtration_on_completion = true; complete_print_exhaust_fan_speed = std::max(complete_print_exhaust_fan_speed, m_config.complete_print_exhaust_fan_speed.get_at(extruder.id())); @@ -3510,7 +3578,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } if (activate_air_filtration_on_completion) - file.write(m_writer.set_exhaust_fan(complete_print_exhaust_fan_speed)); + file.write(m_writer->set_exhaust_fan(complete_print_exhaust_fan_speed)); } // adds tags for time estimators @@ -3523,7 +3591,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(DoExport::update_print_stats_and_format_filament_stats( // Const inputs has_wipe_tower, print.wipe_tower_data(), - m_writer.extruders(), + m_writer->extruders(), // Modifies print.m_print_statistics, // Const input (tool-change fallback for non-wipe-tower prints) @@ -3723,7 +3791,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { CNumericLocalesSetter locales_setter; @@ -3823,7 +3891,7 @@ void GCode::process_layers( ); const auto fan_mover = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = this->m_writer](std::string in)->std::string { + [&fan_mover = this->m_fan_mover, &config = this->config(), &writer = *this->m_writer](std::string in)->std::string { if (config.fan_speedup_time.value != 0 || config.fan_kickstart.value > 0) { if (fan_mover.get() == nullptr) @@ -3891,21 +3959,21 @@ std::string GCode::placeholder_parser_process(const std::string &name, const std PlaceholderParserIntegration &ppi = m_placeholder_parser_integration; try { - ppi.update_from_gcodewriter(m_writer); + ppi.update_from_gcodewriter(*m_writer); std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context); ppi.validate_output_vector_variables(); if (const std::vector &pos = ppi.opt_position->values; ppi.position != pos) { // Update G-code writer. - m_writer.set_position({ pos[0], pos[1], pos[2] }); + m_writer->set_position({ pos[0], pos[1], pos[2] }); this->set_last_pos(this->gcode_to_point({ pos[0], pos[1] })); } - for (const Extruder &e : m_writer.extruders()) { + for (const Extruder &e : m_writer->extruders()) { unsigned int eid = e.id(); assert(eid < ppi.num_extruders); if ( eid < ppi.num_extruders) { - if (! m_writer.config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) + if (! m_writer->config.use_relative_e_distances && ! is_approx(ppi.e_position[eid], ppi.opt_e_position->values[eid])) const_cast(e).set_position(ppi.opt_e_position->values[eid]); if (! is_approx(ppi.e_retracted[eid], ppi.opt_e_retracted->values[eid]) || ! is_approx(ppi.e_restart_extra[eid], ppi.opt_e_restart_extra->values[eid])) @@ -4041,9 +4109,9 @@ void GCode::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &p temp = temp_by_gcode; #endif - // Always call m_writer.set_bed_temperature() so it will set the internal "current" state of the bed temp as if + // Always call m_writer->set_bed_temperature() so it will set the internal "current" state of the bed temp as if // the custom start G-code emited these. - std::string set_temp_gcode = m_writer.set_bed_temperature(bed_temp, wait); + std::string set_temp_gcode = m_writer->set_bed_temperature(bed_temp, wait); if (! temp_set_by_gcode) file.write(set_temp_gcode); } @@ -4063,14 +4131,14 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr int temp = print.config().nozzle_temperature_initial_layer.get_at(first_printing_extruder_id); if (temp_by_gcode >= 0 && temp_by_gcode < 1000) temp = temp_by_gcode; - m_writer.set_temperature(temp, wait, first_printing_extruder_id); + m_writer->set_temperature(temp, wait, first_printing_extruder_id); } else { // Custom G-code does not set the extruder temperature. Do it now. if (print.config().single_extruder_multi_material.value) { // Set temperature of the first printing extruder only. int temp = print.config().nozzle_temperature_initial_layer.get_at(first_printing_extruder_id); if (temp > 0) - file.write(m_writer.set_temperature(temp, wait, first_printing_extruder_id)); + file.write(m_writer->set_temperature(temp, wait, first_printing_extruder_id)); } else { // Set temperatures of all the printing extruders. for (unsigned int tool_id : print.extruders()) { @@ -4082,7 +4150,7 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr temp = print.config().idle_temperature.get_at(tool_id); } if (temp > 0) - file.write(m_writer.set_temperature(temp, wait, tool_id)); + file.write(m_writer->set_temperature(temp, wait, tool_id)); } } } @@ -4585,7 +4653,7 @@ LayerResult GCode::process_layer( //support is attached above the object, and support layers has independent layer height, then the lowest support //interface layer id is 0. bool first_layer = (layer.id() == 0 && abs(layer.bottom_z()) < EPSILON); - m_writer.set_is_first_layer(first_layer); + m_writer->set_is_first_layer(first_layer); unsigned int first_extruder_id = layer_tools.extruders.front(); // Initialize config with the 1st object to be printed at this layer. @@ -4635,7 +4703,7 @@ LayerResult GCode::process_layer( 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) + print.config().before_layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -4668,7 +4736,7 @@ LayerResult GCode::process_layer( 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("timelapse_gcode", - print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; } @@ -4678,7 +4746,7 @@ LayerResult GCode::process_layer( config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); gcode += this->placeholder_parser_process("layer_change_gcode", - print.config().layer_change_gcode.value, m_writer.filament()->id(), &config) + print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); } @@ -4686,38 +4754,64 @@ LayerResult GCode::process_layer( gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n"; //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(print.calib_params().start + static_cast(print_z) * print.calib_params().step); + gcode += writer().set_pressure_advance(print.calib_params().start + static_cast(std::max(0.0, calib_z)) * print.calib_params().step); 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: { - auto _speed = print.calib_params().start + std::floor(print_z / 5.0) * print.calib_params().step; + auto _speed = print.calib_params().start + std::floor(std::max(0.0, calib_z) / 5.0) * print.calib_params().step; m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloat(std::round(_speed))); 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 ConfigOptionFloat(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; } case CalibMode::Calib_Input_shaping_freq: { if (m_layer_index == 1){ gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type); - if (m_writer.get_gcode_flavor() == gcfKlipper) { + if (m_writer->get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -4733,7 +4827,7 @@ LayerResult GCode::process_layer( } case CalibMode::Calib_Input_shaping_damp: { if (m_layer_index == 1){ - if (m_writer.get_gcode_flavor() == gcfKlipper) { + if (m_writer->get_gcode_flavor() == gcfKlipper) { // Disable minimum cruise ratio to ensure consistent motion for calibration gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n"; } @@ -4745,7 +4839,7 @@ LayerResult GCode::process_layer( break; } case CalibMode::Calib_Cornering: { - if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && + if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && !m_config.machine_max_junction_deviation.values.empty() && m_config.machine_max_junction_deviation.values.front() > 0) { gcode += writer().set_junction_deviation(this->interpolate_value_across_layers(print.calib_params().start, print.calib_params().end)); @@ -4760,22 +4854,53 @@ LayerResult GCode::process_layer( if (first_layer) { // Orca: we don't need to optimize the Klipper as only set once if (m_config.default_acceleration.value > 0 && m_config.initial_layer_acceleration.value > 0) { - gcode += m_writer.set_print_acceleration((unsigned int)floor(m_config.initial_layer_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int)floor(m_config.initial_layer_acceleration.value + 0.5)); } if (m_config.default_jerk.value > 0 && m_config.initial_layer_jerk.value > 0) { - gcode += m_writer.set_jerk_xy(m_config.initial_layer_jerk.value); + gcode += m_writer->set_jerk_xy(m_config.initial_layer_jerk.value); } - if (m_writer.get_gcode_flavor() == gcfMarlinFirmware && m_config.default_junction_deviation.value > 0) { - gcode += m_writer.set_junction_deviation(m_config.default_junction_deviation.value); + if (m_writer->get_gcode_flavor() == gcfMarlinFirmware && m_config.default_junction_deviation.value > 0) { + gcode += m_writer->set_junction_deviation(m_config.default_junction_deviation.value); } } - if (!first_layer && !m_second_layer_things_done) { + // First-layer plane: defer the temperature/PLR transition until the + // entire layer is past the first-layer band. When the evaluator is + // inactive (non-belt printers and belt printers without Z shear) we + // fall back to the legacy `!first_layer` predicate so behavior is + // bit-identical to the pre-feature path. + bool past_first_layer_band = !first_layer; + if (m_first_layer_plane && m_first_layer_plane->is_active()) { + past_first_layer_band = false; + if (object_layer != nullptr) { + // Conservatively walk the layer's lslice bboxes; if every bbox's + // most-belt-side corner is outside the first-layer band, the + // layer is fully past it. + const Layer *ol = object_layer; + int min_eff = INT_MAX; + for (const BoundingBox &bb : ol->lslices_bboxes) { + BoundingBoxf bbf( + Vec2d(unscale(bb.min.x()), unscale(bb.min.y())), + Vec2d(unscale(bb.max.x()), unscale(bb.max.y()))); + int eff = m_first_layer_plane->min_effective_index_for_xy_bbox( + bbf, ol->print_z); + if (eff < min_eff) min_eff = eff; + if (min_eff <= 0) break; + } + past_first_layer_band = (min_eff > 0 && min_eff != INT_MAX); + } else if (support_layer != nullptr) { + // Support-only layers: gate on the support layer's bottom_z + // proximity to the plane. Conservative. + past_first_layer_band = !first_layer; + } + } + + 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); + gcode += m_writer->enable_power_loss_recovery(plr_mode); if (print.is_BBL_printer()) { // BBS: open first layer inspection at second layer @@ -4790,23 +4915,23 @@ LayerResult GCode::process_layer( // Reset acceleration at sencond layer // Orca: only set once, don't need to call set_accel_and_jerk if (m_config.default_acceleration.value > 0 && m_config.initial_layer_acceleration.value > 0) { - gcode += m_writer.set_print_acceleration((unsigned int) floor(m_config.default_acceleration.value + 0.5)); + gcode += m_writer->set_print_acceleration((unsigned int) floor(m_config.default_acceleration.value + 0.5)); } if (m_config.default_jerk.value > 0 && m_config.initial_layer_jerk.value > 0) { - gcode += m_writer.set_jerk_xy(m_config.default_jerk.value); + gcode += m_writer->set_jerk_xy(m_config.default_jerk.value); } // Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent // nozzle_temperature_initial_layer vs. temperature settings. - for (const Extruder& extruder : m_writer.extruders()) { + for (const Extruder& extruder : m_writer->extruders()) { if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) && - extruder.id() != m_writer.filament()->id()) + extruder.id() != m_writer->filament()->id()) // In single extruder multi material mode, set the temperature for the current extruder only. continue; int temperature = print.config().nozzle_temperature.get_at(extruder.id()); if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(extruder.id())) - gcode += m_writer.set_temperature(temperature, false, extruder.id()); + gcode += m_writer->set_temperature(temperature, false, extruder.id()); } // BBS @@ -4815,14 +4940,14 @@ LayerResult GCode::process_layer( bed_temp = get_highest_bed_temperature(false,print); else bed_temp = get_bed_temperature(first_extruder_id, false, m_config.curr_bed_type); - gcode += m_writer.set_bed_temperature(bed_temp); + gcode += m_writer->set_bed_temperature(bed_temp); // Mark the temperature transition from 1st to 2nd layer to be finished. m_second_layer_things_done = true; } if (single_object_instance_idx == size_t(-1)) { // Normal (non-sequential) print. - gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer.filament()->id(), first_extruder_id, print.config()); + gcode += ProcessLayer::emit_custom_gcode_per_print_z(*this, layer_tools.custom_gcode, m_writer->filament()->id(), first_extruder_id, print.config()); } // BBS: get next extruder according to flush and soluble @@ -5142,9 +5267,9 @@ LayerResult GCode::process_layer( auto insert_timelapse_gcode = [this, print_z, &print, &most_used_extruder, &layer_object_label_ids,&printed_objects = std::as_const(m_printed_objects)]() -> std::string { PosPickCtx ctx; - ctx.curr_pos = { (coord_t)(scale_(m_writer.get_position().x())),(coord_t)(scale_(m_writer.get_position().y())) }; + ctx.curr_pos = { (coord_t)(scale_(m_writer->get_position().x())),(coord_t)(scale_(m_writer->get_position().y())) }; ctx.curr_layer = this->layer(); - ctx.curr_extruder_id = m_writer.filament()->extruder_id(); + ctx.curr_extruder_id = m_writer->filament()->extruder_id(); ctx.picture_extruder_id = most_used_extruder; if (m_config.print_sequence == PrintSequence::ByObject) ctx.printed_objects = printed_objects; @@ -5162,17 +5287,17 @@ LayerResult GCode::process_layer( config.set_key_value("timelapse_pos_x", new ConfigOptionInt((int)timelapse_pos.x())); config.set_key_value("timelapse_pos_y", new ConfigOptionInt((int)timelapse_pos.y())); config.set_key_value("has_timelapse_safe_pos", new ConfigOptionBool(timelapse_pos != DefaultTimelapsePos)); - timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + "\n"; + timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer->filament()->id(), &config) + "\n"; } if (!timelapse_gcode.empty()) { - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(timelapse_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } } @@ -5218,16 +5343,16 @@ LayerResult GCode::process_layer( config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("most_used_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(most_used_extruder))); - config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer.filament()->extruder_id()))); - wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer.filament()->id(), &config) +"\n"; + config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(m_writer->filament()->extruder_id()))); + wrapping_gcode = this->placeholder_parser_process("wrapping_detection_gcode", print.config().wrapping_detection_gcode.value, m_writer->filament()->id(), &config) +"\n"; } - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(wrapping_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } return wrapping_gcode; }; @@ -5269,7 +5394,7 @@ LayerResult GCode::process_layer( if (should_insert) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -5286,12 +5411,12 @@ LayerResult GCode::process_layer( } else { if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode && - m_writer.need_toolchange(extruder_id) && + m_writer->need_toolchange(extruder_id) && m_config.nozzle_diameter.values.size() == 2 && writer().filament() && (get_extruder_id(writer().filament()->id()) == most_used_extruder)) { gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); has_insert_timelapse_gcode = true; @@ -5351,19 +5476,19 @@ LayerResult GCode::process_layer( // exclude objects if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_start_str( + m_writer->set_object_start_str( std::string("; start printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " + _encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + + m_writer->set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { std::string str = std::string("M486 S") + std::to_string(inst.unique_id) + "\n"; - m_writer.set_object_start_str(str); + m_writer->set_object_start_str(str); } } } @@ -5381,6 +5506,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 (instance_to_print.object_by_extruder.support != nullptr) { m_layer = layers[instance_to_print.layer_id].support_layer; m_object_layer_over_raft = false; @@ -5404,6 +5530,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 @@ -5438,6 +5565,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) { @@ -5474,20 +5602,20 @@ LayerResult GCode::process_layer( } // exclude objects // Don't set m_gcode_label_objects_end if you don't had to write the m_gcode_label_objects_start. - if (!m_writer.is_object_start_str_empty()) { - m_writer.set_object_start_str(""); + if (!m_writer->is_object_start_str_empty()) { + m_writer->set_object_start_str(""); } else if (m_enable_exclude_object) { if (is_BBL_Printer()) { - m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") + + m_writer->set_object_end_str(std::string("; stop printing object, unique label id: ") + std::to_string(instance_to_print.label_object_id) + "\n" + "M625\n"); } else { const auto gflavor = print.config().gcode_flavor.value; if (gflavor == gcfKlipper) { - m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + + m_writer->set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { - m_writer.set_object_end_str(std::string("M486 S-1\n")); + m_writer->set_object_end_str(std::string("M486 S-1\n")); } } } @@ -5539,7 +5667,7 @@ LayerResult GCode::process_layer( if (FILAMENT_CONFIG(retract_when_changing_layer)) { gcode += this->retract(false, false, auto_lift_type, true); } - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); gcode += insert_timelapse_gcode(); } @@ -5551,7 +5679,7 @@ LayerResult GCode::process_layer( void GCode::apply_print_config(const PrintConfig &print_config) { - m_writer.apply_print_config(print_config); + m_writer->apply_print_config(print_config); m_config.apply(print_config); m_scaled_resolution = scaled(print_config.resolution.value); m_enable_exclude_object = m_config.exclude_object; @@ -5643,7 +5771,7 @@ void GCode::append_full_config(const Print &print, std::string &str) void GCode::set_extruders(const std::vector &extruder_ids) { - m_writer.set_extruders(extruder_ids); + m_writer->set_extruders(extruder_ids); // enable wipe path generation if any extruder has wipe enabled m_wipe.enable = false; @@ -5668,13 +5796,13 @@ void GCode::set_origin(const Vec2d &pointf) std::string GCode::preamble() { - std::string gcode = m_writer.preamble(); + std::string gcode = m_writer->preamble(); /* Perform a *silent* move to z_offset: we need this to initialize the Z position of our writer object so that any initial lift taking place before the first layer change will raise the extruder from the correct initial Z instead of 0. */ - m_writer.travel_to_z(m_config.z_offset.value); + m_writer->travel_to_z(m_config.z_offset.value); return gcode; } @@ -5685,28 +5813,28 @@ std::string GCode::change_layer(coordf_t print_z) std::string gcode; if (m_layer_count > 0) // Increment a progress bar indicator. - gcode += m_writer.update_progress(++ m_layer_index, m_layer_count); + gcode += m_writer->update_progress(++ m_layer_index, m_layer_count); //BBS coordf_t z = print_z + m_config.z_offset.value; // in unscaled coordinates - if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer.will_move_z(z)) { + if (FILAMENT_CONFIG(retract_when_changing_layer) && m_writer->will_move_z(z)) { LiftType lift_type = this->to_lift_type(ZHopType(FILAMENT_CONFIG(z_hop_types))); //BBS: force to use SpiralLift when change layer if lift type is auto gcode += this->retract(false, false, ZHopType(FILAMENT_CONFIG(z_hop_types)) == ZHopType::zhtAuto ? LiftType::SpiralLift : lift_type); } - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); if (m_spiral_vase) { //BBS: force to normal lift immediately in spiral vase mode std::ostringstream comment; comment << "move to next layer (" << m_layer_index << ")"; - gcode += m_writer.travel_to_z(z, comment.str()); + gcode += m_writer->travel_to_z(z, comment.str()); } m_need_change_layer_lift_z = true; m_nominal_z = z; - m_writer.get_position().z() = z; + m_writer->get_position().z() = z; // forget last wiping path as wiping after raising Z is pointless // BBS. Dont forget wiping path to reduce stringing. @@ -6027,7 +6155,7 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref, const Point3 ¢er3 = paths.front().polyline.points.front(); pt.rotate(angle, Point(center3.x(), center3.y())); // generate the travel move - gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0, "move inwards before travel", true); + gcode += m_writer->extrude_to_xy(this->point_to_gcode(pt), 0, "move inwards before travel", true); } return gcode; @@ -6347,6 +6475,18 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (is_bridge(path.role())) description += " (bridge)"; + // 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. + const Vec3d path_point_mm{ + unscale(path.first_point().x()), + unscale(path.first_point().y()), + m_layer ? m_layer->print_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) { @@ -6357,7 +6497,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool slope_need_z_travel = false; if (sloped != nullptr && !sloped->is_flat()) { auto target_z = get_sloped_z(sloped->slope_begin.z_ratio); - slope_need_z_travel = m_writer.will_move_z(target_z); + slope_need_z_travel = m_writer->will_move_z(target_z); } // Move to first point of extrusion path // path is 2D. But in slope lift case, lift z is done in travel_to function. @@ -6385,14 +6525,14 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } if (path.z_contoured && !path.polyline.lines().empty()) { - double current_z = m_writer.get_position().z(); + double current_z = m_writer->get_position().z(); double first_z = unscale_(path.polyline.lines().begin()->a.z()) + m_nominal_z; if (GCodeFormatter::quantize_xyzf(first_z) != GCodeFormatter::quantize_xyzf(current_z)) { - gcode += m_writer.travel_to_z(first_z, "set Z for contouring", true); + gcode += m_writer->travel_to_z(first_z, "set Z for contouring", true); } } if (!path.z_contoured && sloped == nullptr) { - double current_z = m_writer.get_position().z(); + double current_z = m_writer->get_position().z(); if (GCodeFormatter::quantize_xyzf(current_z) != GCodeFormatter::quantize_xyzf(m_nominal_z)) { gcode += this->writer().travel_to_z(m_nominal_z, "reset Z after contouring", true); } @@ -6400,7 +6540,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // if needed, write the gcode_label_objects_end then gcode_label_objects_start // should be already done by travel_to, but just in case - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); // compensate retraction gcode += this->unretract(); @@ -6412,7 +6552,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // adjust acceleration if (m_config.default_acceleration.value > 0) { double acceleration; - if (this->on_first_layer() && m_config.initial_layer_acceleration.value > 0) { + if (path_on_first_layer && m_config.initial_layer_acceleration.value > 0) { acceleration = m_config.initial_layer_acceleration.value; #if 0 } else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) { @@ -6438,7 +6578,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // adjust X Y jerk if (m_config.default_jerk.value > 0) { - if (this->on_first_layer() && m_config.initial_layer_jerk.value > 0) { + if (path_on_first_layer && m_config.initial_layer_jerk.value > 0) { jerk = m_config.initial_layer_jerk.value; } else if (m_config.outer_wall_jerk.value > 0 && is_external_perimeter(path.role())) { jerk = m_config.outer_wall_jerk.value; @@ -6454,12 +6594,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } } - if (m_writer.get_gcode_flavor() == gcfKlipper) { - gcode += m_writer.set_accel_and_jerk(acceleration_i, jerk); + if (m_writer->get_gcode_flavor() == gcfKlipper) { + gcode += m_writer->set_accel_and_jerk(acceleration_i, jerk); } else { - gcode += m_writer.set_print_acceleration(acceleration_i); - gcode += m_writer.set_jerk_xy(jerk); + gcode += m_writer->set_print_acceleration(acceleration_i); + gcode += m_writer->set_jerk_xy(jerk); } // calculate effective extrusion length per distance unit (e_per_mm) @@ -6468,6 +6608,20 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, auto _mm3_per_mm = path.mm3_per_mm * this->config().print_flow_ratio; _mm3_per_mm *= filament_flow_ratio; + // Belt printer: compensate for any volume change introduced by the mesh + // forward transform. path.mm3_per_mm is derived from slicer-frame layer + // height × line width, but a slicer-frame slab of volume V maps under the + // back-transform to a machine-frame region of volume V / |det(T)|. The + // mesh transform is now rotation ∘ pre-remap, both orthogonal, so |det(T)| + // is always 1 and this is currently a no-op; it is retained as a guard in + // case a non-orthogonal mesh transform is ever reintroduced. (Machine-frame + // shear/scale acts on the g-code in BeltGCodeWriter, not here.) + if (m_config.belt_printer.value) { + double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant()); + if (det > EPSILON) + _mm3_per_mm /= det; + } + if (path.role() == erTopSolidInfill) { _mm3_per_mm *= m_config.top_solid_infill_flow_ratio; } else if (path.role() == erBottomSurface) { @@ -6500,14 +6654,14 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // 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; } } // Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio - // m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) - double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm; + // m_writer->extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) + double e_per_mm = m_writer->filament()->e_per_mm3() * _mm3_per_mm; e_per_mm /= filament_flow_ratio; // set speed @@ -6558,9 +6712,11 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, 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 FirstLayerPlane-aware effective layer index when active 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); + 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) { @@ -6568,7 +6724,6 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, m_config.get_abs_value("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()) @@ -6652,7 +6807,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool variable_speed = false; std::vector new_points {}; - if (m_config.enable_overhang_speed && !this->on_first_layer() && !object_layer_over_raft() && + if (m_config.enable_overhang_speed && !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 ? m_config.get_abs_value("outer_wall_speed") : m_config.get_abs_value("inner_wall_speed"); @@ -6741,7 +6896,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, //Orca: process custom gcode for extrusion role change if (path.role() != m_last_extrusion_role) { - const auto current_filament_id = m_writer.filament()->id(); + const auto current_filament_id = m_writer->filament()->id(); const std::string& machine_role_change_gcode = m_config.change_extrusion_role_gcode.value; const std::string& filament_role_change_gcode = m_config.filament_change_extrusion_role_gcode.get_at(current_filament_id); const std::string& process_role_change_gcode = m_config.process_change_extrusion_role_gcode.value; @@ -6816,7 +6971,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (m_multi_flow_segment_path_average_mm3_per_mm > 0) { sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), m_multi_flow_segment_path_average_mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6829,7 +6984,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // to issue a zero flow PA change command for this sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6939,7 +7094,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6954,7 +7109,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -6969,7 +7124,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // ORCA: End of adaptive PA code segment } - gcode += m_writer.set_speed(F, "", comment); + gcode += m_writer->set_speed(F, "", comment); { if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { @@ -6987,10 +7142,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } // 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(); + double saved_z = m_writer->get_position().z(); for (const Line3& line : path.polyline.lines()) { std::string tempDescription = description; @@ -7023,12 +7178,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (z < 0.1) { throw RuntimeError("GCode: very low z"); } - gcode += m_writer.extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, + gcode += m_writer->extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy( + gcode += m_writer->extrude_to_xy( this->point_to_gcode(line.b.to_point()), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -7037,7 +7192,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec2d dest2d = this->point_to_gcode(line.b.to_point()); Vec3d dest3d(dest2d(0), dest2d(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz( + gcode += m_writer->extrude_to_xyz( dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -7067,7 +7222,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } - gcode += m_writer.extrude_to_xy( + gcode += m_writer->extrude_to_xy( this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); @@ -7090,7 +7245,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, arc_length); } } - gcode += m_writer.extrude_arc_to_xy( + gcode += m_writer->extrude_arc_to_xy( this->point_to_gcode(arc.end_point), center_offset, dE, @@ -7118,8 +7273,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, Polyline3 l(p); total_length = l.length() * SCALING_FACTOR; } - gcode += m_writer.set_speed(last_set_speed, "", comment); - Vec3d prev = this->point_to_gcode_quantized(new_points[0].p); + gcode += m_writer->set_speed(last_set_speed, "", comment); + Vec3d prev = this->point_to_gcode_quantized(new_points[0].p); bool pre_fan_enabled = false; bool cur_fan_enabled = false; if( m_enable_cooling_markers && enable_overhang_bridge_fan) @@ -7168,7 +7323,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -7183,7 +7338,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } sprintf(buf, ";%sT%u MM3MM:%g ACCEL:%u BR:%d RC:%d OV:%d\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::PA_Change).c_str(), - m_writer.filament()->id(), + m_writer->filament()->id(), _mm3_per_mm, acceleration_i, ((path.role() == erBridgeInfill) ||(path.role() == erOverhangPerimeter)), @@ -7200,10 +7355,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // Ignore small speed variations - emit speed change if the delta between current and new is greater than 60mm/min / 1mm/sec // Reset speed to F if delta to F is less than 1mm/sec if ((std::abs(last_set_speed - new_speed) > 60)) { - gcode += m_writer.set_speed(new_speed, "", comment); + gcode += m_writer->set_speed(new_speed, "", comment); last_set_speed = new_speed; } else if ((std::abs(F - new_speed) <= 60)) { - gcode += m_writer.set_speed(F, "", comment); + gcode += m_writer->set_speed(F, "", comment); last_set_speed = F; } auto dE = e_per_mm * line_length; @@ -7230,16 +7385,16 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (z < 0.1) { throw RuntimeError("GCode: very low z"); } - gcode += m_writer.extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, + gcode += m_writer->extrude_to_xyz(Vec3d(dest2d.x(), dest2d.y(), z), e, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else if (sloped == nullptr) { // Normal extrusion - gcode += m_writer.extrude_to_xy(p.head<2>(), dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer->extrude_to_xy(p.head<2>(), dE, GCodeWriter::full_gcode_comment ? tempDescription : ""); } else { // Sloped extrusion const auto [z_ratio, e_ratio] = sloped->interpolate(path_length / total_length); Vec3d dest3d(p(0), p(1), get_sloped_z(z_ratio)); - gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); + gcode += m_writer->extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); } prev = p; @@ -7292,25 +7447,39 @@ 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); } - else { - bool use_steps = step > 0.f; - if (use_steps) { - if (start_value > end_value) { - start_value += step; - } else { - end_value += step; - } + bool use_steps = step > 0.f; + if (use_steps) { + if (start_value > end_value) { + start_value += step; + } else { + end_value += step; } - float ratio = m_layer_index / (m_layer_count - 1.f); - float value = start_value + ratio * (end_value - start_value); - if (use_steps) { - value = trunc(value / step) * step; - } - return value; } + float value = start_value + ratio * (end_value - start_value); + if (use_steps) { + value = trunc(value / step) * step; + } + return value; } std::string encodeBase64(uint64_t value) @@ -7404,18 +7573,18 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string } } - if (m_writer.get_gcode_flavor() == gcfKlipper) { - gcode += m_writer.set_accel_and_jerk(acceleration_to_set, jerk_to_set); + if (m_writer->get_gcode_flavor() == gcfKlipper) { + gcode += m_writer->set_accel_and_jerk(acceleration_to_set, jerk_to_set); } else { - gcode += m_writer.set_travel_acceleration(acceleration_to_set); - gcode += m_writer.set_jerk_xy(jerk_to_set); + gcode += m_writer->set_travel_acceleration(acceleration_to_set); + gcode += m_writer->set_jerk_xy(jerk_to_set); } // if a retraction would be needed, try to use reduce_crossing_wall to plan a // multi-hop travel path inside the configuration space if (m_config.reduce_crossing_wall && !m_avoid_crossing_perimeters.disabled_once() - && m_writer.is_current_position_clear()) + && m_writer->is_current_position_clear()) //BBS: don't generate detour travel paths when current position is unclea { travel = m_avoid_crossing_perimeters.travel_to(*this, point, &could_be_wipe_disabled); @@ -7462,7 +7631,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // if needed, write the gcode_label_objects_end then gcode_label_objects_start - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); // use G1 because we rely on paths being straight (G0 may make round paths) if (travel.size() >= 2) { @@ -7470,14 +7639,14 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string if (false/*m_spiral_vase*/) { // No lazy z lift for spiral vase mode for (size_t i = 1; i < travel.size(); ++i) { - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } else { if (travel.size() == 2) { // No extra movements emitted by avoid_crossing_perimeters, simply move to the end point with z change const auto& dest2d = this->point_to_gcode(travel.points.back()); Vec3d dest3d(dest2d(0), dest2d(1), z == DBL_MAX ? m_nominal_z : z); - gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else { // Extra movements emitted by avoid_crossing_perimeters, lift the z to normal height at the beginning, then apply the z @@ -7487,16 +7656,16 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // Lift to normal z at beginning Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), m_nominal_z); - gcode += m_writer.travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); + gcode += m_writer->travel_to_xyz(dest3d, comment, m_need_change_layer_lift_z); m_need_change_layer_lift_z = false; } else if (z != DBL_MAX && i == travel.size() - 1) { // Apply z_ratio for the very last point Vec2d dest2d = this->point_to_gcode(travel.points[i]); Vec3d dest3d(dest2d(0), dest2d(1), z); - gcode += m_writer.travel_to_xyz(dest3d, comment); + gcode += m_writer->travel_to_xyz(dest3d, comment); } else { // For all points in between, no z change - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + gcode += m_writer->travel_to_xy(this->point_to_gcode(travel.points[i]), comment); } } } @@ -7599,7 +7768,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp if (clipped_travel.length() > travel_len_thresh) clipped_travel.points.back() = clipped_travel.points.front()+(clipped_travel.points.back() - clipped_travel.points.front()) * (travel_len_thresh / clipped_travel.length()); //BBS: translate to current plate coordinate system - clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer.get_xy_offset().x()), double(m_origin.y() - m_writer.get_xy_offset().y()))); + clipped_travel.translate(Point::new_scale(double(m_origin.x() - m_writer->get_xy_offset().x()), double(m_origin.y() - m_writer->get_xy_offset().y()))); //BBS: force to retract when leave from external perimeter for a long travel //Better way is judging whether the travel move direction is same with last extrusion move. @@ -7651,13 +7820,13 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li { std::string gcode; - if (m_writer.filament() == nullptr) + if (m_writer->filament() == nullptr) return gcode; // wipe (if it's enabled for this extruder and we have a stored wipe path and no-zero wipe distance) if (FILAMENT_CONFIG(wipe) && m_wipe.has_path() && scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON) { Wipe::RetractionValues wipeRetractions = m_wipe.calculateWipeRetractionLengths(*this, toolchange); - gcode += toolchange ? m_writer.retract_for_toolchange(true,wipeRetractions.retractLengthBeforeWipe) : m_writer.retract(true, wipeRetractions.retractLengthBeforeWipe); + gcode += toolchange ? m_writer->retract_for_toolchange(true,wipeRetractions.retractLengthBeforeWipe) : m_writer->retract(true, wipeRetractions.retractLengthBeforeWipe); gcode += m_wipe.wipe(*this,wipeRetractions.retractLengthDuringWipe, toolchange, is_last_retraction); } @@ -7667,14 +7836,14 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li length is honored in case wipe path was too short. */ if ((!this->on_first_layer() || this->config().bottom_surface_pattern != InfillPattern::ipHilbertCurve) && (role != erTopSolidInfill || this->config().top_surface_pattern != InfillPattern::ipHilbertCurve)) - gcode += toolchange ? m_writer.retract_for_toolchange() : m_writer.retract(); + gcode += toolchange ? m_writer->retract_for_toolchange() : m_writer->retract(); - gcode += m_writer.reset_e(); + gcode += m_writer->reset_e(); // Orca: check if should + can lift (roughly from SuperSlicer) RetractLiftEnforceType retract_lift_type = RetractLiftEnforceType(EXTRUDER_CONFIG(retract_lift_enforce)); bool needs_lift = toolchange - || m_writer.filament()->retraction_length() > 0 + || m_writer->filament()->retraction_length() > 0 || m_config.use_firmware_retraction; bool last_fill_extrusion_role_top_infill = (this->m_last_notgapfill_extrusion_role == ExtrusionRole::erTopSolidInfill || this->m_last_notgapfill_extrusion_role == ExtrusionRole::erIroning); @@ -7697,9 +7866,9 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li if (needs_lift && can_lift) { if (apply_instantly) - gcode += m_writer.eager_lift(lift_type); + gcode += m_writer->eager_lift(lift_type); else - gcode += m_writer.lazy_lift(lift_type, m_spiral_vase != nullptr); + gcode += m_writer->lazy_lift(lift_type, m_spiral_vase != nullptr); } return gcode; @@ -7708,11 +7877,11 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override) { int new_extruder_id = get_extruder_id(new_filament_id); - if (!m_writer.need_toolchange(new_filament_id)) + if (!m_writer->need_toolchange(new_filament_id)) return ""; // if we are running a single-extruder setup, just set the extruder and return nothing - if (!m_writer.multiple_extruders) { + if (!m_writer->multiple_extruders) { this->placeholder_parser().set("current_extruder", new_filament_id); this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(new_filament_id)); this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(new_filament_id)); @@ -7735,13 +7904,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo check_add_eol(gcode); } if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); } - gcode += m_writer.toolchange(new_filament_id); + gcode += m_writer->toolchange(new_filament_id); return gcode; } @@ -7756,18 +7925,18 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS: insert skip object label before change filament while by object if (by_object) - m_writer.add_object_change_labels(gcode); + m_writer->add_object_change_labels(gcode); bool add_change_filament_624 = false; - if (m_writer.filament() != nullptr) { + if (m_writer->filament() != nullptr) { // Process the custom filament_end_gcode. set_extruder() is only called if there is no wipe tower // so it should not be injected twice. - unsigned int old_filament_id = m_writer.filament()->id(); + unsigned int old_filament_id = m_writer->filament()->id(); const std::string &filament_end_gcode = m_config.filament_end_gcode.get_at(old_filament_id); if (! filament_end_gcode.empty()) { DynamicConfig config; config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position().z() - m_config.z_offset.value)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer->get_position().z() - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(get_extruder_id(old_filament_id)))); if (!m_filament_instances_code.empty()) { @@ -7783,7 +7952,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // If ooze prevention is enabled, park current extruder in the nearest // standby point and set it to the standby temperature. - if (m_ooze_prevention.enable && m_writer.filament() != nullptr) + if (m_ooze_prevention.enable && m_writer->filament() != nullptr) gcode += m_ooze_prevention.pre_toolchange(*this); // BBS @@ -7796,7 +7965,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo if (toolchange_temp_override > 0) new_filament_temp = toolchange_temp_override; - Vec3d nozzle_pos = m_writer.get_position(); + Vec3d nozzle_pos = m_writer->get_position(); float old_retract_length, old_retract_length_toolchange, wipe_volume; int old_filament_temp, old_filament_e_feedrate; @@ -7804,16 +7973,16 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo //BBS: add handling for filament change in start gcode int old_filament_id = -1; int old_extruder_id = -1; - if (m_writer.filament() != nullptr || m_start_gcode_filament != -1) { + if (m_writer->filament() != nullptr || m_start_gcode_filament != -1) { std::vector flush_matrix(cast(get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, new_extruder_id, m_config.nozzle_diameter.values.size()))); const unsigned int number_of_extruders = (unsigned int) (m_config.filament_colour.values.size()); // if is multi_extruder only use the fist extruder matrix - if (m_writer.filament() != nullptr) - assert(m_writer.filament()->id() < number_of_extruders); + if (m_writer->filament() != nullptr) + assert(m_writer->filament()->id() < number_of_extruders); else assert(m_start_gcode_filament < number_of_extruders); - old_filament_id = m_writer.filament() != nullptr ? m_writer.filament()->id() : m_start_gcode_filament; - old_extruder_id = m_writer.filament() != nullptr ? m_writer.filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); + old_filament_id = m_writer->filament() != nullptr ? m_writer->filament()->id() : m_start_gcode_filament; + old_extruder_id = m_writer->filament() != nullptr ? m_writer->filament()->extruder_id() : get_extruder_id(m_start_gcode_filament); old_retract_length = m_config.retraction_length.get_at(old_filament_id); old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_filament_id); @@ -7823,7 +7992,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo float grab_purge_volume = m_config.grab_length.get_at(new_extruder_id) * 2.4; if (old_extruder_id != new_extruder_id) { //calc flush volume between the same extruder id - int old_filament_id_in_new_extruder = m_writer.filament(new_extruder_id) != nullptr ? m_writer.filament(new_extruder_id)->id() : -1; + int old_filament_id_in_new_extruder = m_writer->filament(new_extruder_id) != nullptr ? m_writer->filament(new_extruder_id)->id() : -1; if (old_filament_id_in_new_extruder == -1) wipe_volume = 0; else { @@ -7967,13 +8136,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo //BBS: gcode writer doesn't know where the extruder is and whether fan speed is changed after inserting tool change gcode //Set this flag so that normal lift will be used the first time after tool change. gcode += ";_FORCE_RESUME_FAN_SPEED\n"; - m_writer.set_current_position_clear(false); + m_writer->set_current_position_clear(false); //BBS: check whether custom gcode changes the z position. Update if changed double temp_z_after_tool_change; if (GCodeProcessor::get_last_z_from_gcode(toolchange_gcode_parsed, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); + Vec3d pos = m_writer->get_position(); pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); + m_writer->set_position(pos); } } } @@ -7981,13 +8150,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // BBS. Reset old extruder E-value. // Keep retract length because Custom GCode will guarantee retract length be the same as toolchange if (m_config.single_extruder_multi_material) { - m_writer.reset_e(); + m_writer->reset_e(); } //BBS: don't add T[next extruder] if there is no T cmd on filament change //We inform the writer about what is happening, but we may not use the resulting gcode. - std::string toolchange_command = m_writer.toolchange(new_filament_id); - if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer.toolchange_prefix(), new_filament_id)) + std::string toolchange_command = m_writer->toolchange(new_filament_id); + if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer->toolchange_prefix(), new_filament_id)) gcode += toolchange_command; else { // user provided his own toolchange gcode, no need to do anything @@ -7998,7 +8167,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo int temp = (m_layer_index <= 0 ? m_config.nozzle_temperature_initial_layer.get_at(new_filament_id) : m_config.nozzle_temperature.get_at(new_filament_id)); - gcode += m_writer.set_temperature(temp, false); + gcode += m_writer->set_temperature(temp, false); } this->placeholder_parser().set("current_extruder", new_filament_id); @@ -8043,7 +8212,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo gcode += m_ooze_prevention.post_toolchange(*this); if (m_config.enable_pressure_advance.get_at(new_filament_id)) { - gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); + gcode += m_writer->set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(new_filament_id)); @@ -8136,7 +8305,7 @@ Vec3d GCode::point_to_gcode(const Point3& point) const Point GCode::gcode_to_point(const Vec2d &point) const { Vec2d pt = point - m_origin; - if (const Extruder *extruder = m_writer.filament(); extruder) + if (const Extruder *extruder = m_writer->filament(); extruder) // This function may be called at the very start from toolchange G-code when the extruder is not assigned yet. pt += m_config.extruder_offset.get_at(extruder->extruder_id()); return scaled(pt); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 176af4c510..0362efc23c 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -4,6 +4,8 @@ #include "libslic3r.h" #include "ExPolygon.hpp" #include "GCodeWriter.hpp" +#include "BeltGCodeWriter.hpp" +#include "FirstLayerPlane.hpp" #include "Layer.hpp" #include "Point.hpp" #include "PlaceholderParser.hpp" @@ -206,16 +208,18 @@ public: m_last_obj_copy(nullptr, Point(std::numeric_limits::max(), std::numeric_limits::max())), // BBS m_toolchange_count(0), - m_nominal_z(0.) + m_nominal_z(0.), + m_writer(std::make_unique()) {} - ~GCode() = default; + virtual ~GCode() = default; +public: // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer - void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -229,8 +233,8 @@ public: Vec3d point_to_gcode_quantized(const Point3& point) const; const FullPrintConfig &config() const { return m_config; } const Layer* layer() const { return m_layer; } - GCodeWriter& writer() { return m_writer; } - const GCodeWriter& writer() const { return m_writer; } + GCodeWriter& writer() { return *m_writer; } + const GCodeWriter& writer() const { return *m_writer; } PlaceholderParser& placeholder_parser() { return m_placeholder_parser_integration.parser; } const PlaceholderParser& placeholder_parser() const { return m_placeholder_parser_integration.parser; } // Process a template through the placeholder parser, collect error messages to be reported @@ -252,7 +256,7 @@ public: std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX); bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type); std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone); - std::string unretract() { return m_writer.unlift() + m_writer.unretract(); } + std::string unretract() { return m_writer->unlift() + m_writer->unretract(); } std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); @@ -304,7 +308,15 @@ public: } }; -private: + // Public accessor for the first-layer plane evaluator. Used by + // CoolingBuffer (which is constructed with a GCode reference and needs + // to read the plane for per-segment fan re-evaluation). All other + // first-layer-plane access points (on_first_layer overload, effective + // index helper) are in the protected section since they're called from + // GCode internals only. + const FirstLayerPlane *first_layer_plane() const { return m_first_layer_plane.get(); } + +protected: class GCodeOutputStream { public: GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {} @@ -332,9 +344,17 @@ private: FILE *f = nullptr; GCodeProcessor &m_processor; }; + + // Virtual hooks for belt printer subclass (BeltGCode). + // No-ops in base GCode; overridden in BeltGCode. + virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} + virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} + virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} + virtual bool should_disable_arc_fitting() const { return false; } + void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); - static std::vector 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, @@ -518,7 +538,7 @@ private: DynamicConfig m_calib_config; // scaled G-code resolution double m_scaled_resolution; - GCodeWriter m_writer; + std::unique_ptr m_writer; struct PlaceholderParserIntegration { void reset(); @@ -609,6 +629,11 @@ private: std::unique_ptr m_cooling_buffer; std::unique_ptr m_spiral_vase; + // First-layer plane evaluator. Constructed once per print from the + // PrintConfig. is_active() == false on non-belt printers and on belt + // printers without a Z-axis shear; in that case all per-path plane + // checks short-circuit to the legacy Layer::id() == 0 path. + std::unique_ptr m_first_layer_plane; std::unique_ptr m_pressure_equalizer; @@ -668,6 +693,25 @@ private: // On the first printing layer. This flag triggers first layer speeds. //BBS bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0 && abs(m_layer->bottom_z()) < EPSILON; } + // Per-point first-layer test. When the FirstLayerPlane evaluator is + // active, the result depends on the supplied slicing-frame point; + // otherwise we delegate to the legacy per-layer test. This is the + // entry point used by per-path call sites in _extrude. + bool on_first_layer(const Vec3d &point_slicing_mm) const { + if (m_first_layer_plane && m_first_layer_plane->is_active()) + return m_first_layer_plane->is_first_layer( + point_slicing_mm, m_config.initial_layer_print_height.value); + return on_first_layer(); + } + // "Effective layer index" used to drive layer-count thresholds like + // slow_down_layers. When the evaluator is active this returns the + // perpendicular distance to the plane in band_thickness_mm units; + // otherwise it returns the legacy slicing layer index. + int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const { + if (m_first_layer_plane && m_first_layer_plane->is_active()) + return m_first_layer_plane->effective_layer_index(point_slicing_mm); + return on_first_layer() ? 0 : layer_id(); + } int layer_id() const { if (m_layer == nullptr) return -1; diff --git a/src/libslic3r/GCode/BeltBackTransform.cpp b/src/libslic3r/GCode/BeltBackTransform.cpp new file mode 100644 index 0000000000..6a25bbd408 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.cpp @@ -0,0 +1,40 @@ +#include "BeltBackTransform.hpp" +#include "../BeltTransform.hpp" + +namespace Slic3r { + +bool BeltBackTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_inverse = Transform3d::Identity(); + + if (!config.belt_printer.value || !config.gcode_back_transform.value) + return false; + + // Require at least one active transform to proceed. + bool has_global_rotation = config.belt_slice_rotation_global.value + && config.belt_slice_rotation.value != BeltRotationAxis::None; + bool has_preslice_global = config.belt_preslice_global.value + || config.preslice_remap_global.value; + if (!has_global_rotation && !has_preslice_global + && !BeltTransformPipeline::has_preslice_remap(config)) + return false; + + // Build the forward pipeline (rotation * pre_remap) and store its inverse. + Transform3d forward = BeltTransformPipeline::build_forward_transform(config); + if (forward.isApprox(Transform3d::Identity())) + return false; + + m_inverse = forward.inverse(); + m_active = true; + return true; +} + +Vec3d BeltBackTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_inverse * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp new file mode 100644 index 0000000000..b98770c688 --- /dev/null +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_BeltBackTransform_hpp_ +#define slic3r_BeltBackTransform_hpp_ + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +// Reverses the pre-slice remap + shear + scale transforms that +// PrintObjectSlice.cpp applies to belt printer geometry, converting G-code +// coordinates from the sliced (remapped/sheared/scaled) frame back to the +// machine's real coordinate space. +// +// Initialized once from PrintConfig, then applied per-point in +// GCodeWriter::to_machine_coords() before axis remapping. +// +// Active when gcode_back_transform is true AND at least one of: +// - a shear axis has global mode enabled, or +// - a pre-slice axis remap is non-identity. +class BeltBackTransform +{ +public: + BeltBackTransform() = default; + + // Initialize from belt printer config. Rebuilds the same pre-slice remap, + // shear, and scale matrices as PrintObjectSlice.cpp and precomputes the + // affine inverse. Returns true if a non-identity back-transform was computed. + bool init_from_config(const PrintConfig &config); + + // Apply the inverse transform to a point. Returns pos unchanged if + // no back-transform is active. + Vec3d apply(const Vec3d &pos) const; + + // True if a non-identity back-transform is active. + bool is_active() const { return m_active; } + +private: + bool m_active = false; + Transform3d m_inverse = Transform3d::Identity(); +}; + +} // namespace Slic3r + +#endif // slic3r_BeltBackTransform_hpp_ diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 8cfb99a46b..a92bb7e4cf 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,10 +1,14 @@ #include "../GCode.hpp" +#include "../FirstLayerPlane.hpp" #include "CoolingBuffer.hpp" #include #include #include +#include +#include #include #include +#include #include #include @@ -28,6 +32,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t m_num_extruders = std::max(ex.id() + 1, m_num_extruders); m_extruder_ids.emplace_back(ex.id()); } + + // Borrow the first-layer plane from the GCode generator. When inactive + // (non-belt printers and belt printers without Z shear), per-line fan + // re-evaluation is skipped and behavior is bit-identical to the legacy + // per-layer path. + m_first_layer_plane = gcodegen.first_layer_plane(); } void CoolingBuffer::reset(const Vec3d &position) @@ -328,6 +338,13 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); + // First-layer plane: per-segment fan re-evaluation post-pass. Walks + // the cooled-down gcode and inserts inline M106 commands at band + // crossings (where the path's perpendicular distance to the plane + // crosses close_fan_the_first_x_layers thresholds). No-op when + // the evaluator is inactive. + if (m_first_layer_plane && m_first_layer_plane->is_active()) + out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched); m_gcode.clear(); } return out; @@ -1014,4 +1031,214 @@ std::string CoolingBuffer::apply_layer_cooldown( return new_gcode; } +// Pure helper: compute the main fan speed for a given effective layer index. +// Mirrors the inline logic in change_extruder_set_fan but is callable from +// per-line code in apply_first_layer_plane_fan_eval. +int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, + unsigned int extruder_id) const +{ +#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id) + float fan_min_speed = EXTRUDER_CFG(fan_min_speed); + float fan_max_speed = EXTRUDER_CFG(fan_max_speed); + bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq); + int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers); + int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer); + float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time)); + float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time)); +#undef EXTRUDER_CFG + + if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) + close_fan_the_first_x_layers = 1; + + float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f; + if (effective_layer_id >= close_fan_the_first_x_layers) { + if (layer_time < slow_down_layer_time) { + fan_speed_new = fan_max_speed; + } else if (layer_time < fan_cooling_layer_time) { + double t = (layer_time - slow_down_layer_time) / + (fan_cooling_layer_time - slow_down_layer_time); + fan_speed_new = float(int(floor(t * fan_min_speed + + (1. - t) * fan_max_speed) + 0.5)); + } + if (effective_layer_id + 1 < full_fan_speed_layer) { + float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers) + / float(full_fan_speed_layer - close_fan_the_first_x_layers); + fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255)); + } + } else { + fan_speed_new = 0.f; + } + return int(fan_speed_new); +} + +// Post-pass: walk the cooled-down gcode line by line, track XYZ position, +// and insert M106 commands at first-layer-plane band crossings so the fan +// follows perpendicular distance to the plane rather than the slicing-layer +// index. Only invoked when the FirstLayerPlane evaluator is active. +// +// This implementation is intentionally minimal: it overrides only the MAIN +// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc +// special fans remain at their layer-level values from apply_layer_cooldown. +// That keeps the per-line logic small while still giving the user precise +// fan control near the belt surface, which is the main quality concern. +std::string CoolingBuffer::apply_first_layer_plane_fan_eval( + std::string &&gcode_in, size_t /*layer_id*/, float layer_time) +{ + if (!m_first_layer_plane || !m_first_layer_plane->is_active()) + return std::move(gcode_in); + + const std::string &gcode = gcode_in; + std::string out; + out.reserve(gcode.size() + 256); + + // Match the PWM floor applied at every other set_fan call in this file so + // band-crossing M106 emissions start the fan reliably at low speeds. + const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); + + // Track position in slicing-frame mm. Seed from m_current_pos which the + // CoolingBuffer keeps up-to-date across layers. + Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]); + + // Track current main fan speed by parsing M106 commands as we walk so + // we can restore it after a band exit. + int current_main_fan = m_fan_speed; + int pre_band_main_fan = current_main_fan; + // Implicit initial state: assume the layer started "out of the band" + // (i.e., the layer-level fan setting from apply_layer_cooldown is in + // effect). The first movement we encounter will reconcile this. + bool in_first_layer_band = false; + unsigned int active_extruder = m_current_extruder; + + auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) { + if (line_sv.size() < 3) return false; + if (line_sv[0] != 'G') return false; + if (line_sv[1] != '0' && line_sv[1] != '1') return false; + if (line_sv[2] != ' ' && line_sv[2] != '\t') return false; + const char *c = line_sv.data() + 3; + const char *end = line_sv.data() + line_sv.size(); + bool any = false; + while (c < end && *c != ';') { + while (c < end && (*c == ' ' || *c == '\t')) ++c; + if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break; + char axis = *c; + ++c; + if (axis == 'X' || axis == 'Y' || axis == 'Z') { + char *next; + double v = std::strtod(c, &next); + if (next != c) { + if (axis == 'X') p.x() = v; + else if (axis == 'Y') p.y() = v; + else p.z() = v; + c = next; + any = true; + continue; + } + } + // Skip unrecognized word. + while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n') + ++c; + } + return any; + }; + + auto parse_m106 = [](const std::string_view &line_sv) -> int { + // Returns -1 if not an M106, otherwise the S value (0..255). + if (line_sv.size() < 4 || line_sv[0] != 'M') return -1; + if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6')) + return -1; + // Find S + size_t s_pos = line_sv.find('S'); + if (s_pos == std::string_view::npos) return -1; + const char *c = line_sv.data() + s_pos + 1; + char *next; + long v = std::strtol(c, &next, 10); + if (next == c) return -1; + return int(std::clamp(v, 0, 255)); + }; + + auto parse_m107 = [](const std::string_view &line_sv) -> bool { + return line_sv.size() >= 4 && line_sv[0] == 'M' && + line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7'; + }; + + auto parse_tool_change = [this](const std::string_view &line_sv) -> int { + // Returns the new extruder id, or -1 if not a toolchange. + if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1; + if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0) + return -1; + const char *c = line_sv.data() + m_toolchange_prefix.size(); + char *next; + long v = std::strtol(c, &next, 10); + if (next == c) return -1; + return int(v); + }; + + const char *p = gcode.c_str(); + const char *end = gcode.c_str() + gcode.size(); + while (p < end) { + const char *line_end = p; + while (line_end < end && *line_end != '\n') ++line_end; + const char *next_line = line_end; + if (next_line < end) ++next_line; // include the '\n' + + std::string_view line_sv(p, line_end - p); + + // Track tool changes so the per-line fan eval uses the right extruder. + int new_tool = parse_tool_change(line_sv); + if (new_tool >= 0) + active_extruder = unsigned(new_tool); + + // Track existing fan commands so we can restore the right value when + // exiting a band. + int m106_speed = parse_m106(line_sv); + if (m106_speed >= 0) { + current_main_fan = m106_speed; + if (!in_first_layer_band) + pre_band_main_fan = m106_speed; + } else if (parse_m107(line_sv)) { + current_main_fan = 0; + if (!in_first_layer_band) + pre_band_main_fan = 0; + } + + // Movement line: parse XYZ, evaluate plane, possibly emit a fan + // change BEFORE this line. + bool moved = parse_xyz_into(line_sv, cur_pos_mm); + if (moved) { + const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm); + const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder); + const bool now_in_band = eff_idx < std::max(close_n, 1); + if (now_in_band != in_first_layer_band) { + // Band crossing: emit a M106 with the appropriate speed. + int target_fan; + if (now_in_band) { + // Entering the first-layer band: fan off. + pre_band_main_fan = current_main_fan; + target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); + } else { + // Exiting the band: restore the layer's normal fan speed. + // Use compute_main_fan_speed with the effective index so + // the linear ramp factor (close_fan→full_fan_speed_layer) + // also follows distance from the plane. + target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); + if (target_fan == 0) + target_fan = pre_band_main_fan; + } + if (target_fan != current_main_fan) { + out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm); + current_main_fan = target_fan; + m_fan_speed = target_fan; + m_current_fan_speed = target_fan; + } + in_first_layer_band = now_in_band; + } + } + + out.append(p, next_line - p); + p = next_line; + } + + return out; +} + } // namespace Slic3r diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index fba27b289b..4bb8f26476 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -10,6 +10,7 @@ namespace Slic3r { class GCode; class Layer; +class FirstLayerPlane; struct PerExtruderAdjustments; // A standalone G-code filter, to control cooling of the print. @@ -18,7 +19,7 @@ struct PerExtruderAdjustments; // // The simple it sounds, the actual implementation is significantly more complex. // Namely, for a multi-extruder print, each material may require a different cooling logic. -// For example, some materials may not like to print too slowly, while with some materials +// For example, some materials may not like to print too slowly, while with some materials // we may slow down significantly. // class CoolingBuffer { @@ -36,6 +37,21 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); + // First-layer plane: per-line fan re-evaluation post-pass. Walks the + // post-cooldown gcode, tracks XYZ position, and inserts M106 commands at + // band-crossing transitions in slicing-frame coordinates. Only runs + // when m_first_layer_plane is active. + std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in, + size_t layer_id, + float layer_time); + + // Pure helper: compute the main fan speed for a given effective layer + // index (layer-id units, mapped through the plane evaluator) and the + // current extruder. Mirrors the inline logic in the change_extruder_set_fan + // lambda but is callable from per-line code. + int compute_main_fan_speed(int effective_layer_id, float layer_time, + unsigned int extruder_id) const; + // G-code snippet cached for the support layers preceding an object layer. std::string m_gcode; // Internal data. @@ -57,6 +73,9 @@ private: unsigned int m_current_extruder; //BBS: current fan speed int m_current_fan_speed; + // First-layer plane evaluator, borrowed from GCode. Null = inactive + // (legacy per-layer fan control). + const FirstLayerPlane *m_first_layer_plane = nullptr; }; } diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index cc5036846e..b15736ef57 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3072,6 +3072,44 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + // Belt printer: derive the physical tilt magnitude from the slicing-rotation + // angle header comment (used to enable the preview's belt view). + if (boost::starts_with(comment, " belt_slice_rotation_angle = ")) { + try { + m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29)))); + } catch (...) {} + return; + } + // Belt printer: parse pre-slice axis remap from header comments. + { + auto trim = [](const std::string &s) -> std::string { + size_t start = s.find_first_not_of(" \t\r\n"); + size_t end = s.find_last_not_of(" \t\r\n"); + return (start == std::string::npos) ? "" : s.substr(start, end - start + 1); + }; + // Pre-slice axis remap + auto parse_remap_axis = [](const std::string &s) -> RemapAxis { + if (s == "pos_x") return RemapAxis::PosX; + if (s == "pos_y") return RemapAxis::PosY; + if (s == "pos_z") return RemapAxis::PosZ; + if (s == "neg_x") return RemapAxis::NegX; + if (s == "neg_y") return RemapAxis::NegY; + if (s == "neg_z") return RemapAxis::NegZ; + if (s == "rev_x") return RemapAxis::RevX; + if (s == "rev_y") return RemapAxis::RevY; + if (s == "rev_z") return RemapAxis::RevZ; + return RemapAxis::PosX; + }; + if (boost::starts_with(comment, " preslice_remap_x = ")) { + m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " preslice_remap_y = ")) { + m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + if (boost::starts_with(comment, " preslice_remap_z = ")) { + m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + } + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; @@ -4895,6 +4933,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()) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 546df6fbb2..8fcf2e0c43 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -245,6 +245,17 @@ 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 }; + RemapAxis preslice_remap_x{ RemapAxis::PosX }; + RemapAxis preslice_remap_y{ RemapAxis::PosY }; + RemapAxis preslice_remap_z{ RemapAxis::PosZ }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -310,6 +321,11 @@ class Print; optimal_assignment = other.optimal_assignment; filament_change_count_map = other.filament_change_count_map; initial_layer_time = other.initial_layer_time; + belt_tilt_angle = other.belt_tilt_angle; + belt_z_origin = other.belt_z_origin; + preslice_remap_x = other.preslice_remap_x; + preslice_remap_y = other.preslice_remap_y; + preslice_remap_z = other.preslice_remap_z; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/GCode/MachineFrameTransform.cpp b/src/libslic3r/GCode/MachineFrameTransform.cpp new file mode 100644 index 0000000000..bdc172d7a4 --- /dev/null +++ b/src/libslic3r/GCode/MachineFrameTransform.cpp @@ -0,0 +1,74 @@ +#include "MachineFrameTransform.hpp" +#include "../Geometry.hpp" + +#include + +namespace Slic3r { + +bool MachineFrameTransform::init_from_config(const PrintConfig &config) +{ + m_active = false; + m_transform = 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 cos_a = std::cos(angle_rad); + if (std::abs(cos_a) <= EPSILON) + return false; + const double tan_a = std::sin(angle_rad) / cos_a; + const double inv_cos = 1.0 / cos_a; + + // Couple the height axis (Z) to the belt-feed axis and stretch the belt-feed + // axis by 1/cos so a unit slicing move maps to the correct belt travel. The + // shear sign matches the belt-floor slope derived from the same rotation in + // BeltTransformPipeline::compute_belt_height_and_floor: + // tilt about X: feed axis Y, Z += +tan·Y, scale Y *= 1/cos + // tilt about Y: feed axis X, Z += -tan·X, scale X *= 1/cos + Matrix3d shear = Matrix3d::Identity(); + Matrix3d scale = Matrix3d::Identity(); + if (axis == BeltRotationAxis::X) { + shear(2, 1) = tan_a; // Z from Y + scale(1, 1) = inv_cos; // Y + } else { // BeltRotationAxis::Y + shear(2, 0) = -tan_a; // Z from X + scale(0, 0) = inv_cos; // 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/cos, 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_active = true; + return true; +} + +Vec3d MachineFrameTransform::apply(const Vec3d &pos) const +{ + if (!m_active) + return pos; + return m_transform * pos; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp new file mode 100644 index 0000000000..f5e71398da --- /dev/null +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -0,0 +1,48 @@ +#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 BeltGCodeWriter::to_machine_coords 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 tan a) plus a 1/cos a scale on the belt-feed 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; + + 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(); +}; + +} // namespace Slic3r + +#endif // slic3r_MachineFrameTransform_hpp_ diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 4ead8dfb49..53e5a3b5a7 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,5 +1,6 @@ #include "GCodeWriter.hpp" #include "CustomGCode.hpp" +#include "Geometry.hpp" #include "I18N.hpp" #include "PrintConfig.hpp" #include @@ -20,6 +21,36 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +void GCodeWriter::set_axis_remap(int rx, int ry, int rz) +{ + m_remap_x = rx; + m_remap_y = ry; + m_remap_z = rz; +} + +void GCodeWriter::set_build_volume_max(const Vec3d &max) +{ + m_build_vol_max = max; +} + +bool GCodeWriter::has_axis_remap() const +{ + return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; +} + +Vec3d GCodeWriter::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) }; +} + bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) { return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); @@ -608,7 +639,13 @@ 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); + 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 = m_is_first_layer ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; w.emit_f(speed * 60.0); @@ -648,8 +685,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 = type; std::string lift_move; double target_lift = 0; { @@ -664,7 +702,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) { // BBS: spiral lift only safe with known position // TODO: check the arc will move within bed area - 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) @@ -839,7 +877,13 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) } 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); @@ -944,7 +988,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: 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 @@ -989,6 +1038,9 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std //BBS: take plate offset into consider Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; + if (has_axis_remap()) + point_on_plate = apply_axis_remap(point_on_plate); + GCodeG1Formatter w; if (z_changed) w.emit_xyz(point_on_plate); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 10e52a1c8e..3170a1e730 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -8,11 +8,11 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" - namespace Slic3r { class GCodeWriter { public: + virtual ~GCodeWriter() = default; GCodeConfig config; bool multiple_extruders; @@ -73,21 +73,21 @@ public: std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()); // SoftFever NOTE: the returned speed is mm/minute double get_current_speed() const { return m_current_speed;} - std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); - std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); + virtual std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); + virtual std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false); std::string travel_to_z(double z, const std::string &comment = std::string(), bool force = false); bool will_move_z(double z) const; - std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + virtual 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); - std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); + virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); std::string unretract(); // do lift instantly - std::string eager_lift(const LiftType type); + virtual std::string eager_lift(const LiftType type); // record a lift request, do realy lift in next travel - std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); + virtual std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false); std::string unlift(); const Vec3d& get_position() const { return m_pos; } Vec3d& get_position() { return m_pos; } @@ -127,9 +127,38 @@ public: void set_is_first_layer(bool bval) { m_is_first_layer = bval; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } + // 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; + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); - private: +protected: + // Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter) + 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; + + virtual std::string _travel_to_z(double z, const std::string &comment); + + // Axis remap state — accessible to subclasses. + 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 axis remap to a point. Returns pos unchanged if remap is identity. + Vec3d apply_axis_remap(const Vec3d &pos) const; + +private: // Extruders are sorted by their ID, so that binary search is possible. std::vector m_filament_extruders; bool m_single_extruder_multi_material; @@ -157,37 +186,21 @@ public: unsigned int m_last_additional_fan_speed; 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; - + 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); std::string _retract(double length, double restart_extra, const std::string &comment); std::string set_acceleration_internal(Acceleration type, unsigned int acceleration); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 948c779137..96cc5f58d8 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1326,7 +1326,15 @@ 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", + "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", "belt_slice_rotation_global", + "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", + "belt_frame_tilt_decouple", "belt_frame_tilt_angle", + "belt_preslice_global", + "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", + "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", + "gcode_flavor", "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", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 66ceb5aedf..a4edf27be5 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -12,6 +12,7 @@ #include "Thread.hpp" #include "Time.hpp" #include "GCode.hpp" +#include "BeltGCode.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "Utils.hpp" @@ -101,6 +102,12 @@ 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", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -281,8 +288,26 @@ 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" + || opt_key == "belt_slice_rotation_global" + || opt_key == "belt_preslice_global" + || opt_key == "preslice_remap_global" + || opt_key == "preslice_remap_x" + || opt_key == "preslice_remap_y" + || opt_key == "preslice_remap_z") { osteps.emplace_back(posSlice); + } else if ( + opt_key == "belt_support_floor_offset" + || opt_key == "belt_support_floor_mode" + || opt_key == "belt_support_z_offset_mode") { + osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" || opt_key == "filament_type" @@ -574,6 +599,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); @@ -582,6 +610,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); } @@ -1283,6 +1314,16 @@ 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.") }; + } + if (nozzles < 2 && extruders.size() > 1) { auto ret = check_multi_filament_valid(*this); if (!ret.string.empty()) @@ -1374,6 +1415,28 @@ StringObjectException Print::validate(std::vector *warnin // Checks that the print does not exceed the max print height 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]; + // Belt printer: the sliced (virtual) Z is belt travel along the conveyor, not + // build height, so the loop below (which measures the sliced layer stack) is + // meaningless here. The real ceiling is the usable vertical clearance above the + // belt, which printable_height holds directly: the gantry travels up the tilted + // plane, so a part of height z needs gantry reach z / cos(tilt), and the machine's + // gantry-axis range is sized to accommodate that (e.g. IdeaFormer IR3 V2: ~354 mm + // of gantry travel = 250 mm vertical at 45°, and printable_height = 250). So compare + // the upright object height against printable_height directly. + if (m_config.belt_printer.value) { + const double max_h = m_config.printable_height.value; + double obj_top = 0.0; + const ModelObject *mo = print_object.model_object(); + for (const ModelInstance *mi : mo->instances) + obj_top = std::max(obj_top, mo->instance_bounding_box(*mi).max.z()); + if (obj_top > max_h + EPSILON) + return StringObjectException{ + Slic3r::format(_u8L("The object %1% exceeds the maximum printable height " + "above the belt (%2% mm)."), + print_object.model_object()->name, max_h), + print_object.model_object(), "" }; + continue; + } //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()) { @@ -2247,15 +2310,24 @@ 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 && + ((m_config.belt_slice_rotation_global.value + && m_config.belt_slice_rotation.value != BeltRotationAxis::None) + || m_config.preslice_remap_global.value + || m_config.belt_preslice_global.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()) @@ -2274,12 +2346,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) { @@ -2641,12 +2715,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; @@ -2655,6 +2734,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. diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index aed2dd68e4..b94f8eb04e 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -17,6 +17,7 @@ #include "GCode/ThumbnailData.hpp" #include "GCode/GCodeProcessor.hpp" #include "MultiMaterialSegmentation.hpp" +#include "BeltTransform.hpp" #include "libslic3r.h" #include @@ -186,6 +187,13 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(data) {} }; +// Returns the model's raw bounding box with pre-slice axis remap applied. +// When no remap is active, returns the unmodified raw_bounding_box(). +inline BoundingBoxf3 belt_remapped_bbox(const ModelObject &model_object, const PrintConfig &config) +{ + return BeltTransformPipeline::remap_bbox(model_object, config); +} + // Single instance of a PrintObject. // As multiple PrintObjects may be generated for a single ModelObject (their instances differ in rotation around Z), // ModelObject's instancess will be distributed among these multiple PrintObjects. @@ -575,7 +583,27 @@ 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 }; + // 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: XY correction from global pre-slice mode, applied to G-code origin. + Vec2d m_belt_global_xy_correction { Vec2d::Zero() }; + // 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; } + double belt_min_z() const { return m_belt_min_z; } + Vec2d belt_global_xy_correction() const { return m_belt_global_xy_correction; } +private: + + // SoftFever // // object id diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index a80ad6f730..ddac9a10bf 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1,6 +1,7 @@ #include "ClipperUtils.hpp" #include "Model.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" #include #include @@ -135,22 +136,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 }); @@ -1527,11 +1535,20 @@ 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 global mode: force each instance into its own PrintObject + // so each gets independent layer Z values. + bool belt_force_separate = m_config.belt_printer.value && ( + (m_config.belt_slice_rotation_global.value + && m_config.belt_slice_rotation.value != BeltRotationAxis::None + && std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON) + || m_config.belt_preslice_global.value + || (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config))); + 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)) @@ -1579,6 +1596,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; @@ -1614,6 +1632,20 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(object->invalidate_step(posSlice)); } } + + // Belt printer global mode: when any object's instances shifted, + // recompute m_belt_global_z_offset for ALL objects (it depends on + // min_shift across all objects, so one move affects everyone). + if (belt_instances_shifted + && m_config.belt_printer.value + && ((m_config.belt_slice_rotation_global.value + && m_config.belt_slice_rotation.value != BeltRotationAxis::None + && std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON) + || m_config.belt_preslice_global.value + || (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config)))) { + 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 e107ca2cb8..fd37b92c02 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1,7 +1,9 @@ #include "PrintConfig.hpp" #include "PrintConfigConstants.hpp" +#include "BeltTransform.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" +#include "Geometry.hpp" #include "MaterialType.hpp" #include "I18N.hpp" #include "format.hpp" @@ -308,6 +310,54 @@ 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_BeltSupportFloorMode { + { "none", int(BeltSupportFloorMode::None) }, + { "generator_only", int(BeltSupportFloorMode::GeneratorOnly) }, + { "clip_only", int(BeltSupportFloorMode::ClipOnly) }, + { "both", int(BeltSupportFloorMode::Both) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode) + +static t_config_enum_values s_keys_map_BeltSupportZOffsetMode { + { "none", int(BeltSupportZOffsetMode::None) }, + { "unconditional", int(BeltSupportZOffsetMode::Unconditional) }, + { "raft_only", int(BeltSupportZOffsetMode::RaftOnly) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportZOffsetMode) + +static t_config_enum_values s_keys_map_FirstLayerPlaneMode { + { "auto", int(FirstLayerPlaneMode::Auto) }, + { "xy", int(FirstLayerPlaneMode::XY) }, + { "yz", int(FirstLayerPlaneMode::YZ) }, + { "xz", int(FirstLayerPlaneMode::XZ) }, + { "belt_affine", int(FirstLayerPlaneMode::BeltAffine) }, + // Back-compat alias: pre-rotation builds serialised this mode as + // "belt_shear". Accept it on parse so old 3MFs / presets keep loading. + { "belt_shear", int(FirstLayerPlaneMode::BeltAffine) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FirstLayerPlaneMode) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -6343,6 +6393,266 @@ 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 = -90; + def->max = 90; + 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 = -90; + def->max = 90; + 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 = comAdvanced; + 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_slice_rotation_global", coBool); + def->label = L("Global"); + def->category = L("Printable space"); + def->tooltip = L("Treat the slicing rotation as part of the global forward transform " + "that BeltBackTransform inverts before the machine-frame remap. " + "Required for rotation-mode belt printers. " + "Defaults to on because virtually all rotation-mode printers need it."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); + + 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 (tan) and " + "scale (1/cos) 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 + auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, + RemapAxis default_axis, ConfigOptionMode mode = comSimple) { + auto def = this->add(key, coEnum); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L(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("preslice_remap_x", "X", + "Before slicing, which model-space axis becomes the slicer's X axis. " + "Use this to re-orient the coordinate system so the slicer's XY plane matches " + "your belt printer's physical bed plane. For a printer whose bed is in the XZ plane, " + "set Y to +Z and Z to +Y (or -Y) to swap the vertical and belt-travel axes. " + "Default +X: no change.", + RemapAxis::PosX, comExpert); + add_belt_remap("preslice_remap_y", "Y", + "Before slicing, which model-space axis becomes the slicer's Y axis. " + "The slicer treats Y as one of the two horizontal bed axes. If your physical " + "belt surface runs along the Z axis, map Y to +Z here so the slicer slices " + "along the correct plane. Default +Y: no change.", + RemapAxis::PosY, comExpert); + add_belt_remap("preslice_remap_z", "Z", + "Before slicing, which model-space axis becomes the slicer's Z axis (layer stacking direction). " + "The slicer builds layers upward along this axis. If your printer's layer-stacking " + "direction is the physical Y axis, map Z to +Y (or -Y for inverted direction). " + "Rev mode mirrors relative to the build volume maximum. Default +Z: no change.", + RemapAxis::PosZ, comExpert); + + def = this->add("preslice_remap_global", coBool); + def->label = L("Global"); + def->category = L("Printable space"); + def->tooltip = L("When enabled, the pre-slice axis remap accounts for each object's bed position. " + "Without this, the remap is applied locally around each object's center, so " + "objects at different positions don't get a position-dependent contribution. " + "Mirrors the 'Global' option on the belt slicing rotation, but for the remap."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(false)); + + add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comExpert); + add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comExpert); + add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comExpert); + + // 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. + + def = this->add("gcode_back_transform", coBool); + def->label = L("G-code back-transform"); + def->category = L("Printable space"); + def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " + "coordinates are in the machine's physical coordinate space. " + "Requires at least one shear axis with global mode enabled."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(true)); + + def = this->add("belt_preslice_global", coBool); + def->label = L("Global mesh transforms"); + def->category = L("Printable space"); + def->tooltip = L("When enabled, pre-slice belt transforms (remap, shear, scale) account for " + "each object's bed position, producing correct machine coordinates without " + "relying on origin snap. Each instance gets its own PrintObject."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(true)); + + // First-layer plane: which surface defines "first layer" for fan / speed / + // accel decisions. On belt printers the slicing-frame layer 0 is a tilted + // slab that no longer corresponds to the physical first printed layer. + // Auto picks BeltAffine when any belt-side affine transform is active + // (Z shear or slicing rotation), otherwise XY (legacy). + def = this->add("first_layer_plane", coEnum); + def->label = L("First layer plane"); + def->category = L("Printable space"); + def->tooltip = L("Selects the reference plane used to decide which extrusions get " + "first-layer settings (no fan, slow speed, initial-layer accel/jerk, " + "deferred temperature drop). On belt printers a single slicing layer " + "contains paths at many machine-Z values, so layer-index based detection " + "fails. Auto resolves to Belt affine plane when any belt-side affine " + "transform (Z shear or slicing rotation) is active, otherwise XY (legacy). " + "Pick XY explicitly to opt out and force the legacy slicing-layer-0 " + "detection."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"auto", "xy", "yz", "xz", "belt_affine"}; + def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt affine plane")}; + def->mode = comExpert; + def->set_default_value(new ConfigOptionEnum(FirstLayerPlaneMode::BeltAffine)); + + def = this->add("first_layer_plane_offset", coFloat); + def->label = L("Belt plane offset"); + def->category = L("Printable space"); + def->tooltip = L("Shifts the first-layer plane along its normal (mm). For axis-aligned " + "planes this is just a coordinate shift. Positive values move the plane " + "away from the belt surface (deeper into the model)."); + def->sidetext = L("mm"); + def->min = -1000; + def->max = 1000; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.0)); + + def = this->add("first_layer_plane_thickness", coFloat); + def->label = L("Plane band thickness"); + def->category = L("Printable space"); + def->tooltip = L("Thickness of one 'band' relative to the first-layer plane, in mm. " + "Used as the unit by which 'No cooling for the first N layers' (and " + "similar layer-count thresholds) is multiplied when the first-layer " + "plane is active. -1 means use initial_layer_print_height."); + def->sidetext = L("mm"); + def->min = -1; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(-1.0)); + + // 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)); + + { + auto def = this->add("belt_support_floor_mode", coEnum); + def->label = L("Floor mode"); + def->category = L("Printable space"); + def->tooltip = L("Controls belt floor awareness for supports. 'None' disables belt floor logic. " + "'Generator only' stops support generation at the belt floor plane."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "generator_only"}; + def->enum_labels = {L("None"), L("Generator only")}; + def->mode = comDevelop; + def->set_default_value(new ConfigOptionEnum(BeltSupportFloorMode::GeneratorOnly)); + } + + { + auto def = this->add("belt_support_z_offset_mode", coEnum); + def->label = L("Z offset mode"); + def->category = L("Printable space"); + def->tooltip = L("How global Z offset is applied to support layers for belt printers with global shear. " + "'None' = don't offset. 'Unconditional' = offset all layers. 'Raft only' = only offset raft layers."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "unconditional", "raft_only"}; + def->enum_labels = {L("None"), L("Unconditional"), L("Raft only")}; + def->mode = comExpert; + def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); + } + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); @@ -11432,10 +11742,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 45281d5ce1..5b1956e31e 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -169,6 +169,61 @@ 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 class BeltSupportFloorMode +{ + None, // No belt floor awareness + GeneratorOnly, // Only in tree support drop_nodes/contact_points + ClipOnly, // Only post-processing clipping + Both, // Both generator and clipping +}; + +enum class BeltSupportZOffsetMode +{ + None, // Don't apply global_z_offset to support layers + Unconditional, // Apply to all support layers + RaftOnly, // Only apply to raft layers +}; + +// Selects which plane the slicer treats as the "first layer plane" — the +// reference surface used to decide which extrusions get first-layer settings +// (no fan, slow speed, initial-layer accel/jerk, deferred temperature drop). +// +// Auto resolves to: +// - XY (inactive, legacy behavior) for non-belt printers and for belt +// printers with no active belt-side transform. +// - BeltAffine for belt printers with any active belt-side affine +// transform (Z shear, slicing rotation, or both). +// +// XY is also used as an explicit "opt out" mode that forces legacy +// per-layer first-layer detection even on belt printers. +enum class FirstLayerPlaneMode +{ + Auto = 0, + XY, + YZ, + XZ, + BeltAffine, // formerly BeltShear; renamed to reflect rotation support +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -528,6 +583,11 @@ 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(BeltSupportFloorMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FirstLayerPlaneMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1480,6 +1540,40 @@ 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)) + ((ConfigOptionBool, belt_slice_rotation_global)) + // 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, preslice_remap_x)) + ((ConfigOptionEnum, preslice_remap_y)) + ((ConfigOptionEnum, preslice_remap_z)) + ((ConfigOptionBool, preslice_remap_global)) + ((ConfigOptionEnum, gcode_remap_x)) + ((ConfigOptionEnum, gcode_remap_y)) + ((ConfigOptionEnum, gcode_remap_z)) + ((ConfigOptionBool, gcode_back_transform)) + ((ConfigOptionBool, belt_preslice_global)) + ((ConfigOptionEnum, first_layer_plane)) + ((ConfigOptionFloat, first_layer_plane_offset)) + ((ConfigOptionFloat, first_layer_plane_thickness)) + ((ConfigOptionFloat, belt_support_floor_offset)) + ((ConfigOptionEnum, belt_support_floor_mode)) + ((ConfigOptionEnum, belt_support_z_offset_mode)) //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 925da0c564..4597136bb6 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -2,6 +2,9 @@ #include "Model.hpp" #include "Point.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" + +#include #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "ElephantFootCompensation.hpp" @@ -12,6 +15,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" @@ -452,11 +456,15 @@ std::vector> PrintObject::detect_extruder_geometric_unprintables() // 3) Generates perimeters, gap fills and fill regions (fill regions of type stInternal). void PrintObject::make_perimeters() { + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters request tid=" << std::this_thread::get_id() << " obj=" << this; // prerequisites this->slice(); - if (! this->set_started(posPerimeters)) + if (! this->set_started(posPerimeters)) { + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; return; + } + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters ENTER tid=" << std::this_thread::get_id() << " obj=" << this; m_print->set_status(15, L("Generating walls")); BOOST_LOG_TRIVIAL(info) << "Generating walls..." << log_memory_info(); @@ -554,6 +562,7 @@ void PrintObject::make_perimeters() m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - end"; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posPerimeters); } @@ -849,7 +858,9 @@ void PrintObject::detect_overhangs_for_lift() void PrintObject::generate_support_material() { + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material request tid=" << std::this_thread::get_id() << " obj=" << this; if (this->set_started(posSupportMaterial)) { + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material ENTER tid=" << std::this_thread::get_id() << " obj=" << this; this->clear_support_layers(); if(!has_support() && !m_print->get_no_check_flag()) { @@ -890,7 +901,10 @@ void PrintObject::generate_support_material() this->_generate_support_material(); m_print->throw_if_canceled(); } + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posSupportMaterial); + } else { + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; } } @@ -1484,9 +1498,15 @@ bool PrintObject::invalidate_step(PrintObjectStep step) invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill }); 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; } @@ -1505,6 +1525,7 @@ bool PrintObject::invalidate_all_steps() bool result = Inherited::invalidate_all_steps() | m_print->invalidate_all_steps(); // Then reset some of the depending values. m_slicing_params.valid = false; + m_belt_floor_z_shift_cache_valid = false; return result; } @@ -3733,8 +3754,28 @@ 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) { + BoundingBoxf3 bb = BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg); + if (BeltTransformPipeline::has_preslice_remap(pcfg)) + object_height = bb.size().z(); + 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; } } @@ -3775,9 +3816,24 @@ 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) { + bb = BeltTransformPipeline::remap_bbox(model_object, print_config); + if (BeltTransformPipeline::has_preslice_remap(print_config)) + object_max_z = (float)bb.size().z(); + 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) @@ -4286,6 +4342,67 @@ void PrintObject::combine_infill() } } +// Belt printer: clip an ExtrusionEntityCollection to a region defined by clip_expoly. +// Handles ExtrusionPath, ExtrusionMultiPath, ExtrusionLoop, and nested ExtrusionEntityCollection. +static void clip_support_fills(ExtrusionEntityCollection &fills, const ExPolygons &clip_region) +{ + ExtrusionEntitiesPtr new_entities; + for (ExtrusionEntity *entity : fills.entities) { + if (auto *path = dynamic_cast(entity)) { + ExtrusionEntityCollection clipped; + path->intersect_expolygons(clip_region, &clipped); + if (!clipped.empty()) { + for (ExtrusionEntity *e : clipped.entities) + new_entities.push_back(e->clone()); + } + delete entity; + } else if (auto *multipath = dynamic_cast(entity)) { + ExtrusionPaths new_paths; + for (const ExtrusionPath &p : multipath->paths) { + ExtrusionEntityCollection clipped; + p.intersect_expolygons(clip_region, &clipped); + for (ExtrusionEntity *e : clipped.entities) + if (auto *cp = dynamic_cast(e)) + new_paths.push_back(std::move(*cp)); + } + if (!new_paths.empty()) { + multipath->paths = std::move(new_paths); + new_entities.push_back(multipath); + } else { + delete entity; + } + } else if (auto *loop = dynamic_cast(entity)) { + ExtrusionPaths new_paths; + for (const ExtrusionPath &p : loop->paths) { + ExtrusionEntityCollection clipped; + p.intersect_expolygons(clip_region, &clipped); + for (ExtrusionEntity *e : clipped.entities) + if (auto *cp = dynamic_cast(e)) + new_paths.push_back(std::move(*cp)); + } + if (!new_paths.empty()) { + // Loop is no longer a closed loop after clipping; emit as individual paths. + for (auto &p : new_paths) + new_entities.push_back(new ExtrusionPath(std::move(p))); + delete entity; + } else { + delete entity; + } + } else if (auto *coll = dynamic_cast(entity)) { + clip_support_fills(*coll, clip_region); + if (!coll->empty()) { + new_entities.push_back(coll); + } else { + delete entity; + } + } else { + // Unknown entity type — keep as-is. + new_entities.push_back(entity); + } + } + fills.entities = std::move(new_entities); +} + void PrintObject::_generate_support_material() { if (is_tree(m_config.support_type.value)) { @@ -4297,6 +4414,25 @@ void PrintObject::_generate_support_material() PrintObjectSupportMaterial support_material(this, m_slicing_params); support_material.generate(*this); } + // Global Z offset for support layers: + // - Normal support: layers already inherit global_z_offset from object layers. + // - 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 diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 07eb885eda..1212100eac 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include @@ -9,6 +11,9 @@ #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" #include "Print.hpp" +#include "BeltTransform.hpp" +#include "BeltSliceStrategy.hpp" +#include "Geometry.hpp" //BBS #include "ShortestPath.hpp" #include "libslic3r/Feature/Interlocking/InterlockingGenerator.hpp" @@ -152,7 +157,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); @@ -167,6 +173,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; @@ -300,20 +310,47 @@ 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) + || BeltTransformPipeline::has_preslice_remap(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; 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 (idx_first_printable_region == -1 && region.model_volume->is_model_part()) idx_first_printable_region = idx_region; else if (idx_first_printable_region != -1) { // 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; } @@ -816,8 +853,12 @@ void groupingVolumesForBrim(PrintObject* object, LayerPtrs& layers, int firstLay // Resulting expolygons of layer regions are marked as Internal. void PrintObject::slice() { - if (! this->set_started(posSlice)) + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this; + if (! this->set_started(posSlice)) { + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; return; + } + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this; //BBS: add flag to reload scene for shell rendering m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE); std::vector layer_height_profile; @@ -828,6 +869,34 @@ void PrintObject::slice() 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 at model_Y=0 (XZ belt plane). After the full + // pipeline (trafo_centered → pre_remap → shear → z_shift), the belt + // surface equation in slicer space is: + // Z_belt = sf * from_axis + belt_surface_z_centered + z_shift_val + // + // belt_surface_z_centered = remapped_bbox.min.z() (the Z position of + // the belt surface in centered-pre-shear slicer space, which is 0 + // without pre-remap but nonzero when e.g. Y↔Z swap shifts the belt + // surface away from Z=0 by the centering offset). + // + // z_shift_val = max(0, -m_belt_min_z) (lifts mesh above Z=0). + // + // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val + 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.; + // With pre-remap, the belt surface (model_Y=0) may not be at Z=0 in + // centered slicer space — add the remapped bbox min Z to compensate. + // Without pre-remap, the belt surface IS at Z=0 and bb.min.z() is + // already folded into m_belt_min_z, so use 0. + const auto &pcfg = this->print()->config(); + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; + m_slicing_params.belt_floor_z_shift = belt_surface_z + z_shift_val; + } + int firstLayerReplacedBy = 0; #if 0 @@ -866,7 +935,193 @@ 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 global mode: offset all layer Z values so objects at + // different bed positions print at different heights on the tilted belt. + // This is a post-slicing adjustment — the sliced geometry is identical + // regardless of global mode, 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) + << " belt_slice_rotation_global=" << pcfg.belt_slice_rotation_global.value + << " belt_preslice_global=" << pcfg.belt_preslice_global.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. + double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) + ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; + // 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.) - belt_surface_z; + 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; + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] centering correction" + << " obj=" << this->model_object()->name + << " m_center_offset_mm=(" << c_off.x() << "," << c_off.y() << ")" + << " centering_z_corr=" << centering_z_corr + << " (added to global_z_offset)"; + } + + // [BELT-DEBUG] Per-object summary so Case A vs Case B can be compared + // side-by-side. Lays out every value that feeds into the final layer + // print_z adjustment. + { + BoundingBoxf3 raw_bb = this->model_object()->raw_bounding_box(); + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] slice() per-object summary" + << " obj=" << this->model_object()->name + << " n_volumes=" << this->model_object()->volumes.size() + << " raw_bbox.min=(" << raw_bb.min.x() << "," << raw_bb.min.y() << "," << raw_bb.min.z() << ")" + << " raw_bbox.max=(" << raw_bb.max.x() << "," << raw_bb.max.y() << "," << raw_bb.max.z() << ")" + << " raw_bbox.center=(" << raw_bb.center().x() << "," << raw_bb.center().y() << ")" + << " m_center_offset=(" << unscale(m_center_offset.x()) << "," << unscale(m_center_offset.y()) << ")" + << " inst_shift=(" << unscale(inst_shift.x()) << "," << unscale(inst_shift.y()) << ")" + << " m_belt_min_z=" << m_belt_min_z + << " belt_surface_z=" << belt_surface_z + << " belt_z_shift=" << belt_z_shift; + // Per-volume bbox + get_matrix translation so order/composition is visible. + int vi = 0; + for (const ModelVolume *mv : this->model_object()->volumes) { + if (!mv->is_model_part()) { ++vi; continue; } + BoundingBoxf3 vol_bb = mv->mesh().transformed_bounding_box(mv->get_matrix()); + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vi + << "] id=" << mv->id().id << " name='" << mv->name << "'" + << " get_matrix.translation=(" << mv->get_matrix().translation().x() << "," << mv->get_matrix().translation().y() << "," << mv->get_matrix().translation().z() << ")" + << " object_bbox.min=(" << vol_bb.min.x() << "," << vol_bb.min.y() << "," << vol_bb.min.z() << ")" + << " object_bbox.max=(" << vol_bb.max.x() << "," << vol_bb.max.y() << "," << vol_bb.max.z() << ")"; + ++vi; + } + } + + if (pcfg.belt_preslice_global.value) { + // 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. + 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) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() + << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() + << ") new=(" << c.x() << "," << c.y() << ")"; + m_belt_global_xy_correction = Vec2d(c.x(), c.y()); + + 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 << ")"; + } else { + // Slicing rotation in global mode: bed-position-dependent Z offset. + // For R(α, X): c.z = sin(α)*d.y so objects at different bed-Y + // values print at different machine Z values along the inclined belt. + if (pcfg.belt_slice_rotation_global.value + && pcfg.belt_slice_rotation.value != BeltRotationAxis::None + && std::abs(pcfg.belt_slice_rotation_angle.value) > EPSILON) { + 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(); + m_belt_global_xy_correction = Vec2d(c.x(), c.y()); + } + + // Pre-slice remap global mode: when on, the remap accounts for the + // instance bed position. The Z component of the correction + // (R - I) * d shifts layer print_z so e.g. a Y↔Z swap with an + // object at Y=50 prints at Z=50. + if (pcfg.preslice_remap_global.value + && BeltTransformPipeline::has_preslice_remap(pcfg)) { + Transform3d R = BeltTransformPipeline::build_preslice_remap(pcfg); + Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); + Vec3d remap_correction = R.linear() * d - d; + global_z_offset += remap_correction.z(); + } + } + + BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset + << " (relative to min across " << this->print()->objects().size() << " objects)"; + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() + << " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset; + m_belt_global_z_offset = global_z_offset; + // [BELT-DEBUG] Final breakdown of all contributions to layer.print_z + // and where the first / last layer end up post-adjustment. + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] global_z_offset breakdown" + << " obj=" << this->model_object()->name + << " belt_z_shift=" << belt_z_shift + << " total_global_z_offset=" << global_z_offset + << " xy_correction=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")" + << " belt_floor_z_shift_before=" << (m_slicing_params.belt_floor_z_shift) + << " n_layers=" << m_layers.size(); + 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-DEBUG] post-adjustment" + << " first_layer.print_z=" << m_layers.front()->print_z + << " last_layer.print_z=" << m_layers.back()->print_z + << " belt_floor_z_shift_after=" << m_slicing_params.belt_floor_z_shift; + } + 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 + BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice EXIT tid=" << std::this_thread::get_id() << " obj=" << this + << " layers=" << m_layers.size() << " belt_min_z=" << m_belt_min_z + << " belt_global_z_offset=" << m_belt_global_z_offset + << " belt_xy=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")"; this->set_done(posSlice); } @@ -1170,7 +1425,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 diff --git a/src/libslic3r/Slicing.hpp b/src/libslic3r/Slicing.hpp index f735ec5518..6b707d4041 100644 --- a/src/libslic3r/Slicing.hpp +++ b/src/libslic3r/Slicing.hpp @@ -111,6 +111,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..d2ffcfd397 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.cpp @@ -0,0 +1,124 @@ +#include "BeltFloorContext.hpp" + +#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 = (print_z - m_z_shift - m_floor_offset) / m_shear_factor; + 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..7b8b704ef6 --- /dev/null +++ b/src/libslic3r/Support/BeltFloorContext.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Slicing.hpp" +#include "../PrintConfig.hpp" + +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; + + // 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.hpp b/src/libslic3r/Support/SupportCommon.hpp index 531daaa0a8..f15c7d417a 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -144,6 +144,12 @@ 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); + } // namespace Slic3r #endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index cd60489e75..ae5ba5849e 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -5,6 +5,7 @@ #include "Print.hpp" #include "SupportMaterial.hpp" #include "SupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "Geometry.hpp" #include "Point.hpp" #include "MutablePolygon.hpp" @@ -367,10 +368,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"; @@ -443,11 +455,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 @@ -603,6 +616,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) @@ -1394,6 +1438,10 @@ 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 double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; float lower_layer_offset = 0; if (layer_id == 0) @@ -1443,9 +1491,17 @@ static inline ExPolygons detect_overhangs( // Overhang polygons for this layer and region. Polygons diff_polygons; Polygons layerm_polygons = to_polygons(layerm->slices.surfaces); + // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + Polygons effective_lower = lower_layer_polygons; + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(effective_lower, tilt_shift); + } 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. @@ -1454,9 +1510,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. @@ -1464,9 +1520,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. @@ -2506,6 +2562,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 @@ -2605,6 +2737,8 @@ 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 + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly; // Allocate empty surface areas, one per object layer. layer_support_areas.assign(object.total_layer_count(), Polygons()); @@ -2665,8 +2799,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 @@ -2680,6 +2815,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]; @@ -2718,6 +2862,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); @@ -2728,6 +2889,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; } @@ -3101,6 +3263,33 @@ 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; + if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly) + 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/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index 0267c0c327..ebc3d2c958 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -8,6 +8,7 @@ #include "TreeModelVolumes.hpp" #include "TreeSupportCommon.hpp" +#include "BeltFloorContext.hpp" #include "../BuildVolume.hpp" #include "../ClipperUtils.hpp" @@ -94,6 +95,29 @@ TreeModelVolumes::TreeModelVolumes( #else { m_anti_overhang = print_object.slice_support_blockers(); + // Belt floor: add belt surface polygons to anti_overhang so support + // is never generated inside the belt. Only in global shear mode — + // in local mode the belt floor clipping handles everything and + // anti_overhang at the bottom layers would block all support. + { + 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() + && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + size_t num_layers_needed = print_object.layer_count(); + // Ensure m_anti_overhang is large enough. + if (m_anti_overhang.size() < num_layers_needed) + m_anti_overhang.resize(num_layers_needed, Polygons{}); + for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { + double print_z = print_object.get_layer(layer_idx)->print_z + - print_object.belt_global_z_offset(); + append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z)); + } + } + } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -102,6 +126,27 @@ TreeModelVolumes::TreeModelVolumes( m_increase_until_radius = config.increase_radius_until_radius; m_radius_0 = config.getRadius(0); m_raft_layers = config.raft_layers; + // Belt printer: add virtual belt raft layers below the object, matching + // the extra layers added in generate_support_areas() so both use the + // same layer indexing. + { + const auto &sp2 = print_object.slicing_parameters(); + const auto &pcfg2 = print_object.print()->config(); + double belt_sf = sp2.belt_floor_shear_factor; + if (std::abs(belt_sf) > EPSILON && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + double bb_min_z = std::abs(belt_remapped_bbox(*print_object.model_object(), pcfg2).min.z()); + double extra_depth = bb_min_z + 10.; + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp2.layer_height)); + if (num_extra > 0) { + std::vector belt_layers; + belt_layers.reserve(num_extra); + for (int i = num_extra; i >= 1; --i) + belt_layers.push_back(sp2.first_object_layer_height - i * sp2.layer_height); + m_raft_layers.insert(m_raft_layers.begin(), belt_layers.begin(), belt_layers.end()); + } + } + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); @@ -114,6 +159,27 @@ 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, polygons_strictly_simple); }); + + // 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() + && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double { + // Use local print_z (subtract global offset from object layer). + return (layer_idx >= num_raft_layers) + ? print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset() + : 0.; + }); + } + } } #endif @@ -469,9 +535,9 @@ 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.size(); + 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) { @@ -517,9 +583,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, ClipperLib::jtMiter, 1.2)); } - collisions = processing_last_mesh && layer_idx < int(anti_overhang.size()) ? - union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::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, ClipperLib::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 eff4cadcaf..0ded395091 100644 --- a/src/libslic3r/Support/TreeModelVolumes.hpp +++ b/src/libslic3r/Support/TreeModelVolumes.hpp @@ -168,6 +168,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 0a8cda619d..904da75f7f 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -14,6 +14,7 @@ #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" #include "TreeSupport3D.hpp" +#include "BeltFloorContext.hpp" #include #include @@ -664,6 +665,14 @@ 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); +} + #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) { @@ -698,6 +707,11 @@ 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 double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -840,8 +854,19 @@ 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 + ExPolygons shifted_lower; + if (has_tilt) { + shifted_lower = lower_polys; // copy + const double lh = lower_layer->height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(shifted_lower, tilt_shift); + } + const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + // 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] = std::move(diff_ex(curr_polys, lower_layer_offseted)); double duration{ std::chrono::duration_cast(clock_::now() - t0).count() }; @@ -1769,6 +1794,136 @@ void TreeSupport::generate() + // Belt floor: extend support below the object's first layer by creating + // additional support layers with geometry copied from the lowest content + // layer and clipped at the belt surface. These layers bypass the tree + // algorithm entirely — they're pure geometry added after draw_circles(). + { + BeltFloorContext ctx; + if (ctx.init(m_slicing_params, *m_print_config) + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const auto &sp = m_slicing_params; + // Find the lowest non-empty, non-brim support layer. + ExPolygons source_areas; + double source_z = 0; + int layers_with_content = 0; + for (size_t i = 0; i < m_object->support_layer_count(); ++i) { + SupportLayer *sl = m_object->get_support_layer(i); + if (sl && !sl->base_areas.empty()) { + layers_with_content++; + if (layers_with_content >= 2) { + source_areas = sl->base_areas; + source_z = sl->print_z; + break; + } + } + } + // Fallback to first content layer. + if (source_areas.empty()) { + for (size_t i = 0; i < m_object->support_layer_count(); ++i) { + SupportLayer *sl = m_object->get_support_layer(i); + if (sl && !sl->base_areas.empty()) { + source_areas = sl->base_areas; + source_z = sl->print_z; + break; + } + } + } + // ORCA-Belt calibration: a counter-rotated calibration object + // stands on a support wedge that lies entirely below the object's + // first layer, where the tree pipeline has no layers at all — so + // no support content can exist yet. Seed the extension directly + // from the floating portion of the first layer (anything more + // than one layer height above the belt floor). For objects whose + // first layer rests on the belt the floating region is empty and + // behavior is unchanged. + double first_z = m_object->support_layer_count() > 0 ? m_object->get_support_layer(0)->print_z : 0.; + bool seeded = false; + if (source_areas.empty() && m_object_config->enable_support.value && !m_object->layers().empty()) { + // The layer grid may start with an empty ghost layer just below + // the object (grid rounding against the belt global Z offset) — + // anchor the seed to the first layer that has geometry. Object + // layer print_z and the floor plane are both in the globally + // offset frame here (belt_floor_z_shift was adjusted alongside + // the layer Z values in PrintObject::slice()). + const Layer *first_layer = nullptr; + for (const Layer *l : m_object->layers()) + if (!l->lslices_extrudable.empty()) { first_layer = l; break; } + if (first_layer != nullptr) { + ExPolygons floating = diff_ex(first_layer->lslices_extrudable, + ctx.surface_polygon(first_layer->bottom_z() - first_layer->height)); + BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge seed: obj=" << m_object->model_object()->name + << " bottom_z=" << first_layer->bottom_z() << " floating=" << floating.size(); + if (!floating.empty()) { + source_areas = std::move(floating); + first_z = first_layer->bottom_z(); + seeded = true; + } + } + } + if (!source_areas.empty()) { + BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); + double from_extent = std::abs(bb.min(ctx.from_axis())); + double bb_min_z = std::abs(bb.min.z()); + // Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed + // distance) + 10mm safety margin. The 10mm is a bodge to avoid + // small cutoff artifacts — ideally computed exactly from belt geometry. + double extra_depth = std::min(from_extent + bb_min_z + 10., std::max(0., first_z)); + if (seeded) { + // Seeded wedge: the depth is known exactly — down to the lowest + // belt-floor point under the floating footprint. The bbox + // heuristic above under-estimates it for meshes centered + // around their origin (every object loaded through the GUI). + double min_floor = first_z; + for (const ExPolygon &ep : source_areas) + for (const Point &pt : ep.contour.points) + min_floor = std::min(min_floor, ctx.floor_print_z(pt)); + extra_depth = std::min(std::max(0., first_z), first_z - min_floor + 2.); + } + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + // Seeded wedge: top layers become a dense support interface so the + // object's floating first layer bridges a roof, not sparse infill. + const int interface_layers = seeded ? std::max(0, m_object_config->support_interface_top_layers.value) : 0; + ExPolygons prev_areas = source_areas; + // Build belt extension layers (lowest Z first). + SupportLayerPtrs belt_ext_layers; + for (int i = num_extra; i >= 1 && !prev_areas.empty(); --i) { + double print_z = first_z - i * sp.layer_height; + if (print_z < -sp.layer_height) continue; + Polygons belt_surface = ctx.surface_polygon(print_z); + ExPolygons clipped = diff_ex(source_areas, belt_surface); + if (clipped.empty()) continue; + SupportLayer *sl = new SupportLayer(0, 0, m_object, sp.layer_height, print_z, -1); + sl->base_areas = clipped; + // Populate area_groups — generate_toolpaths() iterates these, + // not base_areas directly. + // Note: base areas only get infill when support_base_pattern + // is explicitly set (with the default pattern tree bases are + // walls-only) — the calibration flow sets rectilinear. + const bool roof = i <= interface_layers; + for (auto &expoly : sl->base_areas) { + sl->area_groups.emplace_back(&expoly, roof ? SupportLayer::RoofType : SupportLayer::BaseType, 0); + if (roof) + sl->area_groups.back().interface_id = i & 1; + } + sl->lslices = clipped; + sl->lslices_bboxes.reserve(clipped.size()); + for (const ExPolygon &ep : clipped) + sl->lslices_bboxes.emplace_back(get_extents(ep)); + belt_ext_layers.push_back(sl); + } + // Insert at the front of support_layers (they're already in Z order). + if (!belt_ext_layers.empty()) { + auto &sl_vec = m_object->support_layers(); + sl_vec.insert(sl_vec.begin(), belt_ext_layers.begin(), belt_ext_layers.end()); + BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge ext layers=" << belt_ext_layers.size() + << " z=" << belt_ext_layers.front()->print_z << ".." << belt_ext_layers.back()->print_z + << " seeded=" << seeded; + } + } + } + } + profiler.stage_start(STAGE_GENERATE_TOOLPATHS); m_object->print()->set_status(70, _u8L("Generating support")); generate_toolpaths(); @@ -2196,6 +2351,23 @@ 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. + // ts_layer->print_z is at LOCAL Z (global offset applied later in + // _generate_support_material), so use init_local to subtract + // belt_global_z_offset from z_shift. + { + BeltFloorContext ctx; + if (ctx.init_local(m_slicing_params, *m_print_config, m_object->belt_global_z_offset()) + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + 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; @@ -2662,6 +2834,10 @@ 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 size_t top_interface_layers = config.support_interface_top_layers.value; + const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && belt_floor_mode == BeltSupportFloorMode::GeneratorOnly; 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 @@ -2895,16 +3071,23 @@ 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); + // Belt floor: don't drop merged node below belt surface. + // Treat as object-surface termination (not buildplate) so + // the node gets floor/interface areas instead of base pads. + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_position)) { + node_parent->to_buildplate = 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); + m_ts_data->m_mutex.lock(); + contact_nodes[layer_nr_next].push_back(next_node); + m_ts_data->m_mutex.unlock(); + } neighbour->valid = false; p_node->valid = false; - m_ts_data->m_mutex.unlock(); } else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance. { @@ -2948,6 +3131,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: don't drop polygon node below belt surface. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_pt)) { + p_node->to_buildplate = false; + continue; + } SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next, p_node->support_roof_layers_below - (p_node->distance_to_top > 0 ? 1 : 0), to_buildplate, p_node, print_z_next, height_next); @@ -3093,6 +3282,12 @@ void TreeSupport::drop_nodes() if (is_outside) { next_layer_vertex = candidate_vertex; } } } + // Belt floor: don't drop regular node below belt surface. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_layer_vertex)) { + 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); SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next, @@ -3406,6 +3601,10 @@ 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 auto belt_floor_mode = m_print_config->belt_support_floor_mode.value; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && belt_floor_mode == BeltSupportFloorMode::GeneratorOnly; + //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; @@ -3493,6 +3692,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)) { @@ -3528,8 +3731,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 @@ -3663,6 +3868,20 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance poly.simplify(scale_(m_radius_sample_resolution), &outline); } + // Belt floor: add belt surface polygon to layer outlines so the + // collision system treats the belt as a physical surface. + { + BeltFloorContext ctx; + double local_print_z = layer->print_z - object.belt_global_z_offset(); + if (ctx.init_local(object.slicing_parameters(), object.print()->config(), + object.belt_global_z_offset()) + && object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + Polygons belt_surface = ctx.surface_polygon(local_print_z); + for (auto &p : belt_surface) + outline.emplace_back(ExPolygon(p)); + } + } + 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 e0446ad5f1..c3bd186df1 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -446,6 +446,10 @@ 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; + /*! diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 8242bd3f24..89e6c014f1 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -19,6 +19,7 @@ #include "Polygon.hpp" #include "Polyline.hpp" #include "MutablePolygon.hpp" +#include "BeltFloorContext.hpp" #include "SupportCommon.hpp" #include "TriangleMeshSlicer.hpp" #include "TreeSupport.hpp" @@ -209,6 +210,10 @@ 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 double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); + const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); + const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > 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; @@ -230,7 +235,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_x_rad, tilt_y_rad, &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); @@ -254,7 +259,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_src = to_polygons(lower_layer.lslices_extrudable); + if (has_tilt) { + const double lh = lower_layer.height; + Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + translate(lower_src, tilt_shift); + } + Polygons lower_layer_offseted = offset(lower_src, lower_layer_offset); overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted); if (lower_layer_offset == 0) { raw_overhangs = overhangs; @@ -3418,6 +3431,37 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // this struct is used to easy retrieve setting. No other function except those in TreeModelVolumes and generate_initial_areas() have knowledge of the existence of multiple meshes being processed. //FIXME this is a copy // Contains config settings to avoid loading them in every function. This was done to improve readability of the code. + // Belt printer: add virtual "belt raft" layers below the object so + // organic branches can extend below the model's first layer and + // terminate at the belt surface instead of creating a flat base at Z=0. + { + PrintObject &po = *print.get_object(processing.second.front()); + const auto &sp = po.slicing_parameters(); + const auto &pcfg = po.print()->config(); + BeltFloorContext ctx; + ctx.init_local(sp, pcfg, po.belt_global_z_offset()); + if (ctx.is_active() && std::abs(po.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + // z_shift_local is the belt surface height at Y=0 in local coords. + // Extend below the belt so the base expansion and build-plate + // termination happen inside the belt region and get clipped. + // Use the distance from the pre-shear bbox min Z to the part's + // post-shear min Z, plus 10mm for base expansion headroom. + double bb_min_z = std::abs(belt_remapped_bbox(*po.model_object(), pcfg).min.z()); + double extra_depth = bb_min_z + 10.; + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + if (num_extra > 0) { + // Insert belt raft layers at the front, from lowest Z to highest. + std::vector belt_layers; + belt_layers.reserve(num_extra); + for (int i = num_extra; i >= 1; --i) + belt_layers.push_back(sp.first_object_layer_height - i * sp.layer_height); + // Prepend to existing raft_layers (if any). + auto &rl = processing.first.raft_layers; + rl.insert(rl.begin(), belt_layers.begin(), belt_layers.end()); + } + } + } const TreeSupportSettings &config = processing.first; BOOST_LOG_TRIVIAL(info) << "Processing support tree mesh group " << counter + 1 << " of " << grouped_meshes.size() << " containing " << grouped_meshes[counter].second.size() << " meshes."; auto t_start = std::chrono::high_resolution_clock::now(); @@ -3601,6 +3645,27 @@ 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() + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + 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); @@ -3891,6 +3956,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); } @@ -3935,7 +4004,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; @@ -3960,6 +4032,9 @@ 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. + if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) + rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]); remove_small(rest_support, tiny_area); double rest_support_area = area(rest_support); if (rest_support_area < support_area_stop) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 3b032cc57e..bd5e428520 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -280,7 +280,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); @@ -341,8 +341,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]) @@ -367,7 +367,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) + bool force_reselection, const Vec3f &up_direction) { assert(facet_start < m_orig_size_indices); @@ -391,8 +391,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())); @@ -2515,7 +2515,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 11517f5c6c..365994eaed 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -309,6 +309,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 @@ -317,7 +318,8 @@ 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. - bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle + const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) void bucket_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 4b3e05df8d..3509b32257 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -1,4 +1,5 @@ #include "calib.hpp" +#include "BeltGCodeWriter.hpp" #include "BoundingBox.hpp" #include "Config.hpp" #include "Model.hpp" @@ -587,21 +588,21 @@ 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())); // create anchoring frame - 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(); - gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(), + 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); @@ -627,15 +628,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), @@ -646,20 +647,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); } @@ -678,20 +679,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)); for (int k = 0; k < wall_count(); ++k) { to_x += std::cos(to_radians(m_corner_angle) / 2) * side_length; @@ -701,27 +702,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 } @@ -729,7 +730,7 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna } } - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << m_writer->set_pressure_advance(m_params.start); gcode << "; end pressure advance pattern for layer\n"; CustomGCode::Item item; @@ -796,13 +797,36 @@ 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(); + belt_writer->set_belt_back_transform(print_config); + belt_writer->set_machine_frame_transform(print_config); + belt_writer->set_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 (dynamic_cast(m_writer.get()) != nullptr) { + 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 cbac1d3f86..b36c75db62 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -358,7 +358,10 @@ private: const Calib_Params &m_params; - GCodeWriter m_writer; + // Polymorphic so belt printers get a BeltGCodeWriter in world-coordinates + // mode (_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/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index b69ce2bdf8..fa9acffb50 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,6 +387,9 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); + // Belt printer: bed rotation is applied inside render_model() and render_default() + // using m_is_belt_printer and m_belt_angle members. + switch (m_type) { case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } @@ -394,6 +397,10 @@ 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); + render_slicing_arrow(view_matrix, projection_matrix); + render_slicing_plane(view_matrix, projection_matrix); + glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -692,7 +699,13 @@ 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); + // Belt printer: rotate the bed model about the tilt axis so the belt tilt + // is visible. Negative angle: belt surface tilts downward away from the nozzle. + if (m_is_belt_printer && m_belt_angle > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + model_matrix = Eigen::AngleAxisd(-angle_rad, belt_tilt_unit_axis()) * model_matrix; + } 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); @@ -731,6 +744,173 @@ 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) +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + // 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. + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) { + m_gravity_arrow.reset(); + return; + } + + // Gravity direction (matching the slicer's tilt convention) + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + + // Build the arrow model (same dimensions as the axis arrows) + if (!m_gravity_arrow.is_initialized()) { + const float stem_length = Axes::DefaultStemLength; + const float tip_radius = Axes::DefaultTipRadius; + const float tip_length = Axes::DefaultTipLength; + const float stem_radius = stem_length / 75.f; // same ratio as axis cylinders + m_gravity_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); + } + + // The arrow model points along +Z by default. Compute rotation to align with gravity_dir. + // Rotation axis = cross(+Z, gravity_dir), angle = acos(dot(+Z, gravity_dir)) + Vec3d from = Vec3d::UnitZ(); + Vec3d to = gravity_dir; + double dot = from.dot(to); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + // Nearly opposite — rotate 180° 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 = 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_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (!m_is_belt_printer || m_belt_angle <= 0.f) + return; + + // Build the arrow model: shorter and wider than the gravity arrow. + if (!m_slicing_arrow.is_initialized()) { + const float stem_length = 15.0f; // shorter than gravity arrow (25) + const float stem_radius = 1.0f; // wider than gravity arrow (~0.33) + const float tip_radius = 3.0f; // wider tip + const float tip_length = 5.0f; + m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); + } + + // The slicing direction: layers stack along the gantry normal, i.e. the image of + // +Z under the mesh rotation about the tilt axis. Use the same AngleAxis as the + // slicing pipeline so the arrow matches whichever tilt axis is configured. + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Vec3d slice_dir = (Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis()).toRotationMatrix() + * Vec3d::UnitZ()).normalized(); + + // Compute rotation to align +Z (arrow default) with slice_dir. + Vec3d from = Vec3d::UnitZ(); + double dot = from.dot(slice_dir); + Transform3d rot = Transform3d::Identity(); + if (dot < -0.9999) { + rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; + } else if (dot < 0.9999) { + Vec3d axis = from.cross(slice_dir).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; + + // Disable depth test so the arrow is always visible (not occluded by the tilted bed). + glsafe(::glDisable(GL_DEPTH_TEST)); + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d model_matrix = rot; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_slicing_arrow.set_color({ 1.0f, 0.2f, 0.6f, 1.0f }); // pink + m_slicing_arrow.render(); + + shader->stop_using(); + glsafe(::glEnable(GL_DEPTH_TEST)); +} + +void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (!m_is_belt_printer || m_belt_angle <= 0.f) + return; + + // Build a quad in the XZ plane (world frame) representing the belt slicing plane. + // The plane is tilted at belt_angle from horizontal, with normal (0, -sin(a), cos(a)). + // We render it as a semi-transparent quad centered on the build plate. + if (!m_slicing_plane.is_initialized()) { + const float half_size = 120.f; // mm, large enough to be visible + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(2); // 2 triangles + + // Quad corners in local frame (XY plane, will be rotated to match slicing plane) + Vec3f n = Vec3f::UnitZ(); + init_data.add_vertex(Vec3f(-half_size, -half_size, 0.f), n); + init_data.add_vertex(Vec3f( half_size, -half_size, 0.f), n); + init_data.add_vertex(Vec3f( half_size, half_size, 0.f), n); + init_data.add_vertex(Vec3f(-half_size, half_size, 0.f), n); + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(0, 2, 3); + + m_slicing_plane.init_from(std::move(init_data)); + } + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + shader->start_using(); + + // Show a tilted plane representing the slicing direction. + // The slicing plane is rotated by belt_angle about the tilt axis from horizontal. + // Raise it slightly so it's visible above the bed surface. + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Transform3d model_matrix = Transform3d::Identity(); + model_matrix.translate(Vec3d(0., 0., 30.)); + model_matrix.rotate(Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis())); + + shader->set_uniform("view_model_matrix", view_matrix * model_matrix); + shader->set_uniform("projection_matrix", projection_matrix); + + m_slicing_plane.set_color({ 0.2f, 0.6f, 1.0f, 0.3f }); // semi-transparent blue + m_slicing_plane.render(); + + glsafe(::glDisable(GL_BLEND)); + shader->stop_using(); +} + void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); @@ -741,7 +921,15 @@ void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Tr if (shader != nullptr) { shader->start_using(); - shader->set_uniform("view_model_matrix", view_matrix); + // Belt printer: rotate the default bed about X so the belt tilt is visible. + Transform3d view_model_matrix = view_matrix; + if (m_is_belt_printer && m_belt_angle > 0.f) { + double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); + Transform3d belt_rotation = Transform3d::Identity(); + belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); + view_model_matrix = view_matrix * belt_rotation; + } + shader->set_uniform("view_model_matrix", view_model_matrix); shader->set_uniform("projection_matrix", projection_matrix); glsafe(::glEnable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index b791635fd3..788d8f52a3 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -110,6 +110,9 @@ private: //GLTexture m_temp_texture; GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; + GLModel m_gravity_arrow; + GLModel m_slicing_arrow; // Pink arrow showing the effective slicing direction + GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; float m_scale_factor{ 1.0f }; @@ -119,6 +122,11 @@ private: std::vector> m_extruder_shapes; std::vector m_extruder_heights; bool m_is_dark = false; + // Belt printer state for rendering. + bool m_is_belt_printer = false; + float m_belt_angle = 0.f; + // Tilt axis: 0 = X (belt travels along Y, the common case), 1 = Y. + int m_belt_tilt_axis = 0; public: Bed3D() = default; @@ -138,6 +146,16 @@ 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; } + + // Belt printer bed settings. tilt_axis: 0 = X (belt along Y), 1 = Y. + void set_belt_printer(bool enabled, float angle_deg, int tilt_axis = 0) { + m_is_belt_printer = enabled; m_belt_angle = angle_deg; m_belt_tilt_axis = tilt_axis; + } + bool is_belt_printer() const { return m_is_belt_printer; } + float belt_angle() const { return m_belt_angle; } + // Unit vector of the tilt axis in bed space. + Vec3d belt_tilt_unit_axis() const { return m_belt_tilt_axis == 1 ? Vec3d::UnitY() : Vec3d::UnitX(); } // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } @@ -177,7 +195,10 @@ 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); + void render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); + void render_slicing_plane(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 ec7deeb82a..14cab0cf73 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1108,11 +1108,27 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, //use -1 ad a invalid type shader->set_uniform("print_volume.type", -1); } - + + const float normal_z = get_selection_support_normal_z(); + + // Compute up direction accounting for build plate tilt + Vec3f up_direction = Vec3f::UnitZ(); + { + const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg != 0. || tilt_y_deg != 0.) { + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + } + } + shader->set_uniform("volume_world_matrix", volume.first->world_matrix()); 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.normal_z", 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/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index d39a3fdfcb..30e965504d 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -605,9 +605,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co 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. + bool is_belt_printer = false; + { + const auto *belt_opt = preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_opt) + is_belt_printer = belt_opt->value; + } + 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)); @@ -734,13 +742,20 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co } } - 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); + bool have_brim = (config->opt_enum("brim_type") != btNoBrim) && !is_belt_printer; + if (is_belt_printer) + toggle_field("brim_type", false); toggle_field("brim_object_gap", have_brim); toggle_field("brim_use_efc_outline", have_brim); toggle_field("combine_brims", have_brim); @@ -764,7 +779,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co // 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"); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 150a40dc0d..9d9090480f 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -24,6 +24,8 @@ #include "GLToolbar.hpp" #include "GUI_Preview.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/BeltTransform.hpp" +#include "libslic3r/GCode/MachineFrameTransform.hpp" #include "libslic3r/Layer.hpp" #include "Widgets/ProgressDialog.hpp" #include "MsgDialog.hpp" @@ -1118,6 +1120,59 @@ 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 (BeltGCodeWriter:: +// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X = model if +// gcode_back_transform (write already un-rotated to Cartesian) else BeltForward( +// model). So the inverse is: +// model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1 +// All parts are config-driven affines -> handles any rotation/shear/scale/axis- +// remap combination. (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). NB: this is the + // transpose of the column convention used by BeltTransformPipeline::build_preslice_remap + // — the two remaps are not interchangeable. (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(); + + Transform3d bf_inv = Transform3d::Identity(); + if (!cfg.gcode_back_transform.value) + bf_inv = BeltTransformPipeline::build_forward_transform(cfg).inverse(); + + return bf_inv * 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, @@ -1125,13 +1180,25 @@ 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 loaded print here. Plater::set_bed_shape also calls set_belt_printer(), + // but only on bed-shape changes — not reliably on every slice/preview load — so the UI was + // staying hidden even though the (config-driven) designed view rendered. The tilt magnitude + // comes from the G-code header (gcode_result.belt_tilt_angle, abs of the slicing rotation). + m_belt_view_enabled = print.config().belt_printer.value; + 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) m_viewer.toggle_top_layer_only_view_range(); - // 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. + // Belt printers are exempt: the toolpath geometry fed to libvgcode depends on + // the current designed/raw view state (back-transform applied in convert), so + // re-running the conversion is required for the upright view and for toggling + // it (hotkey B) to take effect even when the G-code itself is unchanged. + if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && !print.config().belt_printer.value) { //BBS: add logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result); @@ -1172,8 +1239,89 @@ 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 any mesh rotation + shear + axis remap). When off, the raw + // machine-frame G-code is shown (useful for debugging the transform itself). + const bool is_belt = 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; + for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves) + if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) { // skip layer-0 prime/skirt + ++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; + } + } + // 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; + BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor d=[" << d.x() << "," << d.y() << "," << d.z() + << "] (clip kept " << n_clip << "/" << n_filtered << " moves)"; + } + } + 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 @@ -1342,6 +1490,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_max_print_height = gcode_result.printable_height; m_z_offset = gcode_result.z_offset; + // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); // ORCA: Apply smart default view type when extruder count changes. @@ -2301,7 +2450,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); @@ -4683,6 +4839,32 @@ 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); + // Belt printer: toggle for viewing designed (upright) vs. machine-frame G-code. + // Rendered with a separator and hint text so users can find it easily. + if (m_belt_view_enabled) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt Printer").c_str()); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + // Checked = show the raw machine-frame G-code (designed/upright view off). Worded to + // match the canvas-toolbar menu item "Show raw G-code (belt only)". m_belt_show_designed + // is the inverse of this checkbox, so bind a temporary and flip it on change. + bool show_raw = !m_belt_show_designed; + if (ImGui::Checkbox(_u8L("Show raw G-code (belt only) [B]").c_str(), &show_raw)) { + m_belt_show_designed = !show_raw; + // The designed-view back-transform is baked into the toolpath geometry at load + // time, so the toggle only takes effect once the preview is re-converted. Defer + // the refresh to the next event-loop tick (CallAfter) to avoid re-entering the + // preview load from inside legend rendering. + if (Plater* plater = wxGetApp().plater()) + plater->CallAfter([plater]() { plater->refresh_belt_view(); }); + } + } + 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 4958fc25a8..a0dc4ab3f7 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -243,6 +243,11 @@ private: mutable bool m_no_render_path { false }; bool m_is_dark = false; + bool m_belt_view_enabled = false; + float m_belt_angle_deg = 0.f; + bool m_belt_show_designed = true; // Toggle: designed (upright, back-transformed) view by default; + // turn off (hotkey B) to inspect the raw machine-frame G-code. + libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -342,6 +347,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 eb657953dc..e948860345 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3506,8 +3506,20 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } - //case 'B': - //case 'b': { zoom_to_bed(); break; } + case 'B': + case 'b': { + // Toggle belt printer "show designed" view when in G-code preview with belt mode active. + if (dynamic_cast(m_canvas->GetParent()) != nullptr && + m_gcode_viewer.is_belt_view()) { + m_gcode_viewer.toggle_belt_show_designed(); + // The designed-view back-transform is baked into the toolpath geometry at load + // time, so the toggle only takes visual effect once the preview is re-converted. + if (Plater* plater = wxGetApp().plater()) + plater->refresh_belt_view(); + m_dirty = true; + } + break; + } case 'C': case 'c': { wxGetApp().toggle_show_gcode_window(); m_dirty = true; request_extra_frame(); break; } //case 'G': @@ -9182,6 +9194,21 @@ 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()); }; + // Belt printers, G-code preview only: toggle the designed (upright) view vs the raw + // machine-frame G-code. Same state as hotkey B and the legend checkbox; 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()) { + 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::Separator(); + } + create_menu_item( _utf8(L("3D Navigator")), m_canvas_type != ECanvasType::CanvasAssembleView, // not work on assembly wxGetApp().show_3d_navigator(), diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index e7f789820c..7325149169 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -63,6 +63,7 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF = { L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap", "support_base_pattern", "support_on_build_plate_only","support_critical_regions_only", "support_remove_small_overhang", + "build_plate_tilt_x", "build_plate_tilt_y", "support_base_pattern_spacing", "support_expansion"}}, //BBS { L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} } @@ -92,7 +93,8 @@ std::map> SettingsFactory::OBJECT_C {"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} + {"build_plate_tilt_x","",33},{"build_plate_tilt_y","",34}, + {"support_object_first_layer_gap","",35} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index d56439f05a..8aa2631ebe 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2593,7 +2593,7 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name Slic3r::save_object_mesh(*new_object); // BBS: find an empty cell to put the copied object - auto start_point = wxGetApp().plater()->build_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 ea3ae5d21d..b56613e165 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -351,6 +351,17 @@ 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, + // and belt printers are exempt from the same-result-id load cache 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 (Z) range and only-gcode mode so the view doesn't jump on toggle. + m_loaded_print = nullptr; + load_print(true /*keep_z_range*/, m_only_gcode); +} + //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 5469e32b71..d8c137574b 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -141,6 +141,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 dc2ec0d46e..02703cfb42 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -526,6 +526,14 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle() return auto_support ? support_threshold_angle : 0; } +std::pair GLGizmoFdmSupports::get_build_plate_tilt() +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x = cfg.opt_float("build_plate_tilt_x"); + double tilt_y = cfg.opt_float("build_plate_tilt_y"); + return {tilt_x, tilt_y}; +} + void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { float threshold = (float(M_PI)/180.f)*threshold_deg; @@ -533,6 +541,12 @@ 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 + auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); + double tilt_x_rad = tilt_x_deg * M_PI / 180.0; + double tilt_y_rad = tilt_y_deg * M_PI / 180.0; + Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { if (! mv->is_model_part()) @@ -541,10 +555,8 @@ 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 = std::cos(threshold); // Now calculate dot product of vert_direction and facets' normals. int idx = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index f42d327a0e..73e5be59f0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -60,6 +60,7 @@ private: void select_facets_by_angle(float threshold, bool block); // BBS int get_selection_support_threshold_angle(); + std::pair get_build_plate_tilt(); int m_support_threshold_angle = -1; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 8f0cd9a43d..b64e2f70ed 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -73,6 +73,18 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl return clp_data_out; } +Vec3f GLGizmoPainterBase::get_tilt_up_direction() const +{ + const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); + double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); + if (tilt_x_deg == 0. && tilt_y_deg == 0.) + return Vec3f::UnitZ(); + double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); + double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); + return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); +} + void GLGizmoPainterBase::render_triangles(const Selection& selection) const { auto* shader = wxGetApp().get_shader("mm_gouraud"); @@ -119,11 +131,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) @@ -691,7 +707,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, true, get_tilt_up_direction()); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -803,7 +819,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; @@ -855,7 +871,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, true, get_tilt_up_direction()); 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); @@ -874,12 +890,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()); } } } @@ -953,7 +969,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, false, get_tilt_up_direction()); 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 8ec1cb1fb0..21ee989dd7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -281,6 +281,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/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index 5b5c7da076..015947e9bb 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -189,10 +189,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) { @@ -221,7 +233,7 @@ 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.extruder_id), static_cast(curr.cp_color_id), { 0.0f, 0.0f }, @@ -229,7 +241,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve /* 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.extruder_id), static_cast(curr.cp_color_id), { 0.0f, 0.0f }, @@ -242,7 +254,7 @@ 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), @@ -251,7 +263,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve /* 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.extruder_id), static_cast(curr.cp_color_id), curr.time, @@ -263,6 +275,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 35b5bd7b44..872f297ae6 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.hpp @@ -71,7 +71,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/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2dc92f3b4b..c3b38a5e0e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7146,7 +7146,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) @@ -11365,6 +11365,32 @@ 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); + // Belt printer: configure build volume and bed rendering for belt mode. + { + const auto *belt_opt = config->option("belt_printer"); + bool is_belt = belt_opt && belt_opt->value; + if (is_belt) { + // The slicing rotation is the single source of truth for the belt tilt: + // its magnitude is the physical tilt angle and its axis is the tilt axis. + auto rot_axis = config->option>("belt_slice_rotation")->value; + double rot_angle = config->opt_float("belt_slice_rotation_angle"); + double belt_angle = std::abs(rot_angle); // physical tilt magnitude + int tilt_axis = (rot_axis == BeltRotationAxis::Y) ? 1 : 0; + bool infinite_y = config->opt_bool("belt_printer_infinite_y"); + bed.build_volume().set_belt_printer(true, belt_angle, infinite_y); + bed.set_belt_printer(true, static_cast(belt_angle), tilt_axis); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); + // The belt "designed view" back-transform is rebuilt from the print config at + // G-code load time (GCodeViewer::compute_belt_back_transform), so no mesh-side + // inverse needs to be pushed to the viewer here. + } else { + bed.set_belt_printer(false, 0.f); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + } + 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"); @@ -12615,8 +12641,181 @@ void Plater::add_model(bool imperial_units, std::string fname) } } +// 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(); + + { + const BoundingBoxf3 rb = obj->raw_bounding_box(); + const Vec3d io = inst->get_offset(); + const Vec3d ir = inst->get_rotation(); + BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] helper exit: obj=" << obj->name + << " inst_offset=(" << io.x() << "," << io.y() << "," << io.z() << ")" + << " inst_rot=(" << ir.x() << "," << ir.y() << "," << ir.z() << ")" + << " vol0_offset=(" << obj->volumes.front()->get_offset().x() << "," + << obj->volumes.front()->get_offset().y() << "," << obj->volumes.front()->get_offset().z() << ")" + << " raw_bbox=(" << rb.min.x() << "," << rb.min.y() << "," << rb.min.z() + << ")..(" << rb.max.x() << "," << rb.max.y() << "," << rb.max.z() << ")" + << " min_z=" << obj->min_z(); + } + } + + // 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 + // (BeltGCodeWriter::set_world_coordinates 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 = wxString::Format(L"Pressure Advance Test"); new_project(false, false, calib_pa_name); wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); @@ -12948,6 +13147,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(); } @@ -13125,6 +13325,8 @@ 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); @@ -13141,7 +13343,101 @@ void Plater::calib_temp(const Calib_Params& params) { wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); 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 calib_dir = Slic3r::resources_dir() + "/calib/temperature_tower/"; + std::string asset = calib_dir + "belt_temp_tower_" + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; + if (!boost::filesystem::exists(asset)) { + BOOST_LOG_TRIVIAL(warning) << "[belt_temp] no embossed provini for " << t_start << "->" << t_end + << ", falling back to 230_190 (embossed numbers will not match)"; + asset = calib_dir + "belt_temp_tower_230_190.stl"; + } + add_model(false, asset); + + // 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; + } + } + add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc"); auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; @@ -13211,6 +13507,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 = wxString::Format(L"Max volumetric speed test"); @@ -13277,6 +13674,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; @@ -13341,6 +13740,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); } @@ -13386,6 +13786,7 @@ void Plater::calib_VFA(const Calib_Params& params) cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower); } + _calib_apply_belt_mode(); p->background_process.fff_print()->set_calib_params(params); } @@ -13398,6 +13799,8 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) return; add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc")); + 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; @@ -13449,6 +13852,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); } @@ -13461,6 +13865,8 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) return; add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc")); + 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; @@ -13511,6 +13917,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); } @@ -13526,6 +13933,8 @@ void Plater::Calib_Cornering(const Calib_Params& params) ? "/calib/input_shaping/ringing_tower.drc" : (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : "/calib/cornering/SCV-V2.drc"); add_model(false, Slic3r::resources_dir() + cornering_model_path); + 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; @@ -13576,6 +13985,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); } @@ -13695,6 +14105,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 @@ -13729,6 +14148,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 176f0fdb8e..45ff9f8458 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -329,6 +329,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 + // toolpath geometry at load time. Keeps the current layer range and only-gcode mode. + void refresh_belt_view(); // SoftFever void calib_pa(const Calib_Params& params); @@ -952,6 +956,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/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 8391bb5d61..542248c026 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4,6 +4,7 @@ #include "PresetHints.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/BeltTransform.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -1271,6 +1272,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(); @@ -4489,6 +4497,83 @@ 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"); + belt_og->append_single_option_line("belt_printer_infinite_y"); + // 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. + { + Line line = { L("Belt tilt"), + L("Belt tilt axis and angle, applied as a mesh rotation before " + "slicing. Also drives bed rendering and support gravity tilt. " + "Isometric (no distortion); the back-transform inverts it before " + "the machine-frame remap.") }; + line.append_option(belt_og->get_option("belt_slice_rotation")); + line.append_option(belt_og->get_option("belt_slice_rotation_angle")); + line.append_option(belt_og->get_option("belt_slice_rotation_global")); + belt_og->append_line(line); + } + { + Line line = { L("Pre-slice axis remap"), + L("Remap model axes before slicing so the slicer's coordinate system matches " + "the physical bed orientation. For belt printers whose bed is NOT in the XY plane, " + "use this to swap axes so layers are stacked in the correct physical direction.") }; + line.append_option(belt_og->get_option("preslice_remap_x")); + line.append_option(belt_og->get_option("preslice_remap_y")); + line.append_option(belt_og->get_option("preslice_remap_z")); + line.append_option(belt_og->get_option("preslice_remap_global")); + belt_og->append_line(line); + } + belt_og->append_single_option_line("belt_preslice_global"); + belt_og->append_single_option_line("gcode_back_transform"); + { + Line line = { L("First layer plane"), + L("Reference plane used to decide which extrusions get first-layer " + "settings (no fan, slow speed, deferred temperature drop). On belt " + "printers, Auto resolves to the tilted belt-shear plane so that " + "first-layer treatment follows perpendicular distance from the belt " + "surface, not slicing layer index.") }; + line.append_option(belt_og->get_option("first_layer_plane")); + line.append_option(belt_og->get_option("first_layer_plane_offset")); + line.append_option(belt_og->get_option("first_layer_plane_thickness")); + belt_og->append_line(line); + } + // Support floor: split across lines so each setting's own mode controls + // its visibility (floor_mode = Develop, floor_offset = Advanced, z_offset_mode = Expert). + belt_og->append_single_option_line("belt_support_floor_offset"); + belt_og->append_single_option_line("belt_support_z_offset_mode"); + belt_og->append_single_option_line("belt_support_floor_mode"); + + // Machine-frame transform: the shear (tan) + scale (1/cos) 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. + { + auto mf = page->new_optgroup(L("Machine frame transforms"), L"param_advanced"); + { + Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; + line.append_option(mf->get_option("gcode_remap_x")); + line.append_option(mf->get_option("gcode_remap_y")); + line.append_option(mf->get_option("gcode_remap_z")); + mf->append_line(line); + } + { + Line line = { L("Machine-frame tilt"), + L("The machine-frame shear (tan) and scale (1/cos) are derived from " + "the belt tilt angle. Enable 'Decouple' to set an independent " + "machine-frame angle when the physical gantry tilt differs from " + "the slicing rotation.") }; + line.append_option(mf->get_option("belt_frame_tilt_decouple")); + line.append_option(mf->get_option("belt_frame_tilt_angle")); + mf->append_line(line); + } + } + option = optgroup->get_option("thumbnails"); option.opt.full_width = true; optgroup->append_single_option_line(option, "printer_basic_information_advanced#g-code-thumbnails"); @@ -4533,6 +4618,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"); + optgroup->append_single_option_line("build_plate_tilt_y"); optgroup = page->new_optgroup(L("Cooling Fan"), "param_cooling_fan"); Line line = Line{ L("Fan speed-up time"), optgroup->get_option("fan_speedup_time").opt.tooltip }; @@ -5459,6 +5546,44 @@ 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"); + 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", is_belt); + + // Remap, back-transform, and global mesh-transforms toggles are gated by belt + // mode here; finer mode-based visibility (Advanced vs Expert) is handled by + // each option's ConfigOptionMode in PrintConfig.cpp. + for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) + toggle_line(el, is_belt); + toggle_line("belt_preslice_global", is_belt); + + bool belt_global = is_belt && m_config->opt_bool("belt_preslice_global"); + + // preslice_remap_global: superseded by belt_preslice_global + toggle_option("preslice_remap_global", is_belt && !belt_global); + + // Rotation is the only mesh-side belt transform. Gray out its angle/global + // sub-options 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); + toggle_option("belt_slice_rotation_global", is_belt && rot_axis != BeltRotationAxis::None); + + // Machine-frame transform: derived from the belt tilt. Only the expert + // decouple override is exposed; its angle is enabled only when decoupled. + toggle_line("belt_frame_tilt_decouple", is_belt && expert_or_above); + toggle_option("belt_frame_tilt_angle", + is_belt && expert_or_above && m_config->opt_bool("belt_frame_tilt_decouple")); + + // First-layer plane: visible alongside the rest of belt-printer settings. + toggle_line("first_layer_plane", is_belt); + toggle_option("first_layer_plane_offset", is_belt); + toggle_option("first_layer_plane_thickness", is_belt); + + for (auto el : {"belt_support_floor_mode", "belt_support_floor_offset", "belt_support_z_offset_mode"}) + toggle_line(el, is_belt); const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads"); toggle_line("parallel_printheads_count", support_parallel_printheads); } @@ -5672,6 +5797,31 @@ 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. When belt mode is off, + // reset whichever tilt axis matches a leftover belt value so we don't clobber a + // manually-set tilt on a non-belt tilted printer. + 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)); + } else { + const auto tilt = BeltTransformPipeline::physical_tilt( + m_config->option>("belt_slice_rotation")->value, + m_config->opt_float("belt_slice_rotation_angle")); + double tx = m_config->opt_float("build_plate_tilt_x"); + double ty = m_config->opt_float("build_plate_tilt_y"); + if (tx != 0. && std::abs(tx - tilt.tilt_x_deg) < 0.01) + m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(0.)); + if (ty != 0. && std::abs(ty - tilt.tilt_y_deg) < 0.01) + m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(0.)); + } + toggle_options(); } diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index 8cf33c3851..47188e76d6 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -78,6 +78,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 BeltGCodeWriter 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) @@ -288,6 +301,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); @@ -314,6 +335,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(); } @@ -324,6 +348,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(); } @@ -360,6 +395,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: "); @@ -420,6 +466,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(); @@ -476,11 +527,27 @@ void Temp_Calibration_Dlg::on_start(wxCommandEvent& event) { m_params.start = start; m_params.end = end; 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 5ff0fbf3e3..2ddf056dc6 100644 --- a/src/slic3r/GUI/calib_dlg.hpp +++ b/src/slic3r/GUI/calib_dlg.hpp @@ -59,9 +59,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;