busbars: FDM test-print export (extruded STL + STEP)
Adds a way to print a busbar on a 3D printer to physically verify
sizing before laser-cutting nickel/copper. The existing "Extrude
solid + 0.2mm" path stays for thin-strip STEP — slicers can't print
0.2mm sheet — so the FDM path has its own thickness input (default
2mm, min 0.5mm).
Backend:
- busbar_export.py: new to_stl() writer. Forces extrude_flag = True
(STL is inherently 3D) and bumps thickness up to 2mm if the
incoming value is <0.5mm. Registers under WRITERS["stl"] so the
existing /api/export/<fmt> route serves it for free.
UI:
- index.html: new "FDM test print" block under the Params panel
with its own thickness input and two buttons (STL, STEP). The
existing 'Extrude solid' checkbox + 0.2mm thickness keep
driving plain "Export STEP".
- styles.css: .fdm-block / .fdm-row / .fdm-buttons styles
matching the existing panel typography.
- app.js: _exportFdm(fmt) reuses Exporter.exportFormat with a
shallow-merged params override ({extrude: true, thickness: fdmT}),
so the on-the-fly request gets the FDM settings without
mutating the live params state.
Verified: STL render of a 3-cell strip @ 2mm => 73KB binary STL
(opens cleanly in slicers); STEP @ 2mm => 160KB ISO-10303-21
solid; existing flat STEP path unchanged at 15KB.
This commit is contained in:
@@ -46,6 +46,7 @@ from build123d import (
|
|||||||
Sketch,
|
Sketch,
|
||||||
add,
|
add,
|
||||||
export_step,
|
export_step,
|
||||||
|
export_stl,
|
||||||
extrude,
|
extrude,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -299,8 +300,29 @@ def to_svg(payload: dict) -> bytes:
|
|||||||
return path.read_bytes()
|
return path.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def to_stl(payload: dict) -> bytes:
|
||||||
|
"""STL with the busbars extruded to a printable plate.
|
||||||
|
|
||||||
|
STL is inherently 3D, so extrusion is forced regardless of the payload
|
||||||
|
flag. Thickness defaults to 2 mm when missing or <0.5 mm (slicers can't
|
||||||
|
do 0.2 mm sheet — that figure is for the 'thin nickel strip' STEP case).
|
||||||
|
"""
|
||||||
|
busbars, extrude_flag, thickness = parse_payload(payload)
|
||||||
|
if thickness < 0.5:
|
||||||
|
thickness = 2.0
|
||||||
|
shapes = build_shapes(busbars, extrude_flag=True, thickness=thickness)
|
||||||
|
compound = _as_compound(shapes)
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp) / "busbars.stl"
|
||||||
|
# build123d's export_stl takes a single shape; the Compound carries
|
||||||
|
# all busbars together so the slicer sees them as one job.
|
||||||
|
export_stl(compound, str(path))
|
||||||
|
return path.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
WRITERS = {
|
WRITERS = {
|
||||||
"step": (to_step, "application/step", "step"),
|
"step": (to_step, "application/step", "step"),
|
||||||
"dxf": (to_dxf, "image/vnd.dxf", "dxf"),
|
"dxf": (to_dxf, "image/vnd.dxf", "dxf"),
|
||||||
"svg": (to_svg, "image/svg+xml", "svg"),
|
"svg": (to_svg, "image/svg+xml", "svg"),
|
||||||
|
"stl": (to_stl, "model/stl", "stl"),
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -115,7 +115,23 @@
|
|||||||
<label>Slit width (mm) <input type="number" id="p-slit-width" value="1.0" step="0.1" title="Width of each cross arm"></label>
|
<label>Slit width (mm) <input type="number" id="p-slit-width" value="1.0" step="0.1" title="Width of each cross arm"></label>
|
||||||
<label>Neighbor factor <input type="number" id="p-neighbor-factor" value="1.15" step="0.05" min="1.0" title="Bridge two cells if distance ≤ factor × shortest pair distance"></label>
|
<label>Neighbor factor <input type="number" id="p-neighbor-factor" value="1.15" step="0.05" min="1.0" title="Bridge two cells if distance ≤ factor × shortest pair distance"></label>
|
||||||
<label class="checkbox"><input type="checkbox" id="p-extrude"> Extrude solid</label>
|
<label class="checkbox"><input type="checkbox" id="p-extrude"> Extrude solid</label>
|
||||||
<label>Thickness (mm) <input type="number" id="p-thickness" value="0.2" step="0.05"></label>
|
<label>Thickness (mm) <input type="number" id="p-thickness" value="0.2" step="0.05"
|
||||||
|
title="Used only with 'Extrude solid' above (STEP export of a thin nickel/copper plate). FDM print uses its own thickness below."></label>
|
||||||
|
</div>
|
||||||
|
<div class="fdm-block">
|
||||||
|
<h3 class="fdm-title" title="3D-printable plate for verifying dimensions on a real printer. Forces extrusion regardless of the 'Extrude solid' checkbox.">
|
||||||
|
FDM test print
|
||||||
|
</h3>
|
||||||
|
<div class="fdm-row">
|
||||||
|
<label>Thickness (mm)
|
||||||
|
<input type="number" id="p-fdm-thickness" value="2.0" min="0.5" step="0.1"
|
||||||
|
title="Layer-printable wall thickness. 2 mm is a safe starting value on a 0.4 mm nozzle.">
|
||||||
|
</label>
|
||||||
|
<div class="fdm-buttons">
|
||||||
|
<button id="btn-export-fdm-stl" type="button" class="primary" title="Extruded STL ready for the slicer">STL</button>
|
||||||
|
<button id="btn-export-fdm-step" type="button" title="Extruded STEP for CAD import">STEP</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,16 @@
|
|||||||
$("btn-export-dxf" ).addEventListener("click", () => Exporter.exportFormat("dxf", state, params));
|
$("btn-export-dxf" ).addEventListener("click", () => Exporter.exportFormat("dxf", state, params));
|
||||||
$("btn-export-svg" ).addEventListener("click", () => Exporter.exportFormat("svg", state, params));
|
$("btn-export-svg" ).addEventListener("click", () => Exporter.exportFormat("svg", state, params));
|
||||||
|
|
||||||
|
// ---- FDM test print -----------------------------------------------------
|
||||||
|
// Forces extrusion at the FDM thickness, regardless of the 'Extrude solid'
|
||||||
|
// checkbox above (which keeps owning the thin nickel-strip case).
|
||||||
|
function _exportFdm(fmt) {
|
||||||
|
const t = Math.max(0.5, +$("p-fdm-thickness").value || 2);
|
||||||
|
Exporter.exportFormat(fmt, state, { ...params, extrude: true, thickness: t });
|
||||||
|
}
|
||||||
|
$("btn-export-fdm-stl") .addEventListener("click", () => _exportFdm("stl"));
|
||||||
|
$("btn-export-fdm-step").addEventListener("click", () => _exportFdm("step"));
|
||||||
|
|
||||||
// ---- viewport init ------------------------------------------------------
|
// ---- viewport init ------------------------------------------------------
|
||||||
Viewport.init($("viewport"), state, params, {
|
Viewport.init($("viewport"), state, params, {
|
||||||
onCellClick: (cellId, mods) => {
|
onCellClick: (cellId, mods) => {
|
||||||
|
|||||||
@@ -420,6 +420,36 @@ label.checkbox {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FDM test-print block inside the Params panel */
|
||||||
|
.fdm-block {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.fdm-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
.fdm-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.fdm-row label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.fdm-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.fdm-buttons button {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
input[type=number], input[type=text], select, textarea {
|
input[type=number], input[type=text], select, textarea {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|||||||
Reference in New Issue
Block a user