mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-16 07:22:10 +00:00
Merge pull request #58 from HarrierPigeon/belt/tempTower-TommyB
Belt/temp tower tommy b
This commit is contained in:
BIN
resources/calib/temperature_tower/belt_temp_provino_unit.stl
Normal file
BIN
resources/calib/temperature_tower/belt_temp_provino_unit.stl
Normal file
Binary file not shown.
BIN
resources/calib/temperature_tower/belt_temp_tower_230_190.stl
Normal file
BIN
resources/calib/temperature_tower/belt_temp_tower_230_190.stl
Normal file
Binary file not shown.
BIN
resources/calib/temperature_tower/belt_temp_tower_240_210.stl
Normal file
BIN
resources/calib/temperature_tower/belt_temp_tower_240_210.stl
Normal file
Binary file not shown.
BIN
resources/calib/temperature_tower/belt_temp_tower_250_230.stl
Normal file
BIN
resources/calib/temperature_tower/belt_temp_tower_250_230.stl
Normal file
Binary file not shown.
BIN
resources/calib/temperature_tower/belt_temp_tower_270_230.stl
Normal file
BIN
resources/calib/temperature_tower/belt_temp_tower_270_230.stl
Normal file
Binary file not shown.
BIN
resources/calib/temperature_tower/belt_temp_tower_280_240.stl
Normal file
BIN
resources/calib/temperature_tower/belt_temp_tower_280_240.stl
Normal file
Binary file not shown.
BIN
resources/calib/temperature_tower/belt_temp_tower_320_280.stl
Normal file
BIN
resources/calib/temperature_tower/belt_temp_tower_320_280.stl
Normal file
Binary file not shown.
79
resources/calib/temperature_tower/gen_belt_temp_tower.py
Normal file
79
resources/calib/temperature_tower/gen_belt_temp_tower.py
Normal file
@@ -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)}")
|
||||
@@ -13297,14 +13297,96 @@ void Plater::calib_temp(const Calib_Params& params) {
|
||||
if (params.mode != CalibMode::Calib_Temp_Tower)
|
||||
return;
|
||||
|
||||
// ORCA-Belt: a counter-rotated tower would cantilever every block off a
|
||||
// support wedge — print the temperature blocks as separate objects in
|
||||
// native belt orientation instead.
|
||||
// Belt printers build along the conveyor, not vertically — a tall tower cannot
|
||||
// be sliced. Instead lay a row of DISCRETE provini (one per temperature) along
|
||||
// the belt and change temperature in discrete steps via custom per-layer G-code
|
||||
// (M104), injected in the empty gap just before each provino so the nozzle is
|
||||
// settled by the time that provino prints. We deliberately do NOT call
|
||||
// set_calib_params here: its per-layer interpolation (interpolate_value_across_
|
||||
// layers) would overwrite these discrete M104 events.
|
||||
{
|
||||
double belt_angle_rad = 0.;
|
||||
Vec3d belt_axis = Vec3d::UnitX();
|
||||
if (belt_calib_rotation_params(belt_angle_rad, belt_axis)) {
|
||||
_calib_temp_belt_sectioned(params, std::abs(belt_angle_rad));
|
||||
auto belt_printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
if (belt_printer_config->has("belt_printer") && belt_printer_config->opt_bool("belt_printer")) {
|
||||
// Belt temperature-tower model selector (Calib_Params::test_model):
|
||||
// 0 = "Standard" -> Joe's counter-rotated sectioned tower.
|
||||
// 1 = "Overhang" -> engraved inverted-L provini that stress overhang
|
||||
// print-quality at each discrete temperature (below).
|
||||
if (params.test_model < 1) {
|
||||
double belt_angle_rad = 0.; Vec3d belt_axis = Vec3d::UnitX();
|
||||
belt_calib_rotation_params(belt_angle_rad, belt_axis);
|
||||
_calib_temp_belt_sectioned(params, std::abs(belt_angle_rad));
|
||||
return;
|
||||
}
|
||||
constexpr int TEMP_STEP = 5; // matches Temp_Calibration_Dlg
|
||||
// Shared geometry contract with the offline asset generator
|
||||
// (resources/calib/temperature_tower/gen_belt_temp_tower.py): provini are
|
||||
// replicated along the belt (designed Y) at this pitch. The slicing plane
|
||||
// is oblique (belt_slice_rotation_angle), so the per-zone advance in the
|
||||
// layer print_z space the custom-gcode matcher uses is PITCH*cos(theta).
|
||||
constexpr double PITCH_Y = 74.718; // designed-Y pitch == gen PITCH
|
||||
const double angle = belt_printer_config->has("belt_slice_rotation_angle")
|
||||
? belt_printer_config->opt_float("belt_slice_rotation_angle") : 45.0;
|
||||
const double zone_topz = PITCH_Y * std::cos(angle * M_PI / 180.0);
|
||||
// The custom-gcode matcher attaches each event to a real sliced layer. The
|
||||
// empty inter-provino gap has NO layers, so an event placed there is silently
|
||||
// dropped. Fire it 70 layers ABOVE provino i's start instead — inside the
|
||||
// provino body, past the gap and the overlap with provino i-1's tail, so the
|
||||
// temperature change attaches and applies cleanly. (layer print_z steps by the
|
||||
// process layer height.)
|
||||
auto belt_print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
const double layer_h = belt_print_config->has("layer_height")
|
||||
? belt_print_config->opt_float("layer_height") : 0.2;
|
||||
const double into_provino = 70.0 * layer_h;
|
||||
|
||||
const int t_start = (int) lround(params.start);
|
||||
const int t_end = (int) lround(params.end);
|
||||
std::vector<int> temps;
|
||||
const int tstep = (t_start >= t_end) ? -TEMP_STEP : TEMP_STEP;
|
||||
for (int t = t_start; (tstep < 0) ? (t >= t_end) : (t <= t_end); t += tstep)
|
||||
temps.push_back(t);
|
||||
if (temps.empty()) temps.push_back(t_start);
|
||||
|
||||
const std::string calib_dir = Slic3r::resources_dir() + "/calib/temperature_tower/";
|
||||
std::string asset = calib_dir + "belt_temp_tower_" + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl";
|
||||
if (!boost::filesystem::exists(asset)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "[belt_temp] no embossed provini for " << t_start << "->" << t_end
|
||||
<< ", falling back to 230_190 (embossed numbers will not match)";
|
||||
asset = calib_dir + "belt_temp_tower_230_190.stl";
|
||||
}
|
||||
add_model(false, asset);
|
||||
|
||||
// Place keel-first asset at the belt entry (designed Y = 0) so Z_gcode
|
||||
// starts at 0, centered laterally on the bed, resting on the conveyor.
|
||||
ModelObject* obj = model().objects[0];
|
||||
obj->ensure_on_bed();
|
||||
BoundingBoxf3 obb = obj->bounding_box_exact();
|
||||
auto bed_shape = belt_printer_config->option<ConfigOptionPoints>("printable_area")->values;
|
||||
BoundingBoxf bed_ext = get_extents(bed_shape);
|
||||
obj->translate_instances(Vec3d(bed_ext.center().x() - obb.center().x(), -obb.min.y(), 0.0));
|
||||
|
||||
auto belt_filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
|
||||
belt_filament_config->set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(1, temps.front()));
|
||||
belt_filament_config->set_key_value("nozzle_temperature", new ConfigOptionInts(1, temps.front()));
|
||||
obj->config.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
|
||||
obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
|
||||
obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false));
|
||||
belt_printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
||||
|
||||
const int plate_idx = get_partplate_list().get_curr_plate_index();
|
||||
model().curr_plate_index = plate_idx;
|
||||
CustomGCode::Info &cg_info = model().plates_custom_gcodes[plate_idx];
|
||||
cg_info.mode = CustomGCode::Mode::SingleExtruder;
|
||||
cg_info.gcodes.clear();
|
||||
for (size_t i = 1; i < temps.size(); ++i) {
|
||||
const double pz = double(i) * zone_topz + into_provino; // 70 layers into provino i
|
||||
cg_info.gcodes.push_back(CustomGCode::Item{
|
||||
pz, CustomGCode::Custom, 1, "",
|
||||
"M104 S" + std::to_string(temps[i]) + " ; belt temp zone " + std::to_string(temps[i]) });
|
||||
}
|
||||
|
||||
changed_objects({ 0 });
|
||||
wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty();
|
||||
wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,6 +395,17 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat
|
||||
method_box->Add(m_rbFilamentType, 0, wxALL | wxEXPAND, FromDIP(4));
|
||||
v_sizer->Add(method_box, 0, wxTOP | wxRIGHT | wxLEFT | wxEXPAND, FromDIP(10));
|
||||
|
||||
// Belt temperature-tower model: Standard (sectioned tower) vs Overhang (engraved
|
||||
// inverted-L provini that stress overhang quality per temperature). Only affects
|
||||
// belt printers; the upright tower ignores it, so the picker is only shown on
|
||||
// belts. The dialog is cached across printer switches, so visibility is toggled
|
||||
// per-show in on_show() rather than gated here at construction time.
|
||||
auto labeled_box_model = new LabeledStaticBox(this, _L("Test model"));
|
||||
m_model_box = new wxStaticBoxSizer(labeled_box_model, wxHORIZONTAL);
|
||||
m_rbModel = new RadioGroup(this, { _L("Standard"), _L("Overhang") }, wxVERTICAL);
|
||||
m_model_box->Add(m_rbModel, 0, wxALL | wxEXPAND, FromDIP(4));
|
||||
v_sizer->Add(m_model_box, 0, wxTOP | wxRIGHT | wxLEFT | wxEXPAND, FromDIP(10));
|
||||
|
||||
// Settings
|
||||
wxString start_temp_str = _L("Start temp: ");
|
||||
wxString end_temp_str = _L("End temp: ");
|
||||
@@ -455,6 +466,11 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat
|
||||
|
||||
m_rbFilamentType->Connect(wxEVT_COMMAND_RADIOBOX_SELECTED, wxCommandEventHandler(Temp_Calibration_Dlg::on_filament_type_changed), NULL, this);
|
||||
|
||||
// Refresh the belt-only model picker on every show — the dialog is cached and
|
||||
// reused across printer switches.
|
||||
this->Connect(wxEVT_SHOW, wxShowEventHandler(Temp_Calibration_Dlg::on_show));
|
||||
m_model_box->ShowItems(is_belt_printer_selected());
|
||||
|
||||
wxGetApp().UpdateDlgDarkUI(this);
|
||||
|
||||
Layout();
|
||||
@@ -511,11 +527,27 @@ void Temp_Calibration_Dlg::on_start(wxCommandEvent& event) {
|
||||
m_params.start = start;
|
||||
m_params.end = end;
|
||||
m_params.mode = CalibMode::Calib_Temp_Tower;
|
||||
// Picker only exists on belt printers; default non-belt to the Standard model.
|
||||
m_params.test_model = m_rbModel ? m_rbModel->GetSelection() : 0;
|
||||
m_plater->calib_temp(m_params);
|
||||
EndModal(wxID_OK);
|
||||
|
||||
}
|
||||
|
||||
void Temp_Calibration_Dlg::on_show(wxShowEvent& event) {
|
||||
// ORCA-Belt: the dialog is cached across printer switches, so refresh the
|
||||
// belt-only "Test model" picker on every show. The Overhang model only
|
||||
// applies to belt printers; hide it (and resize the dialog) otherwise.
|
||||
const bool belt = is_belt_printer_selected();
|
||||
if (m_model_box->AreAnyItemsShown() != belt) {
|
||||
m_model_box->ShowItems(belt);
|
||||
Layout();
|
||||
Fit();
|
||||
GetSizer()->SetSizeHints(this);
|
||||
}
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void Temp_Calibration_Dlg::on_filament_type_changed(wxCommandEvent& event) {
|
||||
int selection = event.GetSelection();
|
||||
unsigned long start = 0, end = 0;
|
||||
|
||||
@@ -59,9 +59,12 @@ protected:
|
||||
|
||||
virtual void on_start(wxCommandEvent& event);
|
||||
virtual void on_filament_type_changed(wxCommandEvent& event);
|
||||
void on_show(wxShowEvent& event);
|
||||
Calib_Params m_params;
|
||||
|
||||
RadioGroup* m_rbFilamentType;
|
||||
RadioGroup* m_rbModel = nullptr;
|
||||
wxStaticBoxSizer* m_model_box = nullptr;
|
||||
TextInput* m_tiStart;
|
||||
TextInput* m_tiEnd;
|
||||
TextInput* m_tiStep;
|
||||
|
||||
Reference in New Issue
Block a user