mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-31 14:52:06 +00:00
Belt printers can't slice a tall vertical temperature tower. This adds a belt-specific temperature-tower model — a row of discrete, individually engraved provini laid along the belt, each printed at one temperature via custom per-layer M104. Each provino is an inverted-L overhang that stresses print quality, so the operator reads the best temperature off overhang quality rather than a continuous ramp. It is offered as a "Test model" choice in the temperature calibration dialog (mirroring the Cornering test's selector), so users keep Joe's counter-rotated sectioned tower as "Standard" and can pick this one as "Overhang": - Calib_Params::test_model (existing field) carries the choice. - Temp_Calibration_Dlg gets a Standard/Overhang radio. - Plater::calib_temp belt branch: test_model 0 -> _calib_temp_belt_sectioned (unchanged Standard path), 1 -> the discrete-provini Overhang path. Assets: belt_temp_provino_unit.stl + belt_temp_tower_<start>_<end>.stl (6 ranges) + gen_belt_temp_tower.py (manifold engraving). Based on belt/generic-calibrations. The Overhang path is HW-validated on the IdeaFormer IR3 V2 (discrete M104 + engraved numbers); not re-validated since the rebase.
80 lines
4.1 KiB
Python
80 lines
4.1 KiB
Python
#!/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)}")
|