Belt Printer Initial Push (#14385)

# Description

Initial push - documentation available at #12998 

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
Joseph Robertson
2026-06-24 09:42:40 -05:00
committed by GitHub
107 changed files with 6704 additions and 404 deletions
@@ -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)}")
+29 -5
View File
@@ -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"
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+54
View File
@@ -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"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

@@ -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"
]
}
@@ -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"
]
}
@@ -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"
]
}
@@ -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)"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
]
}
@@ -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"
}
+1
View File
@@ -26,6 +26,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform vec4 uniform_color;
+3 -2
View File
@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -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.
+2 -1
View File
@@ -37,6 +37,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
@@ -85,7 +86,7 @@ void main()
color = LightBlue;
alpha = 1.0;
}
else if( transformed_normal.z < slope.normal_z - EPSILON)
else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
{
color = color * 0.5 + LightRed * 0.5;
alpha = 1.0;
+1
View File
@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
void main()
+1
View File
@@ -26,6 +26,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform vec4 uniform_color;
+3 -2
View File
@@ -23,6 +23,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform mat4 view_model_matrix;
@@ -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.
+2 -1
View File
@@ -37,6 +37,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
@@ -87,7 +88,7 @@ void main()
color = LightBlue;
alpha = 1.0;
}
else if( transformed_normal.z < slope.normal_z - EPSILON)
else if( dot(transformed_normal, slope.up_direction) < slope.normal_z - EPSILON)
{
color = color * 0.5 + LightRed * 0.5;
alpha = 1.0;
+1
View File
@@ -24,6 +24,7 @@ struct SlopeDetection
bool actived;
float normal_z;
mat3 volume_world_normal_matrix;
vec3 up_direction;
};
uniform SlopeDetection slope;
void main()
+76
View File
@@ -0,0 +1,76 @@
#include "BeltGCode.hpp"
#include "BeltGCodeWriter.hpp"
#include "BeltTransform.hpp"
#include "Print.hpp"
namespace Slic3r {
void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers)
{
if (!print.config().belt_printer.value)
return;
auto belt_writer = std::make_unique<BeltGCodeWriter>();
belt_writer->set_is_bbl_machine(is_bbl_printers);
// Axis remap and build volume max are set by base GCode after init_belt_writer returns.
belt_writer->set_belt_back_transform(print.config());
belt_writer->set_machine_frame_transform(print.config());
m_writer = std::move(belt_writer);
}
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
{
if (!print.config().belt_printer.value)
return;
const auto &full_cfg = print.full_print_config();
// Slicing rotation: the belt tilt (axis + angle) and the single source of truth
// for the physical tilt the G-code viewer uses to enable belt view.
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
file.write_format("; belt_slice_rotation_global = %d\n", print.config().belt_slice_rotation_global.value ? 1 : 0);
// Pre-slice remap configs
file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str());
file.write_format("; preslice_remap_y = %s\n", full_cfg.opt_serialize("preslice_remap_y").c_str());
file.write_format("; preslice_remap_z = %s\n", full_cfg.opt_serialize("preslice_remap_z").c_str());
file.write_format("; preslice_remap_global = %d\n", print.config().preslice_remap_global.value ? 1 : 0);
file.write_format("; belt_preslice_global = %d\n", print.config().belt_preslice_global.value ? 1 : 0);
// Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt
// tilt angle (or belt_frame_tilt_angle when decoupled).
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value);
}
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
{
// Global pre-slice mode: adjust origin using computed correction.
// Transform the origin through the belt pipeline so that
// back_transform(T * origin) = origin (correct machine position).
//
// Flags that trigger this path:
// belt_preslice_global — full pipeline (rotation * remap) is global
// preslice_remap_global — only the pre-slice remap is global
// belt_slice_rotation_global — slicing rotation treated as global (matches
// the per-instance Z-offset added in PrintObjectSlice.cpp)
// The XY origin adjustment uses the FULL forward transform, because the
// back_transform applied during G-code emission is always the inverse of
// the full pipeline.
bool use_global = m_config.belt_preslice_global.value
|| (m_config.preslice_remap_global.value
&& BeltTransformPipeline::has_preslice_remap(m_config))
|| (m_config.belt_slice_rotation_global.value
&& m_config.belt_slice_rotation.value != BeltRotationAxis::None
&& std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON);
if (!use_global || !m_config.belt_printer.value)
return;
// Adjust origin: transform through belt forward pipeline so that
// the back-transform correctly recovers model-space positions.
Transform3d T = BeltTransformPipeline::build_forward_transform(m_config);
Vec2d cur_origin = this->origin();
Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.);
Vec3d adjusted = T.linear() * origin3d;
this->set_origin(Vec2d(adjusted.x(), adjusted.y()));
}
} // namespace Slic3r
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "GCode.hpp"
namespace Slic3r {
// Belt-printer-specific GCode export.
//
// Inherits from GCode and overrides virtual hooks to:
// - Create a BeltGCodeWriter instead of a plain GCodeWriter
// - Write belt configuration to the G-code header
// - Adjust the origin for global pre-slice transforms when switching instances
// - Disable arc fitting (G2/G3 not supported on belt printers)
class BeltGCode : public GCode
{
protected:
void init_belt_writer(Print &print, bool is_bbl_printers) override;
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
bool should_disable_arc_fitting() const override { return true; }
};
} // namespace Slic3r
+264
View File
@@ -0,0 +1,264 @@
#include "BeltGCodeWriter.hpp"
#include "FirstLayerPlane.hpp"
#include "Geometry.hpp"
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace {
// Decide whether a particular destination point gets first-layer treatment.
// When the plane evaluator is active, distance from the plane wins; otherwise
// fall back to the layer-coarse m_is_first_layer flag set by the caller.
inline bool belt_point_on_first_layer(
const FirstLayerPlane *plane,
double first_layer_thickness_mm,
bool layer_first_flag,
const Vec3d &point_slicing_mm)
{
if (plane && plane->is_active())
return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm);
return layer_first_flag;
}
} // namespace
// ---- Belt configuration ---------------------------------------------------
void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config)
{
m_belt_back_transform.init_from_config(config);
}
void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config)
{
m_machine_frame_transform.init_from_config(config);
}
Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const
{
// Step 1+2: To Cartesian (back_transform + axis_remap).
// In world-coordinates mode (PA line / PA pattern calibration) the input
// already describes a point relative to the belt surface, so the
// slicer->world back-transform is skipped and only the machine kinematics
// (axis remap + frame shear/scale) are applied.
Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos);
Vec3d result = apply_axis_remap(after_back);
Vec3d after_remap = result;
// Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts
// as a global linear transform on the placed coords.
Vec3d final = m_machine_frame_transform.apply(result);
// [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z
// crosses an integer mm boundary) to keep the log volume manageable while
// still capturing one sample per ~5 layers. Shows the full pipeline so
// Case A vs Case B can be compared step-by-step.
static thread_local int s_last_logged_z = std::numeric_limits<int>::min();
int z_bucket = static_cast<int>(std::floor(pos.z() * 5.0)); // every 0.2mm
if (z_bucket != s_last_logged_z) {
s_last_logged_z = z_bucket;
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords"
<< " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")"
<< " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")"
<< " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")"
<< " final=(" << final.x() << "," << final.y() << "," << final.z() << ")"
<< " mft_active=" << m_machine_frame_transform.is_active()
<< " back_active=" << m_belt_back_transform.is_active();
}
return final;
}
// ---- Overridden movement methods ------------------------------------------
std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment)
{
m_pos(0) = point(0);
m_pos(1) = point(1);
this->set_current_position_clear(true);
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
// Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation)
Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
GCodeG1Formatter w;
w.emit_xyz(machine);
const bool first_layer_for_point = belt_point_on_first_layer(
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer,
Vec3d(point.x(), point.y(), m_pos.z()));
auto speed = first_layer_for_point
? this->config.get_abs_value("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<double>::epsilon())
force_no_extrusion = true;
if (!force_no_extrusion)
filament()->extrude(dE);
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
// Belt printer: transform and emit XYZ (Y and Z are coupled)
Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
GCodeG1Formatter w;
w.emit_xyz(machine);
if (!force_no_extrusion)
w.emit_e(filament()->E());
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion)
{
m_pos = point;
m_lifted = 0;
if (!force_no_extrusion)
filament()->extrude(dE);
Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) };
point_on_plate = to_machine_coords(point_on_plate);
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
if (!force_no_extrusion)
w.emit_e(filament()->E());
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z)
{
// Belt-specific override of travel_to_xyz.
// Key differences from base:
// 1. All coordinates go through to_machine_coords()
// 2. Always emit full XYZ (can't split XY and Z due to coupling)
// 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides)
Vec3d dest_point = point;
const bool first_layer_for_point = belt_point_on_first_layer(
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point);
auto travel_speed =
first_layer_for_point ? this->config.get_abs_value("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
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "GCodeWriter.hpp"
#include "GCode/BeltBackTransform.hpp"
#include "GCode/MachineFrameTransform.hpp"
namespace Slic3r {
class FirstLayerPlane;
// Belt-printer-specific GCode writer.
//
// Inherits from GCodeWriter and overrides movement methods to apply
// coordinate transformation (back-transform, axis remap, machine-frame
// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt).
class BeltGCodeWriter : public GCodeWriter
{
public:
BeltGCodeWriter() : GCodeWriter() {}
// Belt configuration (axis remap is inherited from GCodeWriter)
void set_belt_back_transform(const PrintConfig &config);
void set_machine_frame_transform(const PrintConfig &config);
Vec3d to_machine_coords(const Vec3d &pos) const;
// World-coordinates mode: incoming coordinates are treated as points
// relative to the physical belt surface (X across, Y along the belt,
// Z height above it) instead of slicing-frame coordinates — the
// slicer->world back-transform is skipped. Used by the PA line / PA
// pattern calibration generators, whose logical bed coordinates describe
// first-layer drawings on the build surface.
void set_world_coordinates(bool enable) { m_world_coordinates = enable; }
// First-layer plane: when set to a non-null active evaluator, travel
// speed selection consults the plane per-move and uses
// initial_layer_travel_speed for points within first_layer_height_mm
// of the plane (regardless of slicing layer index).
void set_first_layer_plane(const FirstLayerPlane *plane,
double first_layer_height_mm) {
m_first_layer_plane = plane;
m_first_layer_thickness_mm = first_layer_height_mm;
}
// Overridden movement methods
std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override;
std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override;
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override;
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override;
std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override;
std::string eager_lift(const LiftType type) override;
protected:
std::string _travel_to_z(double z, const std::string &comment) override;
private:
BeltBackTransform m_belt_back_transform;
MachineFrameTransform m_machine_frame_transform;
bool m_world_coordinates = false;
// Borrowed pointer; lifetime owned by GCode. null = inactive.
const FirstLayerPlane *m_first_layer_plane = nullptr;
double m_first_layer_thickness_mm = 0.;
};
} // namespace Slic3r
+143
View File
@@ -0,0 +1,143 @@
#include "BeltSliceStrategy.hpp"
#include "Model.hpp"
#include <limits>
#include <boost/log/trivial.hpp>
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
#include <iomanip>
#include <sstream>
#include <thread>
#endif
namespace Slic3r {
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
const PrintConfig &config,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z)
{
// 1. Standalone pre-slice axis remap (works without belt mode).
const bool has_remap = BeltTransformPipeline::has_preslice_remap(config);
if (has_remap)
trafo = BeltTransformPipeline::build_preslice_remap(config) * trafo;
// 2. Belt rotation — the sole mesh-side belt transform (matching
// BeltTransformPipeline::build_forward_transform). Only active in
// belt-printer mode.
bool has_rotation = false;
if (config.belt_printer.value) {
const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation);
if (has_rotation) {
Transform3d belt_xform = Transform3d::Identity();
belt_xform.linear() = rot;
trafo = belt_xform * trafo;
}
}
if (!has_remap && !has_rotation)
return;
// 3. Z-shift — detect if the mesh clips below the build plate after the
// transforms and lift it. Each mesh vertex must be brought into object space
// via mv->get_matrix() before applying the full trafo (which is in object
// space). Missing this on assemblies (where per-volume get_matrix() positions
// each volume within the object) would compute min_z against mesh-local vertex
// coordinates rather than object-space coordinates, so volumes translated along
// the slicer's Z axis would be silently excluded from the bound check.
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
// Capture the incoming trafo for diagnostic logging.
// This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift.
const Transform3d trafo_pre_shift = trafo;
auto log_mat = [](const Matrix3d &m) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(4);
ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "],"
<< "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "],"
<< "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]";
return ss.str();
};
auto log_vec3 = [](const Vec3d &v) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(4);
ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")";
return ss.str();
};
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter"
<< " has_rotation=" << has_rotation
<< " has_remap=" << has_remap
<< " trafo.linear=" << log_mat(trafo_pre_shift.linear())
<< " trafo.translation=" << log_vec3(trafo_pre_shift.translation())
<< " volumes=" << model_volumes.size();
#endif
double min_z = std::numeric_limits<double>::max();
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
int vol_idx = 0;
#endif
for (const ModelVolume *mv : model_volumes) {
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
if (!mv->is_model_part()) { ++vol_idx; continue; }
#else
if (!mv->is_model_part()) continue;
#endif
Transform3d vol_trafo = trafo * mv->get_matrix();
const auto &its = mv->mesh().its;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
// Per-volume bbox in mesh-frame and post-trafo slicer-frame.
Vec3d mesh_min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());
Vec3d mesh_max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
Vec3d slicer_min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());
Vec3d slicer_max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
double vol_min_z = std::numeric_limits<double>::max();
#endif
for (const stl_vertex &v : its.vertices) {
Vec3d vm = v.cast<double>();
Vec3d pt = vol_trafo * vm;
min_z = std::min(min_z, pt.z());
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
mesh_min = mesh_min.cwiseMin(vm);
mesh_max = mesh_max.cwiseMax(vm);
slicer_min = slicer_min.cwiseMin(pt);
slicer_max = slicer_max.cwiseMax(pt);
vol_min_z = std::min(vol_min_z, pt.z());
#endif
}
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx
<< "] id=" << mv->id().id << " name='" << mv->name << "'"
<< " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max)
<< " get_matrix.translation=" << log_vec3(mv->get_matrix().translation())
<< " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max)
<< " vol_min_z=" << vol_min_z;
++vol_idx;
#endif
}
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z
<< " z_shift_val=" << z_shift_val;
#endif
if (z_shift_val > 0.) {
Transform3d z_shift = Transform3d::Identity();
z_shift.matrix()(2, 3) = z_shift_val;
trafo = z_shift * trafo;
}
// out_belt_min_z is only meaningful in belt mode; the standalone-remap path
// never reported it.
if (out_belt_min_z && config.belt_printer.value) {
const double new_val = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id()
<< " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val;
#endif
*out_belt_min_z = new_val;
}
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit"
<< " final_trafo.linear=" << log_mat(trafo.linear())
<< " final_trafo.translation=" << log_vec3(trafo.translation());
#endif
}
} // namespace Slic3r
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include "libslic3r.h"
#include "Point.hpp"
#include "BeltTransform.hpp"
#include "PrintConfig.hpp"
#include "Model.hpp"
namespace Slic3r {
// Belt printer / pre-slice transform strategy.
//
// Composes, in order, the pre-slice mesh transforms applied before slicing:
// 1. Pre-slice axis remap (standalone — works without belt mode)
// 2. Belt rotation (the sole mesh-side belt transform; shear & scale are a
// g-code-side stage, see MachineFrameTransform)
// 3. Per-object Z-shift that lifts the mesh above the build plate
//
// Isolates this belt/remap-specific logic from the generic slicing pipeline in
// PrintObjectSlice.cpp.
class BeltSliceStrategy
{
public:
// Apply the pre-slice remap + belt rotation + Z-shift to `trafo` in place.
// No-op when neither a remap nor a belt rotation is configured.
//
// out_belt_min_z (if non-null) receives the minimum mesh Z after the
// transforms, but only in belt-printer mode — the standalone-remap path
// never reported it.
static void apply_preslice_transforms(Transform3d &trafo,
const PrintConfig &config,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z = nullptr);
};
} // namespace Slic3r
+223
View File
@@ -0,0 +1,223 @@
#include "BeltTransform.hpp"
#include "Model.hpp"
#include <limits>
namespace Slic3r {
// ---- Matrix builders ------------------------------------------------------
Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &config)
{
Transform3d pre_remap = Transform3d::Identity();
if (!has_preslice_remap(config))
return pre_remap;
int pre_rx = int(config.preslice_remap_x.value);
int pre_ry = int(config.preslice_remap_y.value);
int pre_rz = int(config.preslice_remap_z.value);
// Each remap value selects a source axis and sign.
auto remap_column = [](int r) -> Vec3d {
int axis = r % 3;
Vec3d col = Vec3d::Zero();
if (r < 3) col[axis] = 1.0; // +axis
else if (r < 6) col[axis] = -1.0; // -axis
else col[axis] = -1.0; // Rev: max - pos = -(pos - max)
return col;
};
Matrix3d remap_lin;
remap_lin.col(0) = remap_column(pre_rx);
remap_lin.col(1) = remap_column(pre_ry);
remap_lin.col(2) = remap_column(pre_rz);
pre_remap.linear() = remap_lin;
// Translation for Rev modes (needs build volume extents).
if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) {
BoundingBoxf bbox_bed(config.printable_area.values);
Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(),
config.printable_height.value);
Vec3d remap_trans = Vec3d::Zero();
auto add_rev = [&](int r, int out) {
if (r >= 6) remap_trans[out] = vol_max[r % 3];
};
add_rev(pre_rx, 0);
add_rev(pre_ry, 1);
add_rev(pre_rz, 2);
pre_remap.translation() = remap_trans;
}
return pre_remap;
}
Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
{
BeltRotationAxis axis = config.belt_slice_rotation.value;
double angle_deg = config.belt_slice_rotation_angle.value;
bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON;
if (has_rot_out) *has_rot_out = active;
if (!active)
return Matrix3d::Identity();
double angle_rad = Geometry::deg2rad(angle_deg);
Vec3d unit_axis;
switch (axis) {
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
default: return Matrix3d::Identity();
}
return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
}
Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
{
// Mesh-side belt transform: rotation applied after the pre-slice axis remap.
// (Shear & scale are a g-code-side stage, not part of the mesh transform.)
Transform3d pre_remap = build_preslice_remap(config);
Matrix3d rot = build_rotation_matrix(config);
Transform3d combined = Transform3d::Identity();
combined.linear() = rot;
combined = combined * pre_remap;
return combined;
}
// ---- Bounding box remap ---------------------------------------------------
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config)
{
int pre_rx = int(config.preslice_remap_x.value);
int pre_ry = int(config.preslice_remap_y.value);
int pre_rz = int(config.preslice_remap_z.value);
if (pre_rx == int(RemapAxis::PosX) &&
pre_ry == int(RemapAxis::PosY) &&
pre_rz == int(RemapAxis::PosZ))
return bb; // Identity remap.
auto remap_coord = [](int r, const Vec3d &v) -> double {
int axis = r % 3;
if (r < 3) return v[axis];
return -v[axis];
};
Vec3d mn = bb.min.cast<double>(), mx = bb.max.cast<double>();
BoundingBoxf3 rbb;
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? mx.x() : mn.x(),
(i & 2) ? mx.y() : mn.y(),
(i & 4) ? mx.z() : mn.z());
Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c));
if (i == 0) rbb = BoundingBoxf3(rc, rc);
else rbb.merge(rc);
}
return rbb;
}
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const ModelObject &model_object, const PrintConfig &config)
{
return remap_bbox(model_object.raw_bounding_box(), config);
}
// ---- Belt floor parameters ------------------------------------------------
// Shared implementation for both PrintConfig and DynamicPrintConfig.
// Template avoids duplicating the math for the two config types.
namespace {
template<typename Config>
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
const Config &config, const BoundingBoxf3 &bb, double original_height)
{
BeltTransformPipeline::BeltHeightResult result;
result.object_height = original_height;
// Extract the mesh rotation from config (the sole mesh-side belt transform).
BeltRotationAxis rot_axis;
double rot_angle;
if constexpr (std::is_same_v<Config, PrintConfig>) {
rot_axis = config.belt_slice_rotation.value;
rot_angle = config.belt_slice_rotation_angle.value;
} else {
// DynamicPrintConfig path
auto get_float = [&](const char *key) {
auto *opt = config.template option<ConfigOptionFloat>(key);
return opt ? opt->value : 0.0;
};
auto get_rot_axis = [&](const char *key) {
auto *opt = config.template option<ConfigOptionEnum<BeltRotationAxis>>(key);
return opt ? opt->value : BeltRotationAxis::None;
};
rot_axis = get_rot_axis("belt_slice_rotation");
rot_angle = get_float("belt_slice_rotation_angle");
}
bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON;
if (!has_rotation)
return result;
// Rotation path: sweep the 8 bbox corners through R to get the rotated height,
// then derive the belt floor (the image of machine-Z = 0 under R).
double angle_rad = Geometry::deg2rad(rot_angle);
Vec3d unit_axis;
switch (rot_axis) {
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
default: unit_axis = Vec3d::UnitX(); break;
}
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
double min_rz = std::numeric_limits<double>::max();
double max_rz = std::numeric_limits<double>::lowest();
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
(i & 2) ? bb.max.y() : bb.min.y(),
(i & 4) ? bb.max.z() : bb.min.z());
double z = (R * c).z();
min_rz = std::min(min_rz, z);
max_rz = std::max(max_rz, z);
}
result.object_height = max_rz - min_rz;
// Belt floor in slicer-frame is the image of z_machine = 0 under R.
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
switch (rot_axis) {
case BeltRotationAxis::X:
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
result.floor_params.from_axis = 1; // Y
break;
case BeltRotationAxis::Y:
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
result.floor_params.from_axis = 0; // X
break;
case BeltRotationAxis::Z:
default:
result.floor_params.shear_factor = 0.0;
result.floor_params.from_axis = 1;
break;
}
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
return result;
}
} // anonymous namespace
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height)
{
return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height);
}
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height)
{
return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height);
}
} // namespace Slic3r
+152
View File
@@ -0,0 +1,152 @@
#pragma once
#include "libslic3r.h"
#include "Point.hpp"
#include "BoundingBox.hpp"
#include "PrintConfig.hpp"
#include "Geometry.hpp"
#include <cmath>
namespace Slic3r {
class ModelObject;
// Shared belt-printer transform math.
//
// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
// trafo_out = z_shift * rotation * pre_remap * trafo_in
//
// Rotation is the sole mesh-side belt transform; shear & scale are applied
// to the g-code instead (see MachineFrameTransform). This class provides the
// building blocks so every call site uses the same implementation. z_shift is
// object-dependent (computed from mesh vertex bounds) and is NOT included in
// build_forward_transform(). The machine-frame shear/scale is derived directly
// from the tilt angle in MachineFrameTransform and no longer lives here.
//
// Design note: this mesh-rotation approach replaced an earlier pre-shear
// method (now removed). While that initial pre-shear method was instrumental
// in getting belt printer slicing off the ground in the first place, its place is
// in the past. A big thank you goes to the Unlayered3D team, who recommended
// switching to a pre-slice rotation stage instead. Doing so keeps the slicing
// operation isometric — no distortion of the sliced geometry — while the
// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale
// derived from the same tilt angle.
//
// This fixed a number of issues, including several issues noticed by hotcubcar
// regarding adaptive infills not working, gyroid becoming anisotropic, and more
// that were all mostly resolved as a result of the switch.
//
// This also means that the pre-slice rotation transform methodology can be used
// more cleanly on non-belt printers.
// - HarrierPigeon (Joseph Robertson)
class BeltTransformPipeline
{
public:
// ---- Identity checks --------------------------------------------------
static bool has_preslice_remap(const PrintConfig &config)
{
return int(config.preslice_remap_x.value) != int(RemapAxis::PosX) ||
int(config.preslice_remap_y.value) != int(RemapAxis::PosY) ||
int(config.preslice_remap_z.value) != int(RemapAxis::PosZ);
}
// Overload accepting DynamicPrintConfig (used in static slicing_parameters).
static bool has_preslice_remap(const DynamicPrintConfig &config)
{
auto get_int = [&](const char *key) -> int {
auto *opt = config.option<ConfigOptionEnum<RemapAxis>>(key);
return opt ? int(opt->value) : 0;
};
return get_int("preslice_remap_x") != int(RemapAxis::PosX) ||
get_int("preslice_remap_y") != int(RemapAxis::PosY) ||
get_int("preslice_remap_z") != int(RemapAxis::PosZ);
}
static bool has_rotation(const PrintConfig &config)
{
return config.belt_slice_rotation.value != BeltRotationAxis::None &&
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
}
// Physical belt tilt derived from the slicing rotation — the single source of
// truth for bed rendering, support gravity tilt and the bed-exclusion
// projection. Returns the tilt magnitude in degrees split onto the X and Y
// build-plate tilt axes according to the rotation axis:
// rotation about X → tilt_x = angle (gantry tilts in the YZ plane)
// rotation about Y → tilt_y = angle (gantry tilts in the XZ plane)
// rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt)
// The magnitude uses abs(angle) so a negative rotation still reports a positive
// physical tilt.
struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; };
static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg)
{
PhysicalTilt t;
double mag = std::abs(angle_deg);
switch (axis) {
case BeltRotationAxis::X: t.tilt_x_deg = mag; break;
case BeltRotationAxis::Y: t.tilt_y_deg = mag; break;
default: break; // Z / None: no physical tilt
}
return t;
}
static PhysicalTilt physical_tilt(const PrintConfig &config)
{
return physical_tilt(config.belt_slice_rotation.value,
config.belt_slice_rotation_angle.value);
}
// ---- Matrix builders --------------------------------------------------
// Build the pre-slice axis remap transform (includes Rev-mode translation).
static Transform3d build_preslice_remap(const PrintConfig &config);
// Build the 3x3 rotation matrix from belt_slice_rotation* config.
// Returns Identity if rotation axis is None or angle is ~0.
// Also sets has_rot_out if non-null.
static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
// Combined forward transform (rotation * pre_remap) — the mesh-side belt
// transform that BeltSliceStrategy applies and BeltBackTransform inverts.
// Does NOT include the per-object Z-shift.
static Transform3d build_forward_transform(const PrintConfig &config);
// ---- Bounding box remap -----------------------------------------------
// Remap a bounding box through the pre-slice axis remap.
// Returns the original bbox if remap is identity.
static BoundingBoxf3 remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config);
static BoundingBoxf3 remap_bbox(const ModelObject &model_object, const PrintConfig &config);
// ---- Belt floor parameters --------------------------------------------
struct BeltFloorParams {
double shear_factor = 0.0;
int from_axis = 1;
double z_shift = 0.0;
};
// Result of computing belt height + floor params.
struct BeltHeightResult {
double object_height; // Effective object height after shear/scale
BeltFloorParams floor_params;
};
// Compute effective object height and belt floor parameters from config
// and pre-remapped bounding box. original_height is the input height
// (bb.size().z() or model_object.max_z()).
static BeltHeightResult compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &remapped_bbox,
double original_height);
// Overload for DynamicPrintConfig (used by static slicing_parameters).
static BeltHeightResult compute_belt_height_and_floor(
const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox,
double original_height);
};
} // namespace Slic3r
+4
View File
@@ -895,6 +895,10 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
std::map<ObjectID, ExPolygons>* objectBrimAreasOut,
std::map<ObjectID, ExPolygons>* supportBrimAreasOut)
{
// Belt printer: brim is not compatible with belt printing.
if (print.config().belt_printer.value)
return;
std::map<ObjectID, double> brim_width_map;
std::map<ObjectID, ExPolygons> brimAreaMap;
std::map<ObjectID, ExPolygons> supportBrimAreaMap;
+30
View File
@@ -176,6 +176,31 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name();
}
void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y)
{
m_is_belt_printer = enabled;
m_belt_angle = angle_deg;
m_belt_infinite_y = infinite_y;
// Restart from the unmodified bbox each call. Without this, toggling
// belt mode off (or switching infinite_y true→false) would leave the
// extents inflated and break collision / object_state checks.
BoundingBoxf bboxf = get_extents(m_bed_shape);
m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) };
if (enabled) {
if (infinite_y) {
// Extend the Y bound to a very large value for infinite belt.
m_bboxf.max.y() = 100000.;
}
// Belt printer: the Z extent already equals printable_height (set above), which
// is the usable vertical clearance above the belt. The gantry's axis range is
// sized to reach height/cos(tilt), so no diagonal scaling is applied here — this
// keeps the live "outside build volume" highlight in agreement with Print::validate().
(void) angle_deg;
}
}
#if 0
// Tests intersections of projected triangles, not just their vertices against a bounding box.
// This test also correctly evaluates collision of a non-convex object with the bounding box.
@@ -384,6 +409,11 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i
build_volume.max.z() = std::numeric_limits<double>::max();
if (ignore_bottom)
build_volume.min.z() = -std::numeric_limits<double>::max();
// Belt printer: extend Y bounds for infinite Y.
if (m_is_belt_printer && m_belt_infinite_y) {
build_volume.min.y() = -std::numeric_limits<double>::max();
build_volume.max.y() = std::numeric_limits<double>::max();
}
BoundingBox3Base<Vec3f> build_volumef(build_volume.min.cast<float>(), build_volume.max.cast<float>());
// The following test correctly interprets intersection of a non-convex object with a rectangular build volume.
//return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z());
+9 -1
View File
@@ -57,6 +57,10 @@ public:
// Initialize from PrintConfig::printable_area and PrintConfig::printable_height
BuildVolume(const std::vector<Vec2d> &printable_area, const double printable_height, const std::vector<std::vector<Vec2d>> &extruder_areas, const std::vector<double>& extruder_printable_heights);
// Belt printer configuration.
void set_belt_printer(bool enabled, double angle_deg, bool infinite_y);
bool is_belt_printer() const { return m_is_belt_printer; }
// Source data, unscaled coordinates.
const std::vector<Vec2d>& printable_area() const { return m_bed_shape; }
double printable_height() const { return m_max_print_height; }
@@ -80,7 +84,7 @@ public:
indexed_triangle_set bounding_mesh(bool scale=true) const;
// Center of the print bed, unscaled.
Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
// Convex hull of polygon(), scaled.
const Polygon& convex_hull() const { return m_convex_hull; }
// Smallest enclosing circle of polygon(), scaled.
@@ -139,6 +143,10 @@ private:
// Source definition of the print volume height (PrintConfig::printable_height)
double m_max_print_height { 0.f };
std::vector<double> m_extruder_printable_height;
// Belt printer state.
bool m_is_belt_printer { false };
double m_belt_angle { 0. };
bool m_belt_infinite_y { false };
// Derived values.
BuildVolume_Type m_type { BuildVolume_Type::Invalid };
+16
View File
@@ -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
+5
View File
@@ -396,6 +396,11 @@ inline void translate(ExPolygons &expolys, const Point &p) {
expoly.translate(p);
}
inline void translate(Polygons &polys, const Point &p) {
for (Polygon &poly : polys)
poly.translate(p);
}
inline void polygons_append(Polygons &dst, const ExPolygon &src)
{
dst.reserve(dst.size() + src.holes.size() + 1);
+225
View File
@@ -0,0 +1,225 @@
#include "FirstLayerPlane.hpp"
#include "BeltTransform.hpp"
#include <algorithm>
#include <climits>
#include <cmath>
namespace Slic3r {
namespace {
// Build the row of the gcode-axis-remap matrix R that produces machine_Z,
// AS A FUNCTION OF a slicing-frame point in the GCode generator's coordinate
// space. Without back-transform this is just R.row(2). With back-transform
// the writer applies F^-1 before R, so the effective row is (R * F^-1).row(2).
//
// Returns a pair (gradient, constant) such that:
// machine_Z(p_slicing) = gradient.dot(p_slicing) + constant
struct MachineZAffine {
Vec3d gradient = Vec3d::UnitZ();
double constant = 0.0;
};
MachineZAffine compute_machine_z_affine(const PrintConfig &config)
{
MachineZAffine out;
// R is the matrix form of GCodeWriter::apply_axis_remap. Each output axis
// i picks one slicing-frame component (with sign + optional Rev mode
// translation) based on m_remap_{x,y,z}. We only need row 2 (the z output)
// since machine_Z is what defines the first-layer plane.
int rz = int(config.gcode_remap_z.value);
int axis = rz % 3;
double sign;
double trans;
if (rz < int(RemapAxis::NegX)) { // 0..2 = PosX/Y/Z
sign = 1.0;
trans = 0.0;
} else if (rz < int(RemapAxis::RevX)) { // 3..5 = NegX/Y/Z
sign = -1.0;
trans = 0.0;
} else { // 6..8 = RevX/Y/Z
sign = -1.0;
BoundingBoxf bbox_bed(config.printable_area.values);
Vec3d vol_max(bbox_bed.max.x(),
bbox_bed.max.y(),
config.printable_height.value);
trans = vol_max[axis];
}
Vec3d r_row = Vec3d::Zero();
r_row[axis] = sign;
// Without back-transform, machine_Z(slicing) = r_row · slicing + trans.
out.gradient = r_row;
out.constant = trans;
if (config.gcode_back_transform.value && config.belt_printer.value) {
// BeltGCodeWriter applies F^-1 before R when back-transform is on.
// So machine_Z(slicing) = r_row · (F^-1 · slicing) + trans
// = (r_row^T · F^-1) · slicing + trans
// We need to compose r_row with F^-1 from the LEFT (treating r_row as
// a row vector). Eigen makes this easy: it's just F^-1.transpose() * r_row.
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
Transform3d inverse = forward.inverse();
// Note: forward.translation() is normally zero (per-print transforms
// don't add a translation; the per-object z_shift is added separately
// in PrintObjectSlice). We still incorporate inverse.translation() in
// case a Rev-mode preslice_remap puts a translation in F.
Vec3d composed_grad = inverse.linear().transpose() * r_row;
double composed_trans =
r_row.dot(inverse.translation()) + trans;
out.gradient = composed_grad;
out.constant = composed_trans;
}
return out;
}
} // namespace
FirstLayerPlane::FirstLayerPlane(const PrintConfig &config)
{
// -------- Resolve Auto -------------------------------------------------
FirstLayerPlaneMode mode = config.first_layer_plane.value;
if (mode == FirstLayerPlaneMode::Auto) {
bool belt_affine_active = config.belt_printer.value &&
config.belt_slice_rotation.value != BeltRotationAxis::None &&
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
mode = belt_affine_active ? FirstLayerPlaneMode::BeltAffine
: FirstLayerPlaneMode::XY;
}
m_mode = mode;
// -------- Band thickness ----------------------------------------------
// Note: layer_height lives in PrintObjectConfig, not PrintConfig, so we
// can't fall back to it from here. initial_layer_print_height is in
// PrintConfig and is the right default anyway (the legacy first-layer
// semantics used initial_layer_print_height, not the regular one).
double thickness = config.first_layer_plane_thickness.value;
if (thickness <= 0.0)
thickness = config.initial_layer_print_height.value;
if (thickness <= 0.0)
thickness = 0.2;
m_thickness_mm = thickness;
const double user_offset = config.first_layer_plane_offset.value;
// -------- Build the plane ---------------------------------------------
auto set_axis_aligned = [&](const Vec3d &n_unit, double offset_along_n) {
m_normal = n_unit;
m_offset = offset_along_n;
};
switch (mode) {
case FirstLayerPlaneMode::XY:
// Legacy XY plane. Inactive: short-circuit to layer-index path.
set_axis_aligned(Vec3d::UnitZ(), user_offset);
m_active = false;
return;
case FirstLayerPlaneMode::YZ:
set_axis_aligned(Vec3d::UnitX(), user_offset);
m_active = true;
return;
case FirstLayerPlaneMode::XZ:
set_axis_aligned(Vec3d::UnitY(), user_offset);
m_active = true;
return;
case FirstLayerPlaneMode::BeltAffine: {
// Compute the slicing-frame plane that maps to machine_Z = user_offset
// under the gcode axis remap (and optional back-transform).
MachineZAffine mz = compute_machine_z_affine(config);
double cmag = mz.gradient.norm();
if (cmag < EPSILON) {
// Degenerate: slicing point doesn't affect machine_Z. Fall back.
set_axis_aligned(Vec3d::UnitZ(), user_offset);
m_active = false;
return;
}
// Plane equation: gradient · slicing = user_offset - constant
const double K = user_offset - mz.constant;
m_normal = mz.gradient / cmag;
m_offset = K / cmag;
m_active = true;
return;
}
case FirstLayerPlaneMode::Auto:
// Should have been resolved above.
m_active = false;
return;
}
m_active = false;
}
double FirstLayerPlane::distance_from_plane(const Vec3d &point_slicing_mm) const
{
return m_normal.dot(point_slicing_mm) - m_offset;
}
bool FirstLayerPlane::is_first_layer(const Vec3d &point_slicing_mm,
double first_layer_height_mm) const
{
if (!m_active)
return false;
return distance_from_plane(point_slicing_mm) < first_layer_height_mm;
}
int FirstLayerPlane::effective_layer_index(const Vec3d &point_slicing_mm) const
{
if (!m_active)
return INT_MAX / 2; // Effectively "way past first layer".
double d = distance_from_plane(point_slicing_mm);
if (d <= 0.0)
return 0;
return int(std::floor(d / m_thickness_mm));
}
int FirstLayerPlane::min_effective_index_for_xy_bbox(
const BoundingBoxf &xy_bbox_mm, double slicing_z_mm) const
{
if (!m_active)
return INT_MAX / 2;
// For the rectangular bbox in (x, y) at fixed z, the smallest value of
// (n.x*x + n.y*y + n.z*z - offset) is achieved at one of the four
// corners, with the smaller component picked when the corresponding
// normal coefficient is positive.
const double x_for_min = (m_normal.x() >= 0.0)
? xy_bbox_mm.min.x() : xy_bbox_mm.max.x();
const double y_for_min = (m_normal.y() >= 0.0)
? xy_bbox_mm.min.y() : xy_bbox_mm.max.y();
const double dmin = m_normal.x() * x_for_min
+ m_normal.y() * y_for_min
+ m_normal.z() * slicing_z_mm
- m_offset;
if (dmin <= 0.0)
return 0;
return int(std::floor(dmin / m_thickness_mm));
}
int FirstLayerPlane::min_effective_index_for_bbox3(
const BoundingBoxf3 &bbox_mm) const
{
if (!m_active)
return INT_MAX / 2;
const double x_for_min = (m_normal.x() >= 0.0)
? bbox_mm.min.x() : bbox_mm.max.x();
const double y_for_min = (m_normal.y() >= 0.0)
? bbox_mm.min.y() : bbox_mm.max.y();
const double z_for_min = (m_normal.z() >= 0.0)
? bbox_mm.min.z() : bbox_mm.max.z();
const double dmin = m_normal.x() * x_for_min
+ m_normal.y() * y_for_min
+ m_normal.z() * z_for_min
- m_offset;
if (dmin <= 0.0)
return 0;
return int(std::floor(dmin / m_thickness_mm));
}
} // namespace Slic3r
+76
View File
@@ -0,0 +1,76 @@
#ifndef slic3r_FirstLayerPlane_hpp_
#define slic3r_FirstLayerPlane_hpp_
#include "libslic3r.h"
#include "Point.hpp"
#include "BoundingBox.hpp"
#include "PrintConfig.hpp"
namespace Slic3r {
// Decides which extrusions get "first layer" treatment (no fan, slow speed,
// initial-layer accel/jerk, deferred temperature drop) by reference to a
// configurable plane in slicing-frame coordinates rather than the slicing
// layer index.
//
// On a normal flat-bed printer the plane is XY at slicing_Z = 0 and the
// evaluator is INACTIVE — every call site short-circuits back to the legacy
// `Layer::id() == 0` test. On a belt printer with a Z-from-Y shear the
// belt surface (machine_Z = 0) maps to a plane in slicing-frame coordinates
// derived from the gcode axis remap, so layer-index-based detection no
// longer matches the physical first printed surface.
//
// Plane representation: unit normal `n` (slicing frame) and offset along
// the normal such that the plane equation is `n · p == offset`. Signed
// perpendicular distance is `d(p) = n · p - offset`. Positive distance
// means "away from the belt surface", negative means "below the plane".
class FirstLayerPlane
{
public:
explicit FirstLayerPlane(const PrintConfig &config);
// Inactive when the legacy XY layer-index path should be used. This
// covers all non-belt printers and any belt printer where the user
// explicitly picked XY mode.
bool is_active() const { return m_active; }
FirstLayerPlaneMode effective_mode() const{ return m_mode; }
double band_thickness_mm() const { return m_thickness_mm; }
const Vec3d & normal() const { return m_normal; }
double plane_offset() const { return m_offset; }
// Signed perpendicular distance from a slicing-frame point to the plane.
double distance_from_plane(const Vec3d &point_slicing_mm) const;
// True if perpendicular distance < first_layer_height_mm. When the
// evaluator is inactive this returns false (call sites should fall back
// to the legacy per-layer path before reaching this function).
bool is_first_layer(const Vec3d &point_slicing_mm,
double first_layer_height_mm) const;
// floor((distance - 0) / band_thickness), clamped to [0, +inf). Used
// for "first N layers" thresholds (fan, slow_down_layers). Returns 0
// for points within the band. Returns INT_MAX/2 when inactive.
int effective_layer_index(const Vec3d &point_slicing_mm) const;
// Min effective index over a 2D bbox at a fixed slicing_Z. Used for
// layer-level decisions (e.g. temperature transition gate) where we
// don't want to walk every extrusion in the layer. For axis-aligned
// planes this is exact; for tilted planes it's a tight lower bound
// (the plane projection of the bbox's extreme corner).
int min_effective_index_for_xy_bbox(const BoundingBoxf &xy_bbox_mm,
double slicing_z_mm) const;
// Same as above but the bbox spans a Z range too.
int min_effective_index_for_bbox3(const BoundingBoxf3 &bbox_mm) const;
private:
bool m_active = false;
FirstLayerPlaneMode m_mode = FirstLayerPlaneMode::XY;
Vec3d m_normal = Vec3d::UnitZ(); // unit, slicing frame
double m_offset = 0.0; // n·p == m_offset
double m_thickness_mm = 0.0;
};
} // namespace Slic3r
#endif // slic3r_FirstLayerPlane_hpp_
+387 -218
View File
File diff suppressed because it is too large Load Diff
+53 -9
View File
@@ -4,6 +4,8 @@
#include "libslic3r.h"
#include "ExPolygon.hpp"
#include "GCodeWriter.hpp"
#include "BeltGCodeWriter.hpp"
#include "FirstLayerPlane.hpp"
#include "Layer.hpp"
#include "Point.hpp"
#include "PlaceholderParser.hpp"
@@ -206,16 +208,18 @@ public:
m_last_obj_copy(nullptr, Point(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max())),
// BBS
m_toolchange_count(0),
m_nominal_z(0.)
m_nominal_z(0.),
m_writer(std::make_unique<GCodeWriter>())
{}
~GCode() = default;
virtual ~GCode() = default;
public:
// throws std::runtime_exception on error,
// throws CanceledException through print->throw_if_canceled().
void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
void export_layer_filaments(GCodeProcessorResult* result);
//BBS: set offset for gcode writer
void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
// Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests.
const Vec2d& origin() const { return m_origin; }
@@ -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<LayerToPrint> collect_layers_to_print(const PrintObject &object);
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false);
static std::vector<std::pair<coordf_t, std::vector<LayerToPrint>>> collect_layers_to_print(const Print &print);
std::string generate_skirt(const Print &print,
@@ -518,7 +538,7 @@ private:
DynamicConfig m_calib_config;
// scaled G-code resolution
double m_scaled_resolution;
GCodeWriter m_writer;
std::unique_ptr<GCodeWriter> m_writer;
struct PlaceholderParserIntegration {
void reset();
@@ -609,6 +629,11 @@ private:
std::unique_ptr<CoolingBuffer> m_cooling_buffer;
std::unique_ptr<SpiralVase> m_spiral_vase;
// First-layer plane evaluator. Constructed once per print from the
// PrintConfig. is_active() == false on non-belt printers and on belt
// printers without a Z-axis shear; in that case all per-path plane
// checks short-circuit to the legacy Layer::id() == 0 path.
std::unique_ptr<FirstLayerPlane> m_first_layer_plane;
std::unique_ptr<PressureEqualizer> m_pressure_equalizer;
@@ -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;
+40
View File
@@ -0,0 +1,40 @@
#include "BeltBackTransform.hpp"
#include "../BeltTransform.hpp"
namespace Slic3r {
bool BeltBackTransform::init_from_config(const PrintConfig &config)
{
m_active = false;
m_inverse = Transform3d::Identity();
if (!config.belt_printer.value || !config.gcode_back_transform.value)
return false;
// Require at least one active transform to proceed.
bool has_global_rotation = config.belt_slice_rotation_global.value
&& config.belt_slice_rotation.value != BeltRotationAxis::None;
bool has_preslice_global = config.belt_preslice_global.value
|| config.preslice_remap_global.value;
if (!has_global_rotation && !has_preslice_global
&& !BeltTransformPipeline::has_preslice_remap(config))
return false;
// Build the forward pipeline (rotation * pre_remap) and store its inverse.
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
if (forward.isApprox(Transform3d::Identity()))
return false;
m_inverse = forward.inverse();
m_active = true;
return true;
}
Vec3d BeltBackTransform::apply(const Vec3d &pos) const
{
if (!m_active)
return pos;
return m_inverse * pos;
}
} // namespace Slic3r
+45
View File
@@ -0,0 +1,45 @@
#ifndef slic3r_BeltBackTransform_hpp_
#define slic3r_BeltBackTransform_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Reverses the pre-slice remap + shear + scale transforms that
// PrintObjectSlice.cpp applies to belt printer geometry, converting G-code
// coordinates from the sliced (remapped/sheared/scaled) frame back to the
// machine's real coordinate space.
//
// Initialized once from PrintConfig, then applied per-point in
// GCodeWriter::to_machine_coords() before axis remapping.
//
// Active when gcode_back_transform is true AND at least one of:
// - a shear axis has global mode enabled, or
// - a pre-slice axis remap is non-identity.
class BeltBackTransform
{
public:
BeltBackTransform() = default;
// Initialize from belt printer config. Rebuilds the same pre-slice remap,
// shear, and scale matrices as PrintObjectSlice.cpp and precomputes the
// affine inverse. Returns true if a non-identity back-transform was computed.
bool init_from_config(const PrintConfig &config);
// Apply the inverse transform to a point. Returns pos unchanged if
// no back-transform is active.
Vec3d apply(const Vec3d &pos) const;
// True if a non-identity back-transform is active.
bool is_active() const { return m_active; }
private:
bool m_active = false;
Transform3d m_inverse = Transform3d::Identity();
};
} // namespace Slic3r
#endif // slic3r_BeltBackTransform_hpp_
+227
View File
@@ -1,10 +1,14 @@
#include "../GCode.hpp"
#include "../FirstLayerPlane.hpp"
#include "CoolingBuffer.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <float.h>
#include <string_view>
#include <system_error>
#include <unordered_map>
@@ -28,6 +32,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t
m_num_extruders = std::max(ex.id() + 1, m_num_extruders);
m_extruder_ids.emplace_back(ex.id());
}
// Borrow the first-layer plane from the GCode generator. When inactive
// (non-belt printers and belt printers without Z shear), per-line fan
// re-evaluation is skipped and behavior is bit-identical to the legacy
// per-layer path.
m_first_layer_plane = gcodegen.first_layer_plane();
}
void CoolingBuffer::reset(const Vec3d &position)
@@ -328,6 +338,13 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b
std::vector<PerExtruderAdjustments> per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos);
float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments);
out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments);
// First-layer plane: per-segment fan re-evaluation post-pass. Walks
// the cooled-down gcode and inserts inline M106 commands at band
// crossings (where the path's perpendicular distance to the plane
// crosses close_fan_the_first_x_layers thresholds). No-op when
// the evaluator is inactive.
if (m_first_layer_plane && m_first_layer_plane->is_active())
out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched);
m_gcode.clear();
}
return out;
@@ -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<unsigned int>(std::max(0, m_config.part_cooling_fan_min_pwm.value));
// Track position in slicing-frame mm. Seed from m_current_pos which the
// CoolingBuffer keeps up-to-date across layers.
Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]);
// Track current main fan speed by parsing M106 commands as we walk so
// we can restore it after a band exit.
int current_main_fan = m_fan_speed;
int pre_band_main_fan = current_main_fan;
// Implicit initial state: assume the layer started "out of the band"
// (i.e., the layer-level fan setting from apply_layer_cooldown is in
// effect). The first movement we encounter will reconcile this.
bool in_first_layer_band = false;
unsigned int active_extruder = m_current_extruder;
auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) {
if (line_sv.size() < 3) return false;
if (line_sv[0] != 'G') return false;
if (line_sv[1] != '0' && line_sv[1] != '1') return false;
if (line_sv[2] != ' ' && line_sv[2] != '\t') return false;
const char *c = line_sv.data() + 3;
const char *end = line_sv.data() + line_sv.size();
bool any = false;
while (c < end && *c != ';') {
while (c < end && (*c == ' ' || *c == '\t')) ++c;
if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break;
char axis = *c;
++c;
if (axis == 'X' || axis == 'Y' || axis == 'Z') {
char *next;
double v = std::strtod(c, &next);
if (next != c) {
if (axis == 'X') p.x() = v;
else if (axis == 'Y') p.y() = v;
else p.z() = v;
c = next;
any = true;
continue;
}
}
// Skip unrecognized word.
while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n')
++c;
}
return any;
};
auto parse_m106 = [](const std::string_view &line_sv) -> int {
// Returns -1 if not an M106, otherwise the S value (0..255).
if (line_sv.size() < 4 || line_sv[0] != 'M') return -1;
if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6'))
return -1;
// Find S<value>
size_t s_pos = line_sv.find('S');
if (s_pos == std::string_view::npos) return -1;
const char *c = line_sv.data() + s_pos + 1;
char *next;
long v = std::strtol(c, &next, 10);
if (next == c) return -1;
return int(std::clamp<long>(v, 0, 255));
};
auto parse_m107 = [](const std::string_view &line_sv) -> bool {
return line_sv.size() >= 4 && line_sv[0] == 'M' &&
line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7';
};
auto parse_tool_change = [this](const std::string_view &line_sv) -> int {
// Returns the new extruder id, or -1 if not a toolchange.
if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1;
if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0)
return -1;
const char *c = line_sv.data() + m_toolchange_prefix.size();
char *next;
long v = std::strtol(c, &next, 10);
if (next == c) return -1;
return int(v);
};
const char *p = gcode.c_str();
const char *end = gcode.c_str() + gcode.size();
while (p < end) {
const char *line_end = p;
while (line_end < end && *line_end != '\n') ++line_end;
const char *next_line = line_end;
if (next_line < end) ++next_line; // include the '\n'
std::string_view line_sv(p, line_end - p);
// Track tool changes so the per-line fan eval uses the right extruder.
int new_tool = parse_tool_change(line_sv);
if (new_tool >= 0)
active_extruder = unsigned(new_tool);
// Track existing fan commands so we can restore the right value when
// exiting a band.
int m106_speed = parse_m106(line_sv);
if (m106_speed >= 0) {
current_main_fan = m106_speed;
if (!in_first_layer_band)
pre_band_main_fan = m106_speed;
} else if (parse_m107(line_sv)) {
current_main_fan = 0;
if (!in_first_layer_band)
pre_band_main_fan = 0;
}
// Movement line: parse XYZ, evaluate plane, possibly emit a fan
// change BEFORE this line.
bool moved = parse_xyz_into(line_sv, cur_pos_mm);
if (moved) {
const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm);
const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder);
const bool now_in_band = eff_idx < std::max(close_n, 1);
if (now_in_band != in_first_layer_band) {
// Band crossing: emit a M106 with the appropriate speed.
int target_fan;
if (now_in_band) {
// Entering the first-layer band: fan off.
pre_band_main_fan = current_main_fan;
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
} else {
// Exiting the band: restore the layer's normal fan speed.
// Use compute_main_fan_speed with the effective index so
// the linear ramp factor (close_fan→full_fan_speed_layer)
// also follows distance from the plane.
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
if (target_fan == 0)
target_fan = pre_band_main_fan;
}
if (target_fan != current_main_fan) {
out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm);
current_main_fan = target_fan;
m_fan_speed = target_fan;
m_current_fan_speed = target_fan;
}
in_first_layer_band = now_in_band;
}
}
out.append(p, next_line - p);
p = next_line;
}
return out;
}
} // namespace Slic3r
+20 -1
View File
@@ -10,6 +10,7 @@ namespace Slic3r {
class GCode;
class Layer;
class FirstLayerPlane;
struct PerExtruderAdjustments;
// A standalone G-code filter, to control cooling of the print.
@@ -18,7 +19,7 @@ struct PerExtruderAdjustments;
//
// The simple it sounds, the actual implementation is significantly more complex.
// Namely, for a multi-extruder print, each material may require a different cooling logic.
// For example, some materials may not like to print too slowly, while with some materials
// For example, some materials may not like to print too slowly, while with some materials
// we may slow down significantly.
//
class CoolingBuffer {
@@ -36,6 +37,21 @@ private:
// Returns the adjusted G-code.
std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector<PerExtruderAdjustments> &per_extruder_adjustments);
// First-layer plane: per-line fan re-evaluation post-pass. Walks the
// post-cooldown gcode, tracks XYZ position, and inserts M106 commands at
// band-crossing transitions in slicing-frame coordinates. Only runs
// when m_first_layer_plane is active.
std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in,
size_t layer_id,
float layer_time);
// Pure helper: compute the main fan speed for a given effective layer
// index (layer-id units, mapped through the plane evaluator) and the
// current extruder. Mirrors the inline logic in the change_extruder_set_fan
// lambda but is callable from per-line code.
int compute_main_fan_speed(int effective_layer_id, float layer_time,
unsigned int extruder_id) const;
// G-code snippet cached for the support layers preceding an object layer.
std::string m_gcode;
// Internal data.
@@ -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;
};
}
+45
View File
@@ -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()) {
+16
View File
@@ -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
@@ -0,0 +1,74 @@
#include "MachineFrameTransform.hpp"
#include "../Geometry.hpp"
#include <cmath>
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
@@ -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_
+57 -5
View File
@@ -1,5 +1,6 @@
#include "GCodeWriter.hpp"
#include "CustomGCode.hpp"
#include "Geometry.hpp"
#include "I18N.hpp"
#include "PrintConfig.hpp"
#include <algorithm>
@@ -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);
+38 -25
View File
@@ -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 &center_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<Extruder> 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);
+9 -1
View File
@@ -1326,7 +1326,15 @@ static std::vector<std::string> s_Preset_machine_limits_options {
static std::vector<std::string> 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",
+99 -16
View File
@@ -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<std::string> 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<ObjectID> 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<StringObjectException> *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<StringObjectException> *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<PrintObject*> need_slicing_objects;
std::set<PrintObject*> 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> gcode;
if (m_config.belt_printer.value)
gcode = std::make_unique<BeltGCode>();
else
gcode = std::make_unique<GCode>();
//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.
+29 -1
View File
@@ -17,6 +17,7 @@
#include "GCode/ThumbnailData.hpp"
#include "GCode/GCodeProcessor.hpp"
#include "MultiMaterialSegmentation.hpp"
#include "BeltTransform.hpp"
#include "libslic3r.h"
#include <Eigen/Geometry>
@@ -186,6 +187,13 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor<SupportLaye
ConstSupportLayerPtrsAdaptor(const SupportLayerPtrs *data) : ConstVectorOfPtrsAdaptor<SupportLayer>(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
+35 -3
View File
@@ -1,6 +1,7 @@
#include "ClipperUtils.hpp"
#include "Model.hpp"
#include "Print.hpp"
#include "BeltTransform.hpp"
#include <boost/log/trivial.hpp>
#include <cfloat>
@@ -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<PrintObjectTrafoAndInstances> print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation)
static std::vector<PrintObjectTrafoAndInstances> print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation, bool force_separate_instances = false)
{
std::set<PrintObjectTrafoAndInstances> 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<PrintObjectTrafoAndInstances&>(*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<ModelObjectStatus&>(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<const PrintObjectStatus*> 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<ConfigOptionFloats>("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<PrintObjectStatus*>(*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
+323 -1
View File
@@ -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<BeltRotationAxis>::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>(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<RemapAxis>::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<RemapAxis>(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<FirstLayerPlaneMode>::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>(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<BeltSupportFloorMode>::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>(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<BeltSupportZOffsetMode>::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>(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();
+94
View File
@@ -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<BeltRotationAxis>, 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<RemapAxis>, preslice_remap_x))
((ConfigOptionEnum<RemapAxis>, preslice_remap_y))
((ConfigOptionEnum<RemapAxis>, preslice_remap_z))
((ConfigOptionBool, preslice_remap_global))
((ConfigOptionEnum<RemapAxis>, gcode_remap_x))
((ConfigOptionEnum<RemapAxis>, gcode_remap_y))
((ConfigOptionEnum<RemapAxis>, gcode_remap_z))
((ConfigOptionBool, gcode_back_transform))
((ConfigOptionBool, belt_preslice_global))
((ConfigOptionEnum<FirstLayerPlaneMode>, first_layer_plane))
((ConfigOptionFloat, first_layer_plane_offset))
((ConfigOptionFloat, first_layer_plane_thickness))
((ConfigOptionFloat, belt_support_floor_offset))
((ConfigOptionEnum<BeltSupportFloorMode>, belt_support_floor_mode))
((ConfigOptionEnum<BeltSupportZOffsetMode>, belt_support_z_offset_mode))
//BBS
((ConfigOptionInts, additional_cooling_fan_speed))
((ConfigOptionInts, close_additional_fan_first_x_layers))
+141 -5
View File
@@ -2,6 +2,9 @@
#include "Model.hpp"
#include "Point.hpp"
#include "Print.hpp"
#include "BeltTransform.hpp"
#include <thread>
#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<std::set<int>> 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<ExtrusionPath *>(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<ExtrusionMultiPath *>(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<ExtrusionPath *>(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<ExtrusionLoop *>(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<ExtrusionPath *>(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<ExtrusionEntityCollection *>(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
+261 -5
View File
@@ -1,4 +1,6 @@
#include <boost/log/trivial.hpp>
#include <limits>
#include <thread>
#include <tbb/parallel_for.h>
@@ -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<VolumeSlices> slice_volumes_inner(
ModelVolumePtrs model_volumes,
const std::vector<PrintObjectRegions::LayerRangeRegions> &layer_ranges,
const std::vector<float> &zs,
const std::function<void()> &throw_on_cancel_callback)
const std::function<void()> &throw_on_cancel_callback,
double *out_belt_min_z = nullptr)
{
model_volumes_sort_by_id(model_volumes);
@@ -167,6 +173,10 @@ static std::vector<VolumeSlices> 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<std::vector<ExPolygons>> 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 &region = 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 &region2 = 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<coordf_t> 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<double>(inst_shift.x()) << ", " << unscale<double>(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<double>(m_center_offset.x()),
unscale<double>(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<double>(m_center_offset.x()) << "," << unscale<double>(m_center_offset.y()) << ")"
<< " inst_shift=(" << unscale<double>(inst_shift.x()) << "," << unscale<double>(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<double>(inst_shift.x()), unscale<double>(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<double>(inst_shift.x()), unscale<double>(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<double>(inst_shift.x()), unscale<double>(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<double>(m_center_offset.x())
<< ", " << unscale<double>(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
+7
View File
@@ -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<SlicingParameters>::value, "SlicingParameters class is not POD (and it should be - see constructor).");
+124
View File
@@ -0,0 +1,124 @@
#include "BeltFloorContext.hpp"
#include <cmath>
#include <limits>
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<double>::infinity();
double pos = unscale<double>(m_from_axis == 0 ? pos_slicing.x() : pos_slicing.y());
return m_shear_factor * pos + m_floor_offset + m_z_shift;
}
std::vector<Polygons> BeltFloorContext::compute_per_layer_floors(
size_t num_layers,
const std::function<double(size_t)> &layer_print_z) const
{
std::vector<Polygons> 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
@@ -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<Polygons> compute_per_layer_floors(
size_t num_layers,
const std::function<double(size_t)> &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
+6
View File
@@ -144,6 +144,12 @@ int idx_lower_or_equal(const std::vector<T*> &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_ */
+198 -9
View File
@@ -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<SupporLayerType> support_types_interface {
static constexpr const std::initializer_list<SupporLayerType> 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<Polygons> &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<int>(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<Polygons, Polygons> 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<size_t>(0, support_layers.size()),
[&](const tbb::blocked_range<size_t> &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(
+76 -5
View File
@@ -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<coordf_t> 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<Polygons>{});
@@ -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<LayerIndex>(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<LayerIndex>& 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())
@@ -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<Polygons> m_belt_floor;
private:
// Caching polygons for a range of layers.
+230 -11
View File
@@ -14,6 +14,7 @@
#include "TreeSupportCommon.hpp"
#include "TreeSupport.hpp"
#include "TreeSupport3D.hpp"
#include "BeltFloorContext.hpp"
#include <libnest2d/backends/libslic3r/geometries.hpp>
#include <libnest2d/placers/nfpplacer.hpp>
@@ -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<double>::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<double>(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<second_>(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
+4
View File
@@ -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;
/*!
+78 -3
View File
@@ -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<std::pair<TreeSupportSettings, std::vector<size_t>>> 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<double>(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<std::pair<TreeSupportSettings, std::vector<size_t>>> 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<LayerIndex>(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<LayerIndex> &range) {
for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) {
const Layer &current_layer = *print_object.get_layer(layer_id);
@@ -254,7 +259,15 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> 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<float>(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<coordf_t> 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)
+7 -7
View File
@@ -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> &&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> &&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<Cursor> &&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<Matrix3f>(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>());
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<Matrix3f>(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>());
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
});
+3 -1
View File
@@ -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
+53 -29
View File
@@ -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<ConfigOptionFloats>("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<int>(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<int>(1, speed_perimeter() - 1)), "Accel/flow trick line");
gcode << m_writer.set_print_acceleration(accel);
gcode << m_writer->set_print_acceleration(std::max<int>(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<int>(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<BeltGCodeWriter>();
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<BeltGCodeWriter*>(m_writer.get()) != nullptr) {
m_writer = std::make_shared<GCodeWriter>();
}
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
+4 -1
View File
@@ -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<GCodeWriter> m_writer{std::make_shared<GCodeWriter>()};
Vec3d m_starting_point;
bool m_is_start_point_fixed = false;
+190 -2
View File
@@ -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<double>(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<double>(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<double>(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<double>(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));
+22 -1
View File
@@ -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<std::vector<Vec2d>> m_extruder_shapes;
std::vector<double> 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);
};
+18 -2
View File
@@ -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<Matrix3f>(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>()));
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();
+21 -4
View File
@@ -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<ConfigOptionBool>("belt_printer");
if (belt_opt)
is_belt_printer = belt_opt->value;
}
bool have_volumetric_extrusion_rate_slope = config->option<ConfigOptionFloat>("max_volumetric_extrusion_rate_slope")->value > 0;
float have_volumetric_extrusion_rate_slope_segment_length = config->option<ConfigOptionFloat>("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<DraftShield>("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<BrimType>("brim_type") != btNoBrim);
bool have_brim = (config->opt_enum<BrimType>("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<ConfigOptionPercent>("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<SupportType>("support_type");
+187 -5
View File
@@ -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<int> 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<std::string>& str_tool_colors,
const std::vector<std::string>& 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<double>();
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<Matrix4f>(camera.get_view_matrix().matrix().cast<float>()));
Matrix4f view = camera.get_view_matrix().matrix().cast<float>();
// 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<Matrix4f>(camera.get_projection_matrix().matrix().cast<float>()));
#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);
+10
View File
@@ -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();
+29 -2
View File
@@ -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<Preview*>(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(),
+3 -1
View File
@@ -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<std::string, std::vector<SimpleSettingData>> 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}
}}
+1 -1
View File
@@ -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());
+11
View File
@@ -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)
{
+2
View File
@@ -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();
+16 -4
View File
@@ -526,6 +526,14 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle()
return auto_support ? support_threshold_angle : 0;
}
std::pair<double, double> 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<float>().normalized();
Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast<float>().normalized();
float dot_limit = limit.dot(down);
Vec3f down = (trafo_matrix.inverse() * gravity_dir).cast<float>().normalized();
float dot_limit = std::cos(threshold);
// Now calculate dot product of vert_direction and facets' normals.
int idx = 0;
@@ -60,6 +60,7 @@ private:
void select_facets_by_angle(float threshold, bool block);
// BBS
int get_selection_support_threshold_angle();
std::pair<double, double> get_build_plate_tilt();
int m_support_threshold_angle = -1;
+22 -6
View File
@@ -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<Matrix3f>(trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>());
// 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<TriangleSelector::Cursor> 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<TriangleSelector::Cursor> 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);
@@ -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 };

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