Files

192 lines
8.9 KiB
JavaScript

/**
* Export: the oriented part plus the fins/pad, as STL or 3MF -- every model of the
* plate (ui/models.js), each where it stands.
*/
import { writeBinarySTL, download } from '../stl.js';
import { writePlate3MF } from '../threemf.js';
import { splitInterface } from '../prop.js';
import { el } from './dom.js';
import { t } from './i18n.js';
import { finsVisible } from './settings.js';
import { part, topology, lastResult, rotM3, partName } from './part.js';
import { activeAdded, buildPending } from './finbuild.js';
import { sessionOf } from './session.js';
import { exportName } from '../exportname.js';
import { nozzle } from '../nozzle.js';
import { exportAll, currentPlate, locked } from './plates.js';
import { eachModel, parkedFins } from './models.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 } = topology;
// flat local [x,y,z]... (less `shift`: the file's frame back to the part's) to
// print-space vertices
const toPrint = (flat, shift = [0, 0, 0]) => {
const out = new Array(flat.length / 3);
for (let v = 0; v < out.length; v++) {
const x = flat[v * 3] - shift[0], y = flat[v * 3 + 1] - shift[1], z = flat[v * 3 + 2] - shift[2];
out[v] = [
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,
];
}
return out;
};
const partTris = toPrint(pos);
// 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';
// A file with a slicer's own parts (#104) goes out as the file had it: the part
// uncut, with its negative parts, modifiers and blockers posed the same way --
// the fins are for the cut part, which the slicer cuts again. `partTris` stays
// the part the fins were built for (the session and the STL use it).
const { slicer, centre } = part.geometry.userData;
const kept = slicer && centre ? {
partTris: toPrint(slicer.positions, centre),
colors: slicer.colors,
extras: slicer.extras.map((e) => ({ kind: e.kind, name: e.name, settings: e.settings, tris: toPrint(e.positions, centre) })),
} : null;
// the two settings the slicer must match, carried in the file name (#279)
const name = (stem, ext) => exportName(`${base}-${stem}`, ext,
{ nozzle, layerHeight: el('layer-height').valueAsNumber });
return { partTris, finTris, base, name, colors: part.geometry.userData.colors ?? null, slicer: kept };
}
/** `g` (buildExportGeometry) moved to `xy`, its model's spot on the plate. */
function placed(g, [x, y]) {
const at = (tris) => tris.map((p) => { const q = p.slice(); q[0] += x; q[1] += y; return q; });
const slicer = g.slicer && { ...g.slicer, partTris: at(g.slicer.partTris),
extras: g.slicer.extras.map((e) => ({ ...e, tris: at(e.tris) })) };
return { ...g, partTris: at(g.partTris), finTris: at(g.finTris), slicer };
}
/** Every model of the plate being edited, each its buildExportGeometry where it
* stands; a plate of one is just that one, as it is. */
async function plateGeometry() {
if (currentPlate().models.length === 1) {
const g = buildExportGeometry();
return g ? [g] : [];
}
return (await eachModel((xy) => {
const g = buildExportGeometry();
return g && placed(g, xy);
})).filter(Boolean);
}
/** A file name for the plate `gs`: the model's own, or the first one's and a count. */
const plateName = (gs, stem, ext) =>
gs[0].name(gs.length === 1 ? stem : `and-${gs.length - 1}-more-${stem}`, ext);
// The three formats, each given the plate's models (plateGeometry). 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': (gs) => [writeBinarySTL(gs.flatMap((g) => [...g.partTris, ...g.finTris]), gs[0].base),
plateName(gs, '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. Several models are each their own objects, in one file
// (threemf.js writePlate3MF); the session is one model's, so only a plate of one
// carries it.
'export-3mf': (gs) => {
const models = gs.map((g) => {
const { body, iface } = splitInterface(g.finTris);
const own = g.slicer ?? g;
return { partTris: own.partTris, finTris: body, name: g.base, iface, colors: own.colors,
extras: g.slicer?.extras ?? [] };
});
return [writePlate3MF(models, { title: gs[0].base, separate: true,
session: gs.length === 1 ? sessionOf(gs[0].partTris) : null }),
plateName(gs, 'fins', '.3mf')];
},
// Just the fins + pad (issue #5). One body, so 3MF would add nothing over STL.
'export-fins': (gs) => [writeBinarySTL(gs.flatMap((g) => g.finTris), `${gs[0].base} fins`),
plateName(gs, '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; and
// nothing while a rebuild is out -- the old fins under the new settings' name
const ready = !!(part && topology && lastResult) && !buildPending();
el('export-stl').disabled = el('export-3mf').disabled = !ready;
// the parked models' fins count too
const noFins = activeAdded().length === 0 && !parkedFins();
el('export-fins').disabled = !ready || noFins;
// the landing promises fins baked in: say so when the file would have none --
// with fins off, the button to click; with them on (Draw with nothing drawn yet,
// or a part with no overhang), just that the file is the part alone
const note = el('export-nofins');
note.hidden = !ready || !noFins;
note.textContent = finsVisible ? t('No supports on this plate: STL and 3MF write the part alone.')
: t('No fins yet: STL and 3MF write the part alone. Click Add fins first.');
menu.querySelector('button:not(:disabled)')?.focus();
}
}
btn.addEventListener('click', () => setOpen(menu.hidden));
for (const [id, write] of Object.entries(FORMATS)) {
el(id).addEventListener('click', async () => {
setOpen(false);
if (currentPlate().models.length === 1) {
const g = buildExportGeometry();
if (g) download(...write([g]));
return;
}
// the plate's other models are parked: each is made live in turn to write it
const gs = await locked(plateGeometry);
if (!gs?.length || (id === 'export-fins' && !gs.some((g) => g.finTris.length))) return;
download(...write(gs));
});
}
// Every plate, one 3MF each (ui/plates.js); shown only with more than one plate.
el('export-all').addEventListener('click', () => {
setOpen(false);
exportAll();
});
/** The plate being edited as a 3MF, [blob, name], for Export all plates (which holds
* the plates still); null when there is nothing to write. */
export async function plate3MF() {
const gs = await plateGeometry();
return gs.length ? FORMATS['export-3mf'](gs) : null;
}
// 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(); }
});