mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 05:31:04 +00:00
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:
107 files changed
+6704
-404
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)}")
|
||||
@@ -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: 29 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -26,6 +26,7 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
|
||||
uniform vec4 uniform_color;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,6 +24,7 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
uniform SlopeDetection slope;
|
||||
void main()
|
||||
|
||||
@@ -26,6 +26,7 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
|
||||
uniform vec4 uniform_color;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,6 +24,7 @@ struct SlopeDetection
|
||||
bool actived;
|
||||
float normal_z;
|
||||
mat3 volume_world_normal_matrix;
|
||||
vec3 up_direction;
|
||||
};
|
||||
uniform SlopeDetection slope;
|
||||
void main()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+53
-9
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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_
|
||||
@@ -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);
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "GCode/CoolingBuffer.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class GCodeWriter {
|
||||
public:
|
||||
virtual ~GCodeWriter() = default;
|
||||
GCodeConfig config;
|
||||
bool multiple_extruders;
|
||||
|
||||
@@ -73,21 +73,21 @@ public:
|
||||
std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string());
|
||||
// SoftFever NOTE: the returned speed is mm/minute
|
||||
double get_current_speed() const { return m_current_speed;}
|
||||
std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string());
|
||||
std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false);
|
||||
virtual std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string());
|
||||
virtual std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false);
|
||||
std::string travel_to_z(double z, const std::string &comment = std::string(), bool force = false);
|
||||
bool will_move_z(double z) const;
|
||||
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
//BBS: generate G2 or G3 extrude which moves by arc
|
||||
std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
std::string retract(bool before_wipe = false, double retract_length = 0);
|
||||
std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0);
|
||||
std::string unretract();
|
||||
// do lift instantly
|
||||
std::string eager_lift(const LiftType type);
|
||||
virtual std::string eager_lift(const LiftType type);
|
||||
// record a lift request, do realy lift in next travel
|
||||
std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false);
|
||||
virtual std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false);
|
||||
std::string unlift();
|
||||
const Vec3d& get_position() const { return m_pos; }
|
||||
Vec3d& get_position() { return m_pos; }
|
||||
@@ -127,9 +127,38 @@ public:
|
||||
void set_is_first_layer(bool bval) { m_is_first_layer = bval; }
|
||||
GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; }
|
||||
|
||||
// Axis remap: permute/negate/reverse axes in G-code output.
|
||||
// Works standalone (without belt mode) for printers with non-standard axis conventions.
|
||||
void set_axis_remap(int rx, int ry, int rz);
|
||||
void set_build_volume_max(const Vec3d &max);
|
||||
bool has_axis_remap() const;
|
||||
|
||||
// Returns whether this flavor supports separate print and travel acceleration.
|
||||
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
|
||||
private:
|
||||
protected:
|
||||
// Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter)
|
||||
Vec3d m_pos = Vec3d::Zero();
|
||||
double m_x_offset{ 0 };
|
||||
double m_y_offset{ 0 };
|
||||
double m_lifted;
|
||||
double m_to_lift;
|
||||
LiftType m_to_lift_type;
|
||||
bool m_is_first_layer = true;
|
||||
bool m_is_current_pos_clear = false;
|
||||
double m_current_speed;
|
||||
|
||||
virtual std::string _travel_to_z(double z, const std::string &comment);
|
||||
|
||||
// Axis remap state — accessible to subclasses.
|
||||
int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc.
|
||||
int m_remap_y = 1;
|
||||
int m_remap_z = 2;
|
||||
Vec3d m_build_vol_max = Vec3d::Zero();
|
||||
|
||||
// Apply axis remap to a point. Returns pos unchanged if remap is identity.
|
||||
Vec3d apply_axis_remap(const Vec3d &pos) const;
|
||||
|
||||
private:
|
||||
// Extruders are sorted by their ID, so that binary search is possible.
|
||||
std::vector<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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ®ion = layer_range.volume_regions[idx_region];
|
||||
if (region.bbox->min().z() <= z && region.bbox->max().z() >= z) {
|
||||
if (!bbox_z_in_layer_frame || (region.bbox->min().z() <= z && region.bbox->max().z() >= z)) {
|
||||
if (idx_first_printable_region == -1 && region.model_volume->is_model_part())
|
||||
idx_first_printable_region = idx_region;
|
||||
else if (idx_first_printable_region != -1) {
|
||||
// Test for overlap with some other region.
|
||||
for (int idx_region2 = idx_first_printable_region; idx_region2 < idx_region; ++ idx_region2) {
|
||||
const PrintObjectRegions::VolumeRegion ®ion2 = layer_range.volume_regions[idx_region2];
|
||||
if (region2.bbox->min().z() <= z && region2.bbox->max().z() >= z && overlap_in_xy(*region.bbox, *region2.bbox)) {
|
||||
const bool region2_in_z = !bbox_z_in_layer_frame
|
||||
|| (region2.bbox->min().z() <= z && region2.bbox->max().z() >= z);
|
||||
if (region2_in_z && overlap_in_xy(*region.bbox, *region2.bbox)) {
|
||||
complex = true;
|
||||
break;
|
||||
}
|
||||
@@ -816,8 +853,12 @@ void groupingVolumesForBrim(PrintObject* object, LayerPtrs& layers, int firstLay
|
||||
// Resulting expolygons of layer regions are marked as Internal.
|
||||
void PrintObject::slice()
|
||||
{
|
||||
if (! this->set_started(posSlice))
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this;
|
||||
if (! this->set_started(posSlice)) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)";
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this;
|
||||
//BBS: add flag to reload scene for shell rendering
|
||||
m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE);
|
||||
std::vector<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
|
||||
|
||||
@@ -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).");
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
/*!
|
||||
|
||||
@@ -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 ¤t_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)
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}
|
||||
}}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user