partforge 0.95.0 → 0.96.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
CHANGED
|
@@ -1925,6 +1925,15 @@ returns instead:
|
|
|
1925
1925
|
`onDownload` sink, or downloaded directly if you don't supply one); rejects on
|
|
1926
1926
|
build/export failure or an empty selection. Placement uses the current
|
|
1927
1927
|
view. STEP is routed to OCCT automatically.
|
|
1928
|
+
- `runtime.warmExportKernel() → Promise<boolean>` — pay OCCT's cold boot *before* an
|
|
1929
|
+
export needs it. Because STEP is pinned to OCCT and OCCT's ~11 MB WASM loads on its
|
|
1930
|
+
first job, a part whose preview ran on Manifold pays that whole boot inside its first
|
|
1931
|
+
STEP export — the user waits having just asked for a file, and a host with an export
|
|
1932
|
+
timeout can trip it. Call this when an export becomes likely (your download dialog
|
|
1933
|
+
opening) and the wait lands somewhere harmless instead. Best-effort: resolves `true`
|
|
1934
|
+
once the kernel is up, `false` on any failure or teardown, never rejects, and is a
|
|
1935
|
+
cheap no-op once warm. It costs a speculative ~11 MB download, so fire it on a real
|
|
1936
|
+
signal of intent rather than on mount.
|
|
1928
1937
|
|
|
1929
1938
|
Pass `onDownload({ data, filename, mime })` to `mount()` to receive the exported bytes
|
|
1930
1939
|
yourself (e.g. to download from a different origin) instead of partforge's own DOM download.
|
package/package.json
CHANGED
|
@@ -18,6 +18,11 @@ export function backendForFormat(format, defaultBackend) {
|
|
|
18
18
|
export function createExportController({ send, currentView, title, defaultBackend = () => "manifold", currentParams = () => ({}) }) {
|
|
19
19
|
const pending = new Map(); // jobId -> { resolve, reject, onProgress }
|
|
20
20
|
let nextId = 1;
|
|
21
|
+
// Warm jobs are STRING-namespaced ("warm-N") for the reason mount.js spells
|
|
22
|
+
// out for tessellate-imports: this map is keyed by jobId alone and read before
|
|
23
|
+
// any type check, so a bare numeric id here could be claimed by a pending
|
|
24
|
+
// export (both counters start at 1) and settle the wrong Promise.
|
|
25
|
+
let nextWarmId = 1;
|
|
21
26
|
|
|
22
27
|
function exportParts({ parts, format, quality = "print", onProgress } = {}) {
|
|
23
28
|
const jobId = nextId++;
|
|
@@ -29,12 +34,31 @@ export function createExportController({ send, currentView, title, defaultBacken
|
|
|
29
34
|
});
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
// Pay a backend's cold boot on purpose, before anything needs it. STEP is
|
|
38
|
+
// pinned to OCCT (backendForFormat), whose ~11 MB WASM loads lazily on its
|
|
39
|
+
// first job — so for a Manifold-previewed part the STEP export IS that boot,
|
|
40
|
+
// and the user waits for it having just asked for a file. A host that knows an
|
|
41
|
+
// export is likely (its download dialog just opened) can spend that time
|
|
42
|
+
// earlier instead.
|
|
43
|
+
//
|
|
44
|
+
// Best-effort by contract: resolves true once the kernel is up, false on any
|
|
45
|
+
// failure or teardown, and NEVER rejects — a speculative warm must not become
|
|
46
|
+
// an unhandled rejection in a host that fired it and moved on.
|
|
47
|
+
function warmKernel() {
|
|
48
|
+
const jobId = `warm-${nextWarmId++}`;
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
pending.set(jobId, { resolve: () => resolve(true), reject: () => resolve(false) });
|
|
51
|
+
send({ type: "warm-kernel", jobId }, backendForFormat("step", defaultBackend));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
// Returns true iff this message belonged to a pending export (so the caller
|
|
33
56
|
// can skip legacy handling). `sink` is partforge's onDownload.
|
|
34
57
|
function handleMessage(m, sink) {
|
|
35
58
|
const entry = m && m.jobId != null ? pending.get(m.jobId) : undefined;
|
|
36
59
|
if (!entry) return false;
|
|
37
60
|
if (m.type === "progress") { entry.onProgress?.(m.phase); return true; }
|
|
61
|
+
if (m.type === "kernel-warm") { pending.delete(m.jobId); entry.resolve(); return true; }
|
|
38
62
|
if (m.type === "download") {
|
|
39
63
|
pending.delete(m.jobId);
|
|
40
64
|
triggerDownload(m.data, m.filename, m.mime, sink);
|
|
@@ -73,5 +97,5 @@ export function createExportController({ send, currentView, title, defaultBacken
|
|
|
73
97
|
pending.clear();
|
|
74
98
|
}
|
|
75
99
|
|
|
76
|
-
return { exportParts, handleMessage, dispose };
|
|
100
|
+
return { exportParts, warmKernel, handleMessage, dispose };
|
|
77
101
|
}
|
package/src/framework/jobs.js
CHANGED
|
@@ -338,6 +338,13 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
338
338
|
onProgress("writing 3MF file");
|
|
339
339
|
const data = meshTo3MF(meshes);
|
|
340
340
|
post({ type: "download", data, filename: `${fileBase}.3mf`, mime: "model/3mf", jobId: msg.jobId }, [bufferOf(data)]);
|
|
341
|
+
} else if (msg.type === "warm-kernel") {
|
|
342
|
+
// Deliberately empty. worker.js awaits kernelFor() before calling handle(),
|
|
343
|
+
// so REACHING this branch is the whole result: the backend this job was
|
|
344
|
+
// routed to now has a live kernel. It exists so a host can pay OCCT's cold
|
|
345
|
+
// ~11 MB boot at a moment of its own choosing — when the export dialog
|
|
346
|
+
// opens, say — instead of inside the STEP export the user just asked for.
|
|
347
|
+
post({ type: "kernel-warm", jobId: msg.jobId });
|
|
341
348
|
} else if (msg.type === "tessellate-imports") {
|
|
342
349
|
// OCCT-worker service job for the STEP-on-Manifold crossover: answer with
|
|
343
350
|
// print-quality triangle meshes for every STEP import, transferable.
|
package/src/framework/mount.js
CHANGED
|
@@ -61,7 +61,7 @@ const IMPORT_MESH_BROKEN_MESSAGE = "STEP import tessellation failed to satisfy t
|
|
|
61
61
|
// carries the worker's own error text. See the correlated "error" case below.
|
|
62
62
|
const importTessellateFailedMessage = (workerMessage) => `STEP import tessellation failed — ${workerMessage}`;
|
|
63
63
|
|
|
64
|
-
export function makeHandle({ ready, dispose, viewer, setParams, listExportableParts, exportParts, setHostPane, animation, getView, setView, captureView, attachTooltips, measure, annotate, projection, pickMarker }) {
|
|
64
|
+
export function makeHandle({ ready, dispose, viewer, setParams, listExportableParts, exportParts, warmExportKernel, setHostPane, animation, getView, setView, captureView, attachTooltips, measure, annotate, projection, pickMarker }) {
|
|
65
65
|
return {
|
|
66
66
|
ready, dispose, setParams,
|
|
67
67
|
// Part-declared animation playback (spec 2026-08-02): animations are
|
|
@@ -110,6 +110,13 @@ export function makeHandle({ ready, dispose, viewer, setParams, listExportablePa
|
|
|
110
110
|
onContextLost: (listener) => viewer.onContextLost(listener),
|
|
111
111
|
listExportableParts,
|
|
112
112
|
exportParts,
|
|
113
|
+
// Pay the exact kernel's cold boot ahead of an export. STEP is pinned to
|
|
114
|
+
// OCCT, whose ~11 MB WASM loads on its first job, so a Manifold-previewed
|
|
115
|
+
// part's STEP export otherwise pays that boot inside the export itself.
|
|
116
|
+
// Call this when an export becomes likely (a download dialog opening) to
|
|
117
|
+
// move the wait off the moment the user asked for a file. Best-effort:
|
|
118
|
+
// resolves true/false, never rejects, and is a cheap no-op once warm.
|
|
119
|
+
warmExportKernel: warmExportKernel ?? (() => Promise.resolve(false)),
|
|
113
120
|
// Narrow-layout pane selection, for a host that draws its own tab bar
|
|
114
121
|
// (partforge-cloud does, at the window level). Defaulted to a no-op so the
|
|
115
122
|
// handle's shape never depends on whether this mount resolved a rail.
|
|
@@ -1135,6 +1142,7 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
|
|
|
1135
1142
|
listExportableParts: () =>
|
|
1136
1143
|
exportablePartNames(part, params).map((name) => ({ name, label: partLabel(part, name) })),
|
|
1137
1144
|
exportParts: (opts) => exportCtl.exportParts(opts),
|
|
1145
|
+
warmExportKernel: () => exportCtl.warmKernel(),
|
|
1138
1146
|
animation: animCtl?.runtime ?? null,
|
|
1139
1147
|
measure: {
|
|
1140
1148
|
isEnabled: measureMode.isEnabled,
|
package/src/framework/worker.js
CHANGED
|
@@ -76,7 +76,15 @@ export function runWorker(part, opts = {}) {
|
|
|
76
76
|
return data.quality === "print" ? manifold.print : manifold.preview;
|
|
77
77
|
}
|
|
78
78
|
if (!occt) {
|
|
79
|
-
|
|
79
|
+
// Feedback during cold boot — and CORRELATED, because for a Manifold-previewed
|
|
80
|
+
// part this boot IS the STEP export: backendForFormat pins STEP to OCCT, so the
|
|
81
|
+
// export is the session's first OCCT job and pays the whole ~11 MB WASM load.
|
|
82
|
+
// export-controller claims replies by jobId, so an unstamped message here is
|
|
83
|
+
// dropped and a headless exportParts() caller shows no progress at all for the
|
|
84
|
+
// one phase that can outlast its timeout. Jobs with no jobId (the in-page export
|
|
85
|
+
// buttons) stay unstamped, so their progress still reaches mount's own busy
|
|
86
|
+
// indicator instead of an export controller that has nothing pending.
|
|
87
|
+
postMessage({ type: "progress", phase: "loading exact kernel", ...(data.jobId != null ? { jobId: data.jobId } : {}) });
|
|
80
88
|
booting = booting ?? occtKernel().then((k) => (occt = k));
|
|
81
89
|
await booting;
|
|
82
90
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -412,6 +412,17 @@ export interface PartRuntime {
|
|
|
412
412
|
* build/export failure or an empty selection.
|
|
413
413
|
*/
|
|
414
414
|
exportParts(opts: ExportPartsOptions): Promise<void>;
|
|
415
|
+
/**
|
|
416
|
+
* Pay the exact kernel's cold boot ahead of an export. STEP is pinned to
|
|
417
|
+
* OCCT, whose ~11 MB WASM loads on its first job, so a Manifold-previewed
|
|
418
|
+
* part's STEP export otherwise pays that boot inside the export itself.
|
|
419
|
+
* Call this when an export becomes likely — a download dialog opening — to
|
|
420
|
+
* move the wait off the moment the user asked for a file.
|
|
421
|
+
*
|
|
422
|
+
* Best-effort: resolves `true` once the kernel is up, `false` on any failure
|
|
423
|
+
* or teardown, and never rejects. A no-op once the kernel is warm.
|
|
424
|
+
*/
|
|
425
|
+
warmExportKernel(): Promise<boolean>;
|
|
415
426
|
/**
|
|
416
427
|
* Narrow-layout pane selection, for a host that draws its own tab bar.
|
|
417
428
|
* `null` hands selection back to partforge's built-in bar.
|