Files
support-fins/web/ui/export.js
Matthew Trahanandmatthewtrahan a59fb2ba6d Interface material: wall crests + tines as their own 3MF object for a toolchanger (#21) (#205)
* engine: interface crest (GitHub #21) -- with PROP.iface on, every wall (prop, part-attached, squat, Draw/fill, wedge blade) splits 0.6 mm under its top into body + crest, the crest and the tine comb tagged (v[3] = 1) so triRanges/removal/Worker are untouched; the 3MF writer's separate form writes the tagged triangles as a third object, '<name> interface', in register; off (the default) is byte-identical

* site: Interface material switch (Walls) -- the crest and tines show purple and export as their own 3MF object, for a toolchanger's second filament (PETG under PLA); reaches the Worker via tunables and Draw on the page; re-import still pre-selects the part; German strings; plugins leave it out (one body to the host)

* engine: interface crest review fixes -- sway brace tines are interface too (Auto and Draw); the crest is one trapezoid on the taper line, so no section has a collinear corner (zero-area cap triangles); Interface material is in the undo history; FIN-SPEC lists what is not split yet (sway rib edge, a part-standing wall's foot, a wedge whose contact steps straight up) (review)

* site: the interface is a part of the supports object, not its own -- an object starting in mid-air is refused by Orca ('empty first layer'); as the supports' second component it stands on the plate with them and takes its own filament per part

* engine: interface crest modes -- 'flat' (the default when on) crests a wall station only under an underside within 10deg of level, a full-width (th) 0.6 mm block with no tip taper, tines left in the body; 'all' crests every wall top and the tines as before. A 35deg cube's print with the crest everywhere couldn't lay PETG on the narrow PLA tip through every layer; PETG interfaces are used on flat undersides. Off stays byte-identical (sweep vs main)

* site: Interface material is a dropdown (Off / Flat contacts / Everywhere); the readout says when it is on and no wall got a crest; German strings

* engine: interface modes review fixes -- cutouts under a flat crest keep their old ceiling (holeTop: the taper start, not the crest's cut), so the split loses no plastic (lbracket Y8 lattice lost 4.6%); the crest's part reference is set only while the crest is on and dropped after each build; the readout's 'no flat contacts' note counts walls only (not the pad) and only in Flat contacts; tests: flat mode with lattice/diamond cutouts, mixed flat/tilted walls, squat walls (review)

* engine: the interface crest is one layer of the print (PROP.ifaceLayers 1, the Layer height field, as the tines), not a fixed 0.6 mm -- one tool change each way per flat contact instead of three; tooltip and FIN-SPEC say so; tests: the crest is one 0.2 / 0.3 mm layer

---------

Co-authored-by: matthewtrahan <mathyout@pm.me>
2026-10-09 22:00:37 -05:00

104 lines
4.4 KiB
JavaScript

/**
* Export: the oriented part plus the fins/pad, as STL or 3MF.
*/
import { writeBinarySTL, download } from '../stl.js';
import { writeThreeMF } from '../threemf.js';
import { splitInterface } from '../prop.js';
import { el } from './dom.js';
import { part, topology, lastResult, rotM3, partName } from './part.js';
import { activeAdded } from './finbuild.js';
import { sessionOf } from './session.js';
/**
* Export the part AS ORIENTED, seated on the plate, with the fins as extra
* solids in the same file. The whole promise of the tool is that the STL prints
* the same way for whoever opens it, so the orientation has to be baked in --
* exporting the original frame and hoping the user re-rotates defeats the point.
*/
/**
* The part geometry AS ORIENTED and seated on the plate, plus the fins, kept in
* two separate lists. STL flattens them into one solid; 3MF keeps them distinct.
* Returns null when there is nothing to export.
*/
export function buildExportGeometry() {
if (!part || !topology || !lastResult) return null;
const rot = rotM3.elements;
const dz = lastResult.offset.z;
const dx = lastResult.offset.x, dy = lastResult.offset.y;
const { pos, nFaces } = topology;
const partTris = new Array(nFaces * 3);
for (let f = 0; f < nFaces; f++) {
for (let i = 0; i < 3; i++) {
const o = f * 9 + i * 3;
const x = pos[o], y = pos[o + 1], z = pos[o + 2];
partTris[f * 3 + i] = [
rot[0] * x + rot[3] * y + rot[6] * z + dx,
rot[1] * x + rot[4] * y + rot[7] * z + dy,
rot[2] * x + rot[5] * y + rot[8] * z + dz,
];
}
}
// whichever walls the live mode contributes -- hand-drawn in Draw, suggested
// in Suggest -- plus the pad, all already in print space
const finTris = [...activeAdded()];
const base = partName.replace(/\.(stl|3mf|step|stp)$/i, '') || 'part';
return { partTris, finTris, base };
}
// The three formats. All share the print-space geometry, so a fins-only STL
// lines up with the part when both are imported into one plate.
const FORMATS = {
// STL flattens part + fins into one solid.
'export-stl': (g) => [writeBinarySTL([...g.partTris, ...g.finTris], g.base), `${g.base}-fins.stl`],
// 3MF states millimeters, so the file opens correctly oriented and support-free
// in Bambu Studio, OrcaSlicer, or PrusaSlicer without a re-scale or a re-rotate.
// It writes the supports as their OWN object, so the slicer slices them apart and
// a tine only touches the part (GitHub #38, the grip coupon's split print). The
// site offers only this form: the old one-object assembly kept the pair from being
// arranged apart, too small a gain for tines that fuse. It also carries the
// session (ui/session.js), so opening it here again brings the drawn walls back.
// With Interface material on, the walls' tops and the tines go in a second part of
// the supports object, to set to the second filament (prop/crest.js); off, there
// are none to split.
'export-3mf': (g) => {
const { body, iface } = splitInterface(g.finTris);
return [writeThreeMF(g.partTris, body, g.base, { separate: true, iface, session: sessionOf(g.partTris) }),
`${g.base}-fins.3mf`];
},
// Just the fins + pad (issue #5). One body, so 3MF would add nothing over STL.
'export-fins': (g) => [writeBinarySTL(g.finTris, `${g.base} fins`), `${g.base}-fins-only.stl`],
};
const btn = el('export');
const menu = el('export-menu');
function setOpen(open) {
menu.hidden = !open;
btn.setAttribute('aria-expanded', String(open));
if (open) {
// nothing to write until a part is loaded; no fins-only file without fins
const ready = !!(part && topology && lastResult);
el('export-stl').disabled = el('export-3mf').disabled = !ready;
el('export-fins').disabled = !ready || activeAdded().length === 0;
menu.querySelector('button:not(:disabled)')?.focus();
}
}
btn.addEventListener('click', () => setOpen(menu.hidden));
for (const [id, write] of Object.entries(FORMATS)) {
el(id).addEventListener('click', () => {
setOpen(false);
const g = buildExportGeometry();
if (!g) return;
download(...write(g));
});
}
// click-away / Esc close it, the usual menu contract
document.addEventListener('pointerdown', (e) => {
if (!menu.hidden && !e.target.closest('.menu-wrap')) setOpen(false);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !menu.hidden) { setOpen(false); btn.focus(); }
});