partforge 0.19.0 → 0.20.1
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 +97 -1
- package/docs/ERROR-PATTERNS.md +6 -0
- package/package.json +3 -1
- package/src/app-bracket.js +9 -0
- package/src/app-embed-test.js +68 -0
- package/src/app-nameplate.js +9 -0
- package/src/app-text-smoke.js +10 -0
- package/src/bracket-worker.js +3 -0
- package/src/framework/app.css +23 -6
- package/src/framework/cutaway-controls.js +155 -0
- package/src/framework/cutaway-gizmo.js +686 -0
- package/src/framework/cutaway-math.js +53 -0
- package/src/framework/cutaway-render.js +338 -0
- package/src/framework/cutaway.js +469 -0
- package/src/framework/fonts.js +36 -0
- package/src/framework/geometry/curve-fill.js +86 -0
- package/src/framework/geometry/fonts/Roboto-LICENSE.txt +93 -0
- package/src/framework/geometry/fonts/Roboto-Regular.ttf +0 -0
- package/src/framework/geometry/fonts/default-font.js +3 -0
- package/src/framework/geometry/kernel-front.js +53 -0
- package/src/framework/geometry/kernel.js +1 -1
- package/src/framework/geometry/text2d.js +98 -0
- package/src/framework/geometry-service.js +21 -2
- package/src/framework/jobs.js +9 -0
- package/src/framework/mount.js +278 -223
- package/src/framework/selection/hover.js +102 -36
- package/src/framework/selection/raycast.js +4 -1
- package/src/framework/tooltip.js +282 -0
- package/src/framework/viewer-controls.js +25 -2
- package/src/framework/viewer-lighting.js +13 -0
- package/src/framework/viewer.js +83 -10
- package/src/nameplate-worker.js +3 -0
- package/src/parts/bracket.js +76 -0
- package/src/parts/nameplate.js +74 -0
- package/src/parts/text-smoke.js +21 -0
- package/src/testing/manifold.js +6 -2
- package/src/testing/occt.js +6 -2
- package/src/text-smoke-worker.js +3 -0
package/src/framework/mount.js
CHANGED
|
@@ -2,6 +2,8 @@ import "./app.css"; // shared chrome styles — every part-app gets them via mou
|
|
|
2
2
|
import { triggerDownload, downloadParts } from "./download.js";
|
|
3
3
|
import { createViewer } from "./viewer.js";
|
|
4
4
|
import { attachViewerControls } from "./viewer-controls.js";
|
|
5
|
+
import { attachCutawayControls } from "./cutaway-controls.js";
|
|
6
|
+
import { createTooltipPresenter } from "./tooltip.js";
|
|
5
7
|
import { loadCamera } from "./view-state.js";
|
|
6
8
|
import { buildControls } from "./controls.js";
|
|
7
9
|
import { relevantParamKeys } from "./param-deps.js";
|
|
@@ -17,6 +19,28 @@ import { createViewTabs } from "./view-tabs.js";
|
|
|
17
19
|
import { attachPickToggle, attachHoverLabels, attachPicker, formatSelection } from "./selection/index.js";
|
|
18
20
|
import { createPickRequestClient } from "./pick-request/index.js";
|
|
19
21
|
|
|
22
|
+
function createCleanupStack() {
|
|
23
|
+
const cleanups = [];
|
|
24
|
+
let disposed = false;
|
|
25
|
+
return {
|
|
26
|
+
defer(cleanup) {
|
|
27
|
+
cleanups.push(cleanup);
|
|
28
|
+
},
|
|
29
|
+
dispose() {
|
|
30
|
+
if (disposed) return;
|
|
31
|
+
disposed = true;
|
|
32
|
+
const errors = [];
|
|
33
|
+
while (cleanups.length) {
|
|
34
|
+
try { cleanups.pop()(); } catch (error) { errors.push(error); }
|
|
35
|
+
}
|
|
36
|
+
if (errors.length === 1) throw errors[0];
|
|
37
|
+
if (errors.length > 1) {
|
|
38
|
+
throw new AggregateError(errors, "partforge mount cleanup failed");
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
// Mount a full parametric-part app from a PartDefinition. mount is WIRING: the
|
|
21
45
|
// pieces it composes each live (and are tested) in their own module — the viewer,
|
|
22
46
|
// the schema-driven control panel, the regenerate state machine (regen-loop.js),
|
|
@@ -53,262 +77,293 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick,
|
|
|
53
77
|
pause: elements.chrome?.pause ?? byId("pause"),
|
|
54
78
|
reframe: elements.chrome?.reframe ?? byId("reframe"),
|
|
55
79
|
theme: elements.chrome?.theme ?? byId("theme"),
|
|
80
|
+
cutaway: elements.chrome?.cutaway ?? byId("cutaway"),
|
|
56
81
|
},
|
|
57
82
|
};
|
|
58
83
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
const cleanup = createCleanupStack();
|
|
85
|
+
try {
|
|
86
|
+
const viewer = createViewer(els.viewer, part);
|
|
87
|
+
cleanup.defer(() => viewer.dispose());
|
|
88
|
+
const tooltip = createTooltipPresenter({ id: null });
|
|
89
|
+
cleanup.defer(() => tooltip.dispose());
|
|
90
|
+
const cutawayChrome = attachCutawayControls(viewer, {
|
|
91
|
+
cutaway: els.chrome.cutaway,
|
|
92
|
+
}, { tooltip });
|
|
93
|
+
cleanup.defer(() => cutawayChrome.detach());
|
|
94
|
+
const hover = attachHoverLabels(viewer, { part, tooltip }); // always-on hover inspection (no-op on touch-only devices)
|
|
95
|
+
cleanup.defer(() => hover.detach());
|
|
96
|
+
const ui = createStatusUi({ ...els.status, exports: [els.exports.stl, els.exports.step, els.exports.threeMf] });
|
|
97
|
+
cleanup.defer(() => ui.setStatus(""));
|
|
98
|
+
cleanup.defer(() => ui.hideBusy());
|
|
62
99
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
100
|
+
// ?backend=occt|manifold forces the backend; otherwise it's detected per part.
|
|
101
|
+
let forcedBackend = new URLSearchParams(location.search).get("backend");
|
|
102
|
+
if (forcedBackend !== "occt" && forcedBackend !== "manifold") forcedBackend = null;
|
|
103
|
+
const backendFor = () => forcedBackend ?? detectBackend(part, params);
|
|
67
104
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
105
|
+
// ?debug shows the cache debug overlay; ?debug&nocache starts with caching off.
|
|
106
|
+
const qs = new URLSearchParams(location.search);
|
|
107
|
+
const debug = qs.has("debug");
|
|
108
|
+
let cachingOn = !(debug && qs.has("nocache"));
|
|
109
|
+
let lastGen = { skipped: 0, rebuilt: 0 }; // Layer-1 counts for the most recent generate
|
|
110
|
+
const dbg = debug
|
|
111
|
+
? createDebugOverlay({ initialCachingOn: cachingOn, onToggle: (on) => { cachingOn = on; forceRegen(); } })
|
|
112
|
+
: null;
|
|
113
|
+
if (dbg) cleanup.defer(() => dbg.detach());
|
|
76
114
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
// View tabs (generated from part.views) + live params. A tab switch shows the
|
|
116
|
+
// cached assembly instantly if it's current, else auto-builds what's missing.
|
|
117
|
+
const tabsCtl = createViewTabs(els.tabs, part, {
|
|
118
|
+
onChange: () => { cutawayChrome.reset(); refreshView(); updateRelevance(); loop.kick(); },
|
|
119
|
+
});
|
|
120
|
+
cleanup.defer(() => tabsCtl.detach());
|
|
121
|
+
const view = () => tabsCtl.current();
|
|
122
|
+
const params = { ...part.defaults };
|
|
84
123
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
124
|
+
// Current selection context for the pickers: the active view + live params +
|
|
125
|
+
// derived values. Shared by every pick mode below.
|
|
126
|
+
const getContext = () => {
|
|
127
|
+
let derived = {};
|
|
128
|
+
// A throwing derive must not crash the pick flow — proceed without derived context.
|
|
129
|
+
try { derived = resolveDerived(part, { ...part.defaults, ...params }); } catch { /* derived stays {} */ }
|
|
130
|
+
return { view: view(), params, derived };
|
|
131
|
+
};
|
|
93
132
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
// Click-to-select. Precedence (one click listener is ever live): the programmatic
|
|
134
|
+
// onPick option, else the ?pick clipboard toggle, else the ?pickserver client.
|
|
135
|
+
let picker = null; // { setActive, detach } — armed permanently for onPick
|
|
136
|
+
let pickToggle = null; // { detach }
|
|
137
|
+
let pickClient = null; // { detach }
|
|
138
|
+
if (onPick) {
|
|
139
|
+
picker = attachPicker(viewer, {
|
|
140
|
+
part, getContext,
|
|
141
|
+
onPick: (selection) => onPick({
|
|
142
|
+
selection,
|
|
143
|
+
label: selection.feature?.label ?? part.parts[selection.subPart]?.label ?? selection.subPart,
|
|
144
|
+
prompt: formatSelection(selection, { style: "prompt" }),
|
|
145
|
+
token: formatSelection(selection, { style: "token" }),
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
cleanup.defer(() => picker.detach());
|
|
149
|
+
picker.setActive(true);
|
|
150
|
+
} else if (qs.has("pick")) {
|
|
151
|
+
pickToggle = attachPickToggle(viewer, { part, getContext });
|
|
152
|
+
cleanup.defer(() => pickToggle.detach());
|
|
153
|
+
} else if (qs.has("pickserver")) {
|
|
154
|
+
// Agent-driven mode: arm the picker only when the local pick-server asks for a
|
|
155
|
+
// click. `?pickserver` or `?pickserver=http://host:port`.
|
|
156
|
+
const serverUrl = typeof qs.get("pickserver") === "string" && qs.get("pickserver")
|
|
157
|
+
? qs.get("pickserver") : "http://127.0.0.1:4518";
|
|
158
|
+
pickClient = createPickRequestClient({ serverUrl, viewer, part, getContext });
|
|
159
|
+
cleanup.defer(() => pickClient.detach());
|
|
160
|
+
}
|
|
119
161
|
|
|
120
|
-
|
|
121
|
-
|
|
162
|
+
let framedView = null; // the view the camera was last framed to (null until first show)
|
|
163
|
+
let cameraRestored = false; // saved camera applied once, on the first frame after load
|
|
122
164
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
165
|
+
// Per-sub-part cache-validity tracker (Layer 1): view/version/caching change over
|
|
166
|
+
// time, so they're passed as getters; params is a stable in-place-mutated object.
|
|
167
|
+
const cache = createMeshCache(part, viewer, {
|
|
168
|
+
params,
|
|
169
|
+
getView: view,
|
|
170
|
+
getParamsVersion: () => loop.version(),
|
|
171
|
+
isCaching: () => cachingOn,
|
|
172
|
+
});
|
|
173
|
+
const isCurrent = cache.isCurrent;
|
|
174
|
+
const missingParts = () => viewSubParts(part, view(), params).filter((n) => !isCurrent(n));
|
|
133
175
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
176
|
+
// The regenerate state machine (ready gating / debounce / stale-redo) lives in
|
|
177
|
+
// regen-loop.js; this send callback is the one place a build job is dispatched.
|
|
178
|
+
const loop = createRegenLoop({
|
|
179
|
+
missingParts,
|
|
180
|
+
send: (missing) => {
|
|
181
|
+
const needed = viewSubParts(part, view(), params);
|
|
182
|
+
lastGen = { skipped: needed.length - missing.length, rebuilt: missing.length }; // for the overlay
|
|
183
|
+
ui.showBusy("generating");
|
|
184
|
+
service.send({ type: "generate", subparts: missing, view: view(), params, cache: cachingOn }, backendFor());
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
cleanup.defer(() => loop.dispose());
|
|
145
188
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
189
|
+
// First-build readiness: resolves on the first accepted meshes result, rejects on
|
|
190
|
+
// a first-build error. Guarded against unhandled rejection when never awaited.
|
|
191
|
+
let readySettled = false;
|
|
192
|
+
let resolveReady, rejectReady;
|
|
193
|
+
const ready = new Promise((res, rej) => { resolveReady = res; rejectReady = rej; });
|
|
194
|
+
ready.catch(() => {});
|
|
195
|
+
cleanup.defer(() => {
|
|
196
|
+
if (readySettled) return;
|
|
197
|
+
readySettled = true;
|
|
198
|
+
rejectReady(new Error("disposed before first build"));
|
|
199
|
+
});
|
|
152
200
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
201
|
+
// Reflect the active view. If every needed part is current, show it and enable
|
|
202
|
+
// export. If stale (a regenerate is in flight), keep the old mesh visible so the
|
|
203
|
+
// view doesn't flicker. If nothing's built yet, show nothing.
|
|
204
|
+
// Show the assembly, framing the camera only the first time we show a given view
|
|
205
|
+
// (initial load / tab switch) — never on a regenerate, so zoom/orbit are kept.
|
|
206
|
+
function showView(needed) {
|
|
207
|
+
const frame = view() !== framedView;
|
|
208
|
+
viewer.showAssembly(needed, { frame });
|
|
209
|
+
if (frame) {
|
|
210
|
+
framedView = view();
|
|
211
|
+
if (!cameraRestored) {
|
|
212
|
+
const cam = loadCamera();
|
|
213
|
+
if (cam) viewer.setCameraState(cam);
|
|
214
|
+
cameraRestored = true;
|
|
215
|
+
}
|
|
167
216
|
}
|
|
168
217
|
}
|
|
169
|
-
}
|
|
170
218
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
219
|
+
function refreshView() {
|
|
220
|
+
const needed = viewSubParts(part, view(), params);
|
|
221
|
+
if (needed.every(isCurrent)) {
|
|
222
|
+
showView(needed);
|
|
223
|
+
ui.setExportEnabled(true);
|
|
224
|
+
const tris = needed.reduce((s, n) => s + viewer.subTriangles(n), 0);
|
|
225
|
+
ui.setStatus(`${tris.toLocaleString()} triangles`);
|
|
226
|
+
} else if (needed.every((n) => viewer.hasSubMesh(n))) {
|
|
227
|
+
showView(needed); // stale but present — keep it visible during regenerate
|
|
228
|
+
ui.setExportEnabled(false);
|
|
229
|
+
} else {
|
|
230
|
+
viewer.hideAssembly();
|
|
231
|
+
ui.setExportEnabled(false);
|
|
232
|
+
}
|
|
184
233
|
}
|
|
185
|
-
}
|
|
186
234
|
|
|
187
|
-
|
|
235
|
+
ui.showBusy("booting kernel"); // visible from first paint until the kernel is ready
|
|
188
236
|
|
|
189
|
-
|
|
190
|
-
|
|
237
|
+
// Bundle filename for a multi-part export (single parts download under their own name).
|
|
238
|
+
const zipName = `${part.meta?.title ?? "parts"}.zip`.toLowerCase().replace(/\s+/g, "-");
|
|
191
239
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
240
|
+
// --- shared message handler ------------------------------------------------
|
|
241
|
+
function onWorkerMessage({ data }) {
|
|
242
|
+
switch (data.type) {
|
|
243
|
+
case "ready":
|
|
244
|
+
loop.ready(); // auto-build the default view (keeps the busy spinner up)
|
|
245
|
+
break;
|
|
246
|
+
case "progress":
|
|
247
|
+
ui.showBusy(data.phase);
|
|
248
|
+
ui.setStatus(`${data.phase}…`);
|
|
249
|
+
break;
|
|
250
|
+
case "meshes": {
|
|
251
|
+
if (loop.buildDone()) { // stale results (params changed mid-build) are discarded
|
|
252
|
+
for (const m of data.meshes) {
|
|
253
|
+
viewer.setSubGeometry(m.name, m); // disposes any previous mesh for this name
|
|
254
|
+
cache.record(m.name);
|
|
255
|
+
}
|
|
256
|
+
ui.hideBusy();
|
|
257
|
+
refreshView();
|
|
258
|
+
if (data.ms && missingParts().length === 0) {
|
|
259
|
+
ui.setStatus(`${ui.statusText()} · ${(data.ms / 1000).toFixed(1)} s`);
|
|
260
|
+
}
|
|
261
|
+
dbg?.update({ ms: data.ms, hits: data.cache?.hits ?? 0, misses: data.cache?.misses ?? 0, skipped: lastGen.skipped, rebuilt: lastGen.rebuilt });
|
|
262
|
+
onBuild?.({ status: "success", ms: data.ms });
|
|
263
|
+
if (!readySettled) { readySettled = true; resolveReady(); }
|
|
207
264
|
}
|
|
265
|
+
loop.kick(); // stale → rebuild; fresh → the view may still need parts (tab switched mid-build)
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case "download-parts":
|
|
269
|
+
ui.hideBusy();
|
|
270
|
+
downloadParts(data, zipName);
|
|
271
|
+
ui.setStatus(`${data.parts.length} part(s) downloaded`);
|
|
272
|
+
break;
|
|
273
|
+
case "download":
|
|
208
274
|
ui.hideBusy();
|
|
275
|
+
triggerDownload(data.data, data.filename, data.mime);
|
|
276
|
+
ui.setStatus(`${data.filename} downloaded`);
|
|
277
|
+
break;
|
|
278
|
+
case "needs-occt":
|
|
279
|
+
forcedBackend = "occt"; // probe missed; this part needs OCCT — stick to it
|
|
280
|
+
loop.buildDone();
|
|
281
|
+
loop.kick();
|
|
282
|
+
break;
|
|
283
|
+
case "error":
|
|
284
|
+
loop.buildDone();
|
|
285
|
+
ui.hideBusy();
|
|
286
|
+
ui.setStatus(`failed: ${data.message}`, true);
|
|
209
287
|
refreshView();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
dbg?.update({ ms: data.ms, hits: data.cache?.hits ?? 0, misses: data.cache?.misses ?? 0, skipped: lastGen.skipped, rebuilt: lastGen.rebuilt });
|
|
214
|
-
onBuild?.({ status: "success", ms: data.ms });
|
|
215
|
-
if (!readySettled) { readySettled = true; resolveReady(); }
|
|
216
|
-
}
|
|
217
|
-
loop.kick(); // stale → rebuild; fresh → the view may still need parts (tab switched mid-build)
|
|
218
|
-
break;
|
|
288
|
+
onBuild?.({ status: "error", error: data.message });
|
|
289
|
+
if (!readySettled) { readySettled = true; rejectReady(new Error(data.message)); }
|
|
290
|
+
break;
|
|
219
291
|
}
|
|
220
|
-
case "download-parts":
|
|
221
|
-
ui.hideBusy();
|
|
222
|
-
downloadParts(data, zipName);
|
|
223
|
-
ui.setStatus(`${data.parts.length} part(s) downloaded`);
|
|
224
|
-
break;
|
|
225
|
-
case "download":
|
|
226
|
-
ui.hideBusy();
|
|
227
|
-
triggerDownload(data.data, data.filename, data.mime);
|
|
228
|
-
ui.setStatus(`${data.filename} downloaded`);
|
|
229
|
-
break;
|
|
230
|
-
case "needs-occt":
|
|
231
|
-
forcedBackend = "occt"; // probe missed; this part needs OCCT — stick to it
|
|
232
|
-
loop.buildDone();
|
|
233
|
-
loop.kick();
|
|
234
|
-
break;
|
|
235
|
-
case "error":
|
|
236
|
-
loop.buildDone();
|
|
237
|
-
ui.hideBusy();
|
|
238
|
-
ui.setStatus(`failed: ${data.message}`, true);
|
|
239
|
-
refreshView();
|
|
240
|
-
onBuild?.({ status: "error", error: data.message });
|
|
241
|
-
if (!readySettled) { readySettled = true; rejectReady(new Error(data.message)); }
|
|
242
|
-
break;
|
|
243
292
|
}
|
|
244
|
-
}
|
|
245
293
|
|
|
246
|
-
|
|
294
|
+
const service = createGeometryService({ createWorker, onMessage: onWorkerMessage });
|
|
295
|
+
cleanup.defer(() => service.terminate());
|
|
247
296
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
297
|
+
const panel = buildControls(els.controls, part.parameters, params, onParamChange);
|
|
298
|
+
cleanup.defer(() => panel.dispose());
|
|
299
|
+
const updateRelevance = () => panel.applyRelevance(relevantParamKeys(part, view(), params));
|
|
300
|
+
updateRelevance(); // initial view
|
|
251
301
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
302
|
+
function onParamChange() {
|
|
303
|
+
loop.markDirty(); // bump the version first: refreshView below must see the parts as stale
|
|
304
|
+
refreshView(); // keep showing the now-stale mesh (no flicker); disable export
|
|
305
|
+
updateRelevance();
|
|
306
|
+
}
|
|
257
307
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
308
|
+
// Re-run the active view under the current caching setting, so toggling the
|
|
309
|
+
// ?debug switch updates the readout for the same design without a param change.
|
|
310
|
+
function forceRegen() {
|
|
311
|
+
for (const n of viewSubParts(part, view(), params)) cache.forget(n);
|
|
312
|
+
refreshView();
|
|
313
|
+
loop.kick();
|
|
314
|
+
}
|
|
265
315
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
316
|
+
const onStlClick = () => {
|
|
317
|
+
ui.showBusy("exporting STL");
|
|
318
|
+
service.send({ type: "export-stl", view: view(), params, quality: "print" }, backendFor());
|
|
319
|
+
};
|
|
320
|
+
if (els.exports.stl) {
|
|
321
|
+
els.exports.stl.addEventListener("click", onStlClick);
|
|
322
|
+
cleanup.defer(() => els.exports.stl.removeEventListener("click", onStlClick));
|
|
323
|
+
}
|
|
271
324
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
325
|
+
const onStepClick = () => {
|
|
326
|
+
ui.showBusy("exporting STEP");
|
|
327
|
+
service.send({ type: "export-step", view: view(), params }, "occt"); // STEP is always OCCT
|
|
328
|
+
};
|
|
329
|
+
if (els.exports.step) {
|
|
330
|
+
els.exports.step.addEventListener("click", onStepClick);
|
|
331
|
+
cleanup.defer(() => els.exports.step.removeEventListener("click", onStepClick));
|
|
332
|
+
}
|
|
277
333
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
334
|
+
const on3mfClick = () => {
|
|
335
|
+
ui.showBusy("exporting 3MF");
|
|
336
|
+
service.send({ type: "export-3mf", view: view(), params, quality: "print" }, backendFor());
|
|
337
|
+
};
|
|
338
|
+
if (els.exports.threeMf) {
|
|
339
|
+
els.exports.threeMf.addEventListener("click", on3mfClick);
|
|
340
|
+
cleanup.defer(() => els.exports.threeMf.removeEventListener("click", on3mfClick));
|
|
341
|
+
}
|
|
283
342
|
|
|
284
|
-
|
|
285
|
-
|
|
343
|
+
// Optional host-page viewer chrome (pause / reframe / theme) + camera persistence.
|
|
344
|
+
const chrome = attachViewerControls(viewer, els.chrome, { tooltip });
|
|
345
|
+
cleanup.defer(() => chrome.detach());
|
|
286
346
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
disposed = true;
|
|
294
|
-
if (!readySettled) { readySettled = true; rejectReady(new Error("disposed before first build")); }
|
|
295
|
-
picker?.detach();
|
|
296
|
-
pickToggle?.detach();
|
|
297
|
-
pickClient?.detach();
|
|
298
|
-
hover.detach();
|
|
299
|
-
loop.dispose();
|
|
300
|
-
service.terminate();
|
|
301
|
-
els.exports.stl?.removeEventListener("click", onStlClick);
|
|
302
|
-
els.exports.step?.removeEventListener("click", onStepClick);
|
|
303
|
-
els.exports.threeMf?.removeEventListener("click", on3mfClick);
|
|
304
|
-
chrome.detach();
|
|
305
|
-
tabsCtl.detach();
|
|
306
|
-
panel.dispose();
|
|
307
|
-
dbg?.detach();
|
|
308
|
-
ui.hideBusy();
|
|
309
|
-
ui.setStatus("");
|
|
310
|
-
viewer.dispose();
|
|
311
|
-
}
|
|
347
|
+
// Full teardown of everything this mount created. Idempotent. A disposed runtime
|
|
348
|
+
// can never surface a late build result (workers are terminated, the loop is
|
|
349
|
+
// terminal), which is what makes cross-mount swap races safe for embedders.
|
|
350
|
+
function dispose() {
|
|
351
|
+
cleanup.dispose();
|
|
352
|
+
}
|
|
312
353
|
|
|
313
|
-
|
|
354
|
+
return { ready, dispose };
|
|
355
|
+
} catch (error) {
|
|
356
|
+
try {
|
|
357
|
+
cleanup.dispose();
|
|
358
|
+
} catch (cleanupError) {
|
|
359
|
+
const cleanupErrors = cleanupError instanceof AggregateError
|
|
360
|
+
? cleanupError.errors
|
|
361
|
+
: [cleanupError];
|
|
362
|
+
throw new AggregateError(
|
|
363
|
+
[error, ...cleanupErrors],
|
|
364
|
+
"partforge mount construction and cleanup failed",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
314
369
|
}
|