partforge 0.133.3 → 0.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/AUTHORING-PARTS.md +80 -19
- package/docs/KERNEL-CONTRACT.md +1 -1
- package/package.json +1 -1
- package/src/app-materials.js +3 -1
- package/src/framework/export/bundle.js +413 -0
- package/src/framework/export/drawing.js +356 -0
- package/src/framework/export/dxf.js +150 -0
- package/src/framework/export/formats.js +142 -0
- package/src/framework/export/layout.js +92 -0
- package/src/framework/export/placement.js +14 -0
- package/src/framework/export/readme.js +103 -0
- package/src/framework/export/svg.js +97 -0
- package/src/framework/export-controller.js +7 -2
- package/src/framework/export-rows.js +34 -0
- package/src/framework/jobs.js +16 -0
- package/src/framework/lint/rules-materials.js +9 -2
- package/src/framework/materials/assets/SOURCES.md +10 -4
- package/src/framework/materials/assets/pattern-plywood-color.jpg +0 -0
- package/src/framework/materials/assets/pattern-plywood-normal.jpg +0 -0
- package/src/framework/materials/assets/pattern-plywood-rough.jpg +0 -0
- package/src/framework/materials/assets.js +3 -0
- package/src/framework/materials/patterns.js +103 -0
- package/src/framework/materials/physical.js +5 -2
- package/src/framework/materials/presets.js +11 -5
- package/src/framework/materials/resolve.js +4 -2
- package/src/framework/materials/sheet-look.js +204 -0
- package/src/framework/mount.js +74 -37
- package/src/framework/process/exporters.js +18 -0
- package/src/framework/process/laser/export.js +53 -0
- package/src/framework/sheet/part.js +32 -17
- package/src/framework/viewer.js +53 -29
- package/src/index.js +4 -0
- package/src/parts/laser-box.js +1 -1
- package/src/parts/material-swatches.js +58 -2
- package/types/index.d.ts +72 -5
- package/types/part.d.ts +4 -0
- package/types/testing.d.ts +7 -3
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
// `sp.sheet.generatedBuild` is the build made here; an author who replaces `build`
|
|
10
10
|
// afterwards is detectable as `sp.build !== sp.sheet.generatedBuild`.
|
|
11
11
|
//
|
|
12
|
+
// Likewise `sp.sheet.generatedPlace` is the place made here (the pose, then the author's
|
|
13
|
+
// own) and `sp.sheet.place` the author's own place, or null: the realistic look
|
|
14
|
+
// (materials/sheet-look.js) trusts a sheet's canonical frame only with no author place
|
|
15
|
+
// and `sp.place === sp.sheet.generatedPlace`.
|
|
16
|
+
//
|
|
12
17
|
// Placement (decision 10): the pose applies for display AND export, then the author's
|
|
13
18
|
// own `place` — so STEP/3MF/STL files of a sheet forge come out assembled.
|
|
14
19
|
import { RENAMED_KEYS, RESERVED_KEYS, SHEET_KEYS, SUBPART_PASSTHROUGH_KEYS } from "./constants.js";
|
|
@@ -67,32 +72,42 @@ export function sheetPart(spec) {
|
|
|
67
72
|
const s = resolveSheet(k, sub, p, d);
|
|
68
73
|
return (processById(s.process).preview ?? sheetPreview)(k, s);
|
|
69
74
|
};
|
|
75
|
+
|
|
76
|
+
// Placement (decision 10): the pose applies for display AND export, then the author's own
|
|
77
|
+
// place. Made BEFORE the record is frozen, so the record can name it. Everything the
|
|
78
|
+
// place reads is taken NOW, into the same values the record freezes — never read off
|
|
79
|
+
// `spec` later, which an author may reuse and change for the next panel: the build and
|
|
80
|
+
// the burn's frame both read the frozen record, and a place that read the spec live would
|
|
81
|
+
// move the panel away from them.
|
|
82
|
+
const pose = spec.pose ?? null;
|
|
83
|
+
const thickness = spec.thickness;
|
|
84
|
+
const authorPlace = spec.place ?? null;
|
|
85
|
+
const generatedPlace = pose !== null || authorPlace ? (solid, ctx) => {
|
|
86
|
+
const resolved = typeof pose === "function" ? pose(ctx.p, ctx.d) : pose;
|
|
87
|
+
let posed = solid;
|
|
88
|
+
if (resolved !== null) {
|
|
89
|
+
const reason = validatePose(resolved);
|
|
90
|
+
if (reason) throw new Error(`sheet pose: ${reason}`);
|
|
91
|
+
const t = typeof thickness === "function" ? thickness(ctx.p, ctx.d) : thickness;
|
|
92
|
+
posed = applyPose(solid, resolved, t);
|
|
93
|
+
}
|
|
94
|
+
return authorPlace ? authorPlace(posed, ctx) : posed;
|
|
95
|
+
} : null;
|
|
96
|
+
|
|
70
97
|
const sheet = Object.freeze({
|
|
71
98
|
process,
|
|
72
99
|
material: spec.material,
|
|
73
|
-
thickness
|
|
100
|
+
thickness,
|
|
74
101
|
profile: spec.profile,
|
|
75
102
|
score: spec.score ?? null,
|
|
76
103
|
engrave: spec.engrave ?? null,
|
|
77
|
-
pose
|
|
104
|
+
pose,
|
|
105
|
+
place: authorPlace,
|
|
78
106
|
generatedBuild,
|
|
107
|
+
generatedPlace,
|
|
79
108
|
});
|
|
80
109
|
sub.build = generatedBuild;
|
|
81
|
-
|
|
82
|
-
const authorPlace = spec.place;
|
|
83
|
-
if (sheet.pose !== null || authorPlace) {
|
|
84
|
-
sub.place = (solid, ctx) => {
|
|
85
|
-
const pose = typeof sheet.pose === "function" ? sheet.pose(ctx.p, ctx.d) : sheet.pose;
|
|
86
|
-
let posed = solid;
|
|
87
|
-
if (pose !== null) {
|
|
88
|
-
const reason = validatePose(pose);
|
|
89
|
-
if (reason) throw new Error(`sheet pose: ${reason}`);
|
|
90
|
-
const t = typeof sheet.thickness === "function" ? sheet.thickness(ctx.p, ctx.d) : sheet.thickness;
|
|
91
|
-
posed = applyPose(solid, pose, t);
|
|
92
|
-
}
|
|
93
|
-
return authorPlace ? authorPlace(posed, ctx) : posed;
|
|
94
|
-
};
|
|
95
|
-
}
|
|
110
|
+
if (generatedPlace) sub.place = generatedPlace;
|
|
96
111
|
sub.sheet = sheet;
|
|
97
112
|
return sub;
|
|
98
113
|
}
|
package/src/framework/viewer.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ensureBoxUVs } from "./materials/uv.js";
|
|
|
12
12
|
import { loadEnvironmentRig } from "./materials/environment.js";
|
|
13
13
|
import { assetUrl } from "./materials/assets.js";
|
|
14
14
|
import { resolveEnvironmentId } from "./materials/resolve.js";
|
|
15
|
+
import { burnsFor, realisticDisplay } from "./materials/sheet-look.js";
|
|
15
16
|
import { neutralToneMapToSrgb8 } from "./materials/tonemap-readback.js";
|
|
16
17
|
import { createCutaway } from "./cutaway.js";
|
|
17
18
|
import { CUTAWAY_OVERLAY_RENDER_ORDER } from "./cutaway-render.js";
|
|
@@ -591,9 +592,11 @@ export function createViewer(container, part) {
|
|
|
591
592
|
const textureCache = new Map(); // asset file name -> Texture
|
|
592
593
|
const textureLoader = new THREE.TextureLoader();
|
|
593
594
|
let pmrem = null;
|
|
594
|
-
let printFrames = {};
|
|
595
|
-
let
|
|
596
|
-
let
|
|
595
|
+
let printFrames = {}; // name -> mat4 array: layer lines' display → export map
|
|
596
|
+
let sheetFrames = {}; // name -> { frame, t, plies }: a burning sheet's canonical frame (sheet-look.js)
|
|
597
|
+
let frameSource = null; // () => { print, sheet }; mount's lazy provider (setFrameSource)
|
|
598
|
+
let framesStale = false; // the source has frames the materials have not seen
|
|
599
|
+
const IDENTITY_MAT4 = new THREE.Matrix4().toArray();
|
|
597
600
|
const modeListeners = new Set();
|
|
598
601
|
const envListeners = new Set();
|
|
599
602
|
const SHADOW_LOWRES_MS = 100; // at most one low-res shadow per this, while a part moves
|
|
@@ -728,9 +731,14 @@ export function createViewer(container, part) {
|
|
|
728
731
|
function physicalFor(name) {
|
|
729
732
|
let m = physicalMats.get(name);
|
|
730
733
|
if (!m) {
|
|
731
|
-
|
|
734
|
+
const sp = part.parts[name];
|
|
735
|
+
// `burn` is decided once per sub-part (sheet-look.js burnsFor): only a laser sheet in
|
|
736
|
+
// a wood compiles the burn pass, so every other program is exactly what it was.
|
|
737
|
+
// realisticDisplay: a laser sheet with no material draws as its stock (sheet-look.js).
|
|
738
|
+
m = cloneKeepsPattern(buildPhysicalMaterial(realisticDisplay(sp), { printFrame: printFrames[name], burn: burnsFor(sp), loadTexture }));
|
|
732
739
|
physicalMats.set(name, m);
|
|
733
740
|
syncGrain(name);
|
|
741
|
+
applySheetFrame(m, sheetFrames[name]);
|
|
734
742
|
}
|
|
735
743
|
return m;
|
|
736
744
|
}
|
|
@@ -859,7 +867,7 @@ export function createViewer(container, part) {
|
|
|
859
867
|
// ground only moves on showAssembly, never because a capture happened.
|
|
860
868
|
function enterRealistic(rig, { live = true, reground = true } = {}) {
|
|
861
869
|
try {
|
|
862
|
-
|
|
870
|
+
syncFrames(); // before the swap: the first realistic frame has the export-pose layers and the sheets' burns
|
|
863
871
|
renderMode = "realistic";
|
|
864
872
|
// A regen can land between the proxy compile and here, while the mode
|
|
865
873
|
// was still CAD, so setSubGeometry skipped the UVs. Idempotent.
|
|
@@ -929,7 +937,7 @@ export function createViewer(container, part) {
|
|
|
929
937
|
// target bound — each only for the synchronous compile() inside
|
|
930
938
|
// compileAsync, so the view on screen is untouched while it waits.
|
|
931
939
|
function compileRealistic(rig, { forCapture = false } = {}) {
|
|
932
|
-
|
|
940
|
+
syncFrames();
|
|
933
941
|
const proxy = new THREE.Scene();
|
|
934
942
|
proxy.environment = rig.envMap;
|
|
935
943
|
for (const n of names) {
|
|
@@ -1030,30 +1038,45 @@ export function createViewer(container, part) {
|
|
|
1030
1038
|
for (const [n, m] of physicalMats) m.userData.patternUniforms?.pfPrintFrame.value.fromArray(printFrames[n] ?? identity);
|
|
1031
1039
|
}
|
|
1032
1040
|
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1041
|
+
// A burning sheet's canonical frame (sheet-look.js sheetFrameFor). No frame — not yet
|
|
1042
|
+
// delivered, or not computable at these params — is pfSheetT = 0: plain wood.
|
|
1043
|
+
function applySheetFrame(m, f) {
|
|
1044
|
+
const u = m.userData.patternUniforms;
|
|
1045
|
+
if (!u?.pfSheetFrame) return;
|
|
1046
|
+
u.pfSheetFrame.value.fromArray(f?.frame ?? IDENTITY_MAT4);
|
|
1047
|
+
u.pfSheetT.value = f?.t ?? 0;
|
|
1048
|
+
u.pfPlies.value = f?.plies ?? 0;
|
|
1049
|
+
}
|
|
1050
|
+
function setSheetFrames(frames) {
|
|
1051
|
+
sheetFrames = frames ?? {};
|
|
1052
|
+
for (const [n, m] of physicalMats) applySheetFrame(m, sheetFrames[n]);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// The lazy form mount uses. Frames are PULLED only when something is about to draw the
|
|
1056
|
+
// realistic look: the live view (or a switch loading), and any capture that borrows it —
|
|
1057
|
+
// compileRealistic and enterRealistic are the two doors every such path goes through. A
|
|
1058
|
+
// delivery just marks them stale (invalidateFrames); in CAD nothing is computed. One
|
|
1059
|
+
// source carries both kinds: `print` (layer lines; a placed sub-part's costs two
|
|
1060
|
+
// geometry-free probe builds, and every sub-part with no material draws layer lines) and
|
|
1061
|
+
// `sheet` (a burning sheet's canonical frame, from data).
|
|
1062
|
+
function setFrameSource(source) {
|
|
1063
|
+
frameSource = typeof source === "function" ? source : null;
|
|
1064
|
+
invalidateFrames();
|
|
1065
|
+
}
|
|
1066
|
+
function invalidateFrames() {
|
|
1067
|
+
framesStale = true;
|
|
1068
|
+
if (renderMode === "realistic" || realisticPending) syncFrames();
|
|
1069
|
+
}
|
|
1070
|
+
function syncFrames() {
|
|
1071
|
+
if (!frameSource || !framesStale) return;
|
|
1072
|
+
framesStale = false;
|
|
1051
1073
|
let frames;
|
|
1052
|
-
try { frames =
|
|
1053
|
-
console.warn("partforge: computing
|
|
1074
|
+
try { frames = frameSource(); } catch (e) {
|
|
1075
|
+
console.warn("partforge: computing material frames failed", e);
|
|
1054
1076
|
return;
|
|
1055
1077
|
}
|
|
1056
|
-
setPrintFrames(frames);
|
|
1078
|
+
setPrintFrames(frames?.print);
|
|
1079
|
+
setSheetFrames(frames?.sheet);
|
|
1057
1080
|
}
|
|
1058
1081
|
|
|
1059
1082
|
// Resolves once the current environment's rig is loaded (captures wait on it).
|
|
@@ -2735,8 +2758,9 @@ export function createViewer(container, part) {
|
|
|
2735
2758
|
isRealisticPending: () => realisticPending,
|
|
2736
2759
|
onEnvironmentChange: (cb) => { envListeners.add(cb); return () => envListeners.delete(cb); },
|
|
2737
2760
|
setPrintFrames,
|
|
2738
|
-
|
|
2739
|
-
|
|
2761
|
+
setSheetFrames,
|
|
2762
|
+
setFrameSource,
|
|
2763
|
+
invalidateFrames,
|
|
2740
2764
|
whenRealisticReady,
|
|
2741
2765
|
dispose,
|
|
2742
2766
|
}, () => requestRender(API_GRACE_MS));
|
package/src/index.js
CHANGED
|
@@ -4,3 +4,7 @@
|
|
|
4
4
|
// Part build functions import geometry helpers from "partforge/geometry" instead.
|
|
5
5
|
export { mount } from "./framework/index.js";
|
|
6
6
|
export { viewSubParts } from "./framework/part-model.js";
|
|
7
|
+
// The cut & print kit's options surface, for a host that draws its own options screen
|
|
8
|
+
// (partforge-cloud's coupling test reads KIT_OPTIONS_ERROR from here). export/formats.js
|
|
9
|
+
// is import-free, so this adds nothing to the app bundle but a few constants.
|
|
10
|
+
export { EXPORT_FORMATS, KIT_OPTIONS_ERROR, KIT_DEFAULTS, validateKitOptions } from "./framework/export/formats.js";
|
package/src/parts/laser-box.js
CHANGED
|
@@ -42,7 +42,7 @@ const hingeSlots = (k, p, d, pose, z) => d.hingeX.flatMap((hx) =>
|
|
|
42
42
|
[hx - HINGE.tongueX, hx + HINGE.tongueX].map((x) =>
|
|
43
43
|
k.shape2d(tab(p).slot).translate(worldToSheet(pose, [x, p.depth / 2, z]))));
|
|
44
44
|
|
|
45
|
-
const PLY = { views: ["box"], display: { material: "
|
|
45
|
+
const PLY = { views: ["box"], display: { material: "plywood" }, material: "birch plywood", thickness: (p) => p.t };
|
|
46
46
|
const panel = (name, label, extra = {}) => sheetPart({
|
|
47
47
|
...PLY, label,
|
|
48
48
|
profile: (k, p, d) => d.box[name].outline, // drawn as seen from outside
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Dev-only contact sheet for the material library: one 30 mm sample per preset,
|
|
2
2
|
// laid out in a grid, so a change to a preset or the pattern shader can be
|
|
3
|
-
// judged by eye in every environment (materials.html).
|
|
3
|
+
// judged by eye in every environment (materials.html). The "Laser-cut" views
|
|
4
|
+
// (below) do the same for the laser burn. Not a reference part.
|
|
4
5
|
import { PRESETS } from "../framework/materials/presets.js";
|
|
6
|
+
import { sheetPart } from "../framework/sheet/part.js";
|
|
7
|
+
import { sheetHole } from "../framework/sheet/joinery.js";
|
|
5
8
|
|
|
6
9
|
const ids = Object.keys(PRESETS);
|
|
7
10
|
const COLS = 6, PITCH = 40;
|
|
@@ -19,10 +22,59 @@ const FILAMENT_COLOURS = [
|
|
|
19
22
|
const withHole = (k) =>
|
|
20
23
|
k.box({ size: [30, 30, 12] }).fillet({ r: 3 }).cut(k.cylinder({ r: 5, h: 20 }).translate([0, 0, -4]));
|
|
21
24
|
|
|
25
|
+
// The "Laser-cut" view: sheet swatches as the laser burn draws them — each 30 × 30 with a
|
|
26
|
+
// cut hole, an engraved label (its thickness) and a score line — in every wood at 3, 6 and
|
|
27
|
+
// 9 mm; one oak panel standing on edge (posed, so its frame is not the identity); and the
|
|
28
|
+
// looks that must NOT burn: a clear-acrylic sheet and an oak block that is not a sheet part.
|
|
29
|
+
// "Laser-cut, unburnt" is its twin: the same swatches with an identity author `place`,
|
|
30
|
+
// which shows plain wood (materials/sheet-look.js) — same geometry, same place, same
|
|
31
|
+
// material — so the two views differ ONLY by the burn and scripts/capture-contact-sheet.mjs
|
|
32
|
+
// can diff them pixel for pixel — and one row of sheets that name no material, drawn as
|
|
33
|
+
// their stock (sheet-look.js STOCK_LOOKS).
|
|
34
|
+
const SW = 30;
|
|
35
|
+
const WOODS = ["plywood", "oak", "walnut"];
|
|
36
|
+
const CONTROLS_Y = -WOODS.length * PITCH;
|
|
37
|
+
const LASER_SWATCHES = [
|
|
38
|
+
...WOODS.flatMap((material, row) => [3, 6, 9].map((t, col) => ({
|
|
39
|
+
key: `${material}_${t}`, label: `${PRESETS[material].label}, ${t} mm`, at: [col * PITCH, -row * PITCH], t, display: { material },
|
|
40
|
+
}))),
|
|
41
|
+
{ key: "acrylic_3", label: "Clear acrylic, 3 mm", at: [0, CONTROLS_Y], t: 3, display: { material: "clear-acrylic" }, stock: "clear acrylic" },
|
|
42
|
+
{ key: "oak_standing", label: "Oak, 3 mm, standing", at: [PITCH, CONTROLS_Y], t: 3, display: { material: "oak" }, standing: true },
|
|
43
|
+
{ key: "oak_block", label: "Oak block (not a sheet part)", at: [2 * PITCH, CONTROLS_Y], t: 3, display: { material: "oak" }, block: true },
|
|
44
|
+
{ key: "stock_plywood", label: "No material, \"birch plywood\" stock", at: [0, CONTROLS_Y - PITCH], t: 3, stock: "birch plywood" },
|
|
45
|
+
{ key: "stock_acrylic", label: "No material, \"clear acrylic\" stock", at: [PITCH, CONTROLS_Y - PITCH], t: 3, stock: "clear acrylic" },
|
|
46
|
+
{ key: "stock_stained", label: "A bare colour on \"birch plywood\" stock", at: [2 * PITCH, CONTROLS_Y - PITCH], t: 3, stock: "birch plywood", display: { color: 0x2e8b3d } },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function laserSwatch(s, unburnt) {
|
|
50
|
+
const [x, y] = s.at;
|
|
51
|
+
const common = {
|
|
52
|
+
label: unburnt ? `${s.label} (unburnt)` : s.label,
|
|
53
|
+
views: [unburnt ? "unburnt" : "laser"],
|
|
54
|
+
...(s.display ? { display: s.display } : {}),
|
|
55
|
+
};
|
|
56
|
+
if (s.block) {
|
|
57
|
+
return { ...common, build: (k) => k.box({ min: [x, y, 0], max: [x + SW, y + SW, s.t] })
|
|
58
|
+
.cut(k.cylinder({ d: 8, h: s.t + 2 }).translate([x + 21, y + 21, -1])) };
|
|
59
|
+
}
|
|
60
|
+
// A standing swatch is drawn at the drawing origin; its pose stands it on edge at `at`.
|
|
61
|
+
const [u, v] = s.standing ? [0, 0] : [x, y];
|
|
62
|
+
return sheetPart({
|
|
63
|
+
...common,
|
|
64
|
+
material: s.stock ?? "birch plywood",
|
|
65
|
+
thickness: s.t,
|
|
66
|
+
profile: (k) => k.shape2d([[u, v], [u + SW, v], [u + SW, v + SW], [u, v + SW]]).cut(sheetHole({ d: 8, at: [u + 21, v + 21] })),
|
|
67
|
+
engrave: (k) => k.text2d(String(s.t), { size: 10, align: "center", valign: "middle" }).translate([u + 9, v + 20]),
|
|
68
|
+
score: () => [[[u + 3, v + 7], [u + SW - 3, v + 7]]],
|
|
69
|
+
...(s.standing ? { pose: { face: "-Y", up: "+Z", at: [x, y, 0] } } : {}),
|
|
70
|
+
...(unburnt ? { place: (solid) => solid } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
22
74
|
export default {
|
|
23
75
|
meta: { title: "Material swatches", units: "mm", environment: "studio" },
|
|
24
76
|
defaults: {},
|
|
25
|
-
views: { all: { label: "All presets", default: true }, colours: { label: "PLA colours" }, petg: { label: "PETG colours" }, print: { label: "Layer lines" } },
|
|
77
|
+
views: { all: { label: "All presets", default: true }, colours: { label: "PLA colours" }, petg: { label: "PETG colours" }, print: { label: "Layer lines" }, laser: { label: "Laser-cut" }, unburnt: { label: "Laser-cut, unburnt" } },
|
|
26
78
|
parts: {
|
|
27
79
|
...Object.fromEntries(ids.map((id, i) => [id.replaceAll("-", "_"), {
|
|
28
80
|
label: PRESETS[id].label,
|
|
@@ -65,5 +117,9 @@ export default {
|
|
|
65
117
|
build: (k) => k.box({ size: [30, 12, 40] }).translate([50, 0, 0]),
|
|
66
118
|
display: { material: "pla-print", color: 0xe0592a },
|
|
67
119
|
},
|
|
120
|
+
...Object.fromEntries(LASER_SWATCHES.flatMap((s) => [
|
|
121
|
+
[`laser_${s.key}`, laserSwatch(s, false)],
|
|
122
|
+
[`unburnt_${s.key}`, laserSwatch(s, true)],
|
|
123
|
+
])),
|
|
68
124
|
},
|
|
69
125
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -25,8 +25,49 @@ export type HostRailLayout =
|
|
|
25
25
|
| { mode: "overlay" }
|
|
26
26
|
| null;
|
|
27
27
|
|
|
28
|
-
/** An export file format. STEP is routed to OCCT automatically. */
|
|
29
|
-
export type ExportFormat = "stl" | "step" | "3mf";
|
|
28
|
+
/** An export file format. STEP is routed to OCCT automatically; `"bundle"` is the cut & print kit (a ZIP). */
|
|
29
|
+
export type ExportFormat = "stl" | "step" | "3mf" | "bundle";
|
|
30
|
+
|
|
31
|
+
/** One entry of `runtime.listExportFormats()` / `EXPORT_FORMATS`. `needsSheet` marks a format that needs a sheet part checked. */
|
|
32
|
+
export interface ExportFormatInfo {
|
|
33
|
+
id: ExportFormat;
|
|
34
|
+
label: string;
|
|
35
|
+
ext: string;
|
|
36
|
+
mime: string;
|
|
37
|
+
needsSheet: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A sheet part's stock, as `listExportableParts()` reports it. `group` is the kit's stock-group key, e.g. `"birch plywood|3.00"`. */
|
|
41
|
+
export interface ExportableSheetInfo {
|
|
42
|
+
process: string;
|
|
43
|
+
material: string;
|
|
44
|
+
thickness: number;
|
|
45
|
+
group: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The cut & print kit's download options — `exportParts({ format: "bundle", options })`.
|
|
50
|
+
* Every key is optional; `validateKitOptions` fills the defaults and throws an error
|
|
51
|
+
* starting `"cut kit options:"` for anything it cannot honour.
|
|
52
|
+
*/
|
|
53
|
+
export interface KitOptions {
|
|
54
|
+
/** `"own-laser"` (default): SVG, laid out on sheets. `"service"`: DXF, one cut-only file per piece. */
|
|
55
|
+
destination?: "own-laser" | "service";
|
|
56
|
+
/** Derived from `destination`; when given it must match. */
|
|
57
|
+
cutFormat?: "svg" | "dxf";
|
|
58
|
+
/** mm, 0–0.5: the cut lines move out by half of it. Default 0 (nominal geometry). */
|
|
59
|
+
kerf?: number;
|
|
60
|
+
colors?: "lightburn";
|
|
61
|
+
/** Sheet size in mm per stock group; `"*"` is an explicit default. `null` (default) is 300 × 300 mm for every group. */
|
|
62
|
+
stock?: Array<{ group: string; size: [number, number] }> | null;
|
|
63
|
+
/** mm, 0–50. Default 5. */
|
|
64
|
+
margin?: number;
|
|
65
|
+
/** mm, 1–50. Default 3. */
|
|
66
|
+
spacing?: number;
|
|
67
|
+
printFormat?: "stl" | "3mf";
|
|
68
|
+
/** Whole number, 1–20. Default 1. */
|
|
69
|
+
sets?: number;
|
|
70
|
+
}
|
|
30
71
|
|
|
31
72
|
/**
|
|
32
73
|
* A semantic click result: which sub-part was hit, where (in the sub-part's own
|
|
@@ -346,9 +387,11 @@ export interface ExportPartsOptions {
|
|
|
346
387
|
/** Sub-part names, as `listExportableParts()` reports them. */
|
|
347
388
|
parts: string[];
|
|
348
389
|
format: ExportFormat;
|
|
349
|
-
/** Mesh quality for STL/3MF. Defaults to `"print"`. */
|
|
390
|
+
/** Mesh quality for STL/3MF (and a kit's printed parts). Defaults to `"print"`. */
|
|
350
391
|
quality?: "preview" | "print";
|
|
351
392
|
onProgress?: (phase: string) => void;
|
|
393
|
+
/** The cut & print kit's download options (`format: "bundle"`; the other formats ignore them). */
|
|
394
|
+
options?: KitOptions;
|
|
352
395
|
}
|
|
353
396
|
|
|
354
397
|
export interface CaptureCurrentOptions {
|
|
@@ -581,9 +624,12 @@ export interface PartRuntime {
|
|
|
581
624
|
onContextLost(listener: () => void): () => void;
|
|
582
625
|
/**
|
|
583
626
|
* Every exportable sub-part — excludes any `exportable: false` part, respects
|
|
584
|
-
* each part's `enabled(params)` — INDEPENDENT of the active view.
|
|
627
|
+
* each part's `enabled(params)` — INDEPENDENT of the active view. A sheet
|
|
628
|
+
* part's row adds `sheet`, evaluated at the current params (omitted when that throws).
|
|
585
629
|
*/
|
|
586
|
-
listExportableParts(): Array<{ name: string; label: string }>;
|
|
630
|
+
listExportableParts(): Array<{ name: string; label: string; sheet?: ExportableSheetInfo }>;
|
|
631
|
+
/** The formats `exportParts` writes in this version — fresh copies of `EXPORT_FORMATS`. */
|
|
632
|
+
listExportFormats(): ExportFormatInfo[];
|
|
587
633
|
/**
|
|
588
634
|
* Headless export of a chosen subset. Resolves once the file is written
|
|
589
635
|
* (handed to your `onDownload` sink, or downloaded directly); rejects on
|
|
@@ -648,6 +694,27 @@ export interface PartRuntime {
|
|
|
648
694
|
/** Mount a full parametric-part app from a `PartDefinition`. */
|
|
649
695
|
export function mount(part: PartDefinition, options: MountOptions): PartRuntime;
|
|
650
696
|
|
|
697
|
+
/** Every format `exportParts` can write in this version (`runtime.listExportFormats()` returns copies). */
|
|
698
|
+
export const EXPORT_FORMATS: ReadonlyArray<ExportFormatInfo>;
|
|
699
|
+
|
|
700
|
+
/** The prefix every cut & print kit options error starts with. */
|
|
701
|
+
export const KIT_OPTIONS_ERROR: "cut kit options:";
|
|
702
|
+
|
|
703
|
+
/** The kit's option defaults; `stock: null` is 300 × 300 mm for every stock group. */
|
|
704
|
+
export const KIT_DEFAULTS: Readonly<{
|
|
705
|
+
destination: "own-laser";
|
|
706
|
+
kerf: number;
|
|
707
|
+
colors: "lightburn";
|
|
708
|
+
stock: null;
|
|
709
|
+
margin: number;
|
|
710
|
+
spacing: number;
|
|
711
|
+
printFormat: "stl";
|
|
712
|
+
sets: number;
|
|
713
|
+
}>;
|
|
714
|
+
|
|
715
|
+
/** Normalise kit options — defaults filled, `cutFormat` derived — or throw a `"cut kit options: …"` error. */
|
|
716
|
+
export function validateKitOptions(options: unknown): Required<KitOptions>;
|
|
717
|
+
|
|
651
718
|
/**
|
|
652
719
|
* The sub-parts a view shows: declared in the view and `enabled` for these
|
|
653
720
|
* params, in `Object.keys(part.parts)` order. Handy for app-side view logic.
|
package/types/part.d.ts
CHANGED
|
@@ -441,6 +441,10 @@ export interface SheetDeclaration<P = ResolvedParams, D = Derived> {
|
|
|
441
441
|
pose: SheetPose | ((p: P, d: D) => SheetPose | null) | null;
|
|
442
442
|
/** The build `sheetPart` made; `sub.build !== sub.sheet.generatedBuild` is a custom build. */
|
|
443
443
|
generatedBuild: (k: GeometryKernel, p: P, d: D) => Solid;
|
|
444
|
+
/** The author's own `place` (the spec's), run after the pose; `null` when there is none. */
|
|
445
|
+
place: ((solid: Solid, ctx: PlaceContext<P, D>) => Solid) | null;
|
|
446
|
+
/** The `place` `sheetPart` installed (the pose, then the author's own), or `null`; `sub.place !== sub.sheet.generatedPlace` is a replaced place. */
|
|
447
|
+
generatedPlace: ((solid: Solid, ctx: PlaceContext<P, D>) => Solid) | null;
|
|
444
448
|
}
|
|
445
449
|
|
|
446
450
|
export interface ViewDefinition {
|
package/types/testing.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { GeometryKernel, Mesh, Point3, Solid } from "./kernel.js";
|
|
10
10
|
import type { Derived, FontSource, PartDefinition, ResolvedParams } from "./part.js";
|
|
11
|
+
import type { KitOptions } from "./index.js";
|
|
11
12
|
|
|
12
13
|
export type { Derived, GeometryKernel, Mesh, PartDefinition, ResolvedParams, Solid };
|
|
13
14
|
|
|
@@ -46,7 +47,8 @@ export type MatchTarget =
|
|
|
46
47
|
|
|
47
48
|
/** A job the worker loop accepts. */
|
|
48
49
|
export interface WorkerJob {
|
|
49
|
-
|
|
50
|
+
/** `export-bundle` builds the cut & print kit (a ZIP) from `parts`, as `exportParts({ format: "bundle" })` does. */
|
|
51
|
+
type: "generate" | "export-stl" | "export-step" | "export-3mf" | "export-bundle" | "inspect";
|
|
50
52
|
view?: string;
|
|
51
53
|
params?: ResolvedParams;
|
|
52
54
|
/** `generate`: which sub-parts to build. */
|
|
@@ -55,10 +57,12 @@ export interface WorkerJob {
|
|
|
55
57
|
parts?: string[];
|
|
56
58
|
quality?: "preview" | "print";
|
|
57
59
|
cache?: boolean;
|
|
58
|
-
/** Correlation id echoed on every reply. */
|
|
59
|
-
jobId?: number;
|
|
60
|
+
/** Correlation id echoed on every reply (the export controller mints strings). */
|
|
61
|
+
jobId?: number | string;
|
|
60
62
|
/** Single-file export name base. */
|
|
61
63
|
name?: string;
|
|
64
|
+
/** `export-bundle` only: the kit's download options; absent means every default. */
|
|
65
|
+
options?: KitOptions;
|
|
62
66
|
/**
|
|
63
67
|
* `inspect`: score the part's six canonical silhouettes against these. Absent or
|
|
64
68
|
* empty leaves `match` off the report entirely.
|