partforge 0.95.0 → 0.97.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/bin/cli.js +14 -1
- package/docs/AUTHORING-PARTS.md +208 -1
- package/docs/ERROR-PATTERNS.md +24 -0
- package/docs/KERNEL-CONTRACT.md +1 -0
- package/package.json +1 -1
- package/src/app-relief.js +16 -0
- package/src/framework/app.css +30 -0
- package/src/framework/backend-select.js +7 -2
- package/src/framework/export-controller.js +25 -1
- package/src/framework/geometry/heightfield.js +129 -0
- package/src/framework/geometry/kernel.js +3 -0
- package/src/framework/geometry/manifold-backend.js +61 -0
- package/src/framework/geometry/occt-backend.js +148 -1
- package/src/framework/geometry/op-options.js +10 -0
- package/src/framework/geometry/png-decode.js +107 -0
- package/src/framework/geometry/solid-hash.js +96 -0
- package/src/framework/image-ingest.js +41 -0
- package/src/framework/image-source.js +72 -0
- package/src/framework/images.js +66 -0
- package/src/framework/jobs.js +62 -0
- package/src/framework/lint/index.js +2 -1
- package/src/framework/lint/rules-images.js +109 -0
- package/src/framework/measure/measure-mode.js +2 -1
- package/src/framework/mount.js +11 -2
- package/src/framework/oracle/verify.js +6 -1
- package/src/framework/panel/image-picker.js +152 -0
- package/src/framework/panel/render.js +1 -0
- package/src/framework/panel/widget-specs.js +2 -0
- package/src/framework/panel/widgets/image.js +164 -0
- package/src/framework/panel/widgets/index.js +9 -5
- package/src/framework/param-deps.js +7 -2
- package/src/framework/worker.js +9 -1
- package/src/index.js +1 -0
- package/src/parts/assets/relief-demo.png +0 -0
- package/src/parts/relief.js +84 -0
- package/src/relief-worker.js +3 -0
- package/src/testing/manifold.js +7 -1
- package/src/testing/occt.js +4 -1
- package/types/index.d.ts +24 -0
- package/types/kernel.d.ts +32 -0
- package/types/part.d.ts +21 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// The `type: "image"` control. Its VALUE is an image source — either a URL
|
|
2
|
+
// string (the same grammar `PartDefinition.images` already accepts) or raw PNG
|
|
3
|
+
// bytes (an ArrayBuffer/typed array): the partforge-cloud sandbox cannot fetch
|
|
4
|
+
// URLs, so it puts the bytes straight in the param. Everything downstream
|
|
5
|
+
// (presets, undo, the params hash, `when`) works with either shape, no special
|
|
6
|
+
// case — this widget is the only place on the main thread that has to look at
|
|
7
|
+
// the difference.
|
|
8
|
+
//
|
|
9
|
+
// Two renderings, mirroring widgets/font.js exactly. With a host-supplied
|
|
10
|
+
// `imageCatalog` it is a button — a thumbnail + label — opening the picker.
|
|
11
|
+
// Without one it degrades to a URL text field, so a standalone partforge app
|
|
12
|
+
// (which ships no catalog) still exposes the parameter.
|
|
13
|
+
//
|
|
14
|
+
// Main-thread only: the preview is a plain `<img>` bound to the source URL —
|
|
15
|
+
// the browser decodes the PNG natively. Do NOT import `png-decode.js` (or
|
|
16
|
+
// anything from images.js) here; that decoder belongs to the worker's build
|
|
17
|
+
// path, not the panel.
|
|
18
|
+
import { attachInfo } from "../info.js";
|
|
19
|
+
import { IMAGE_ALLOW_DEFAULT, imageSourceAllowed } from "../../image-source.js";
|
|
20
|
+
|
|
21
|
+
function el(tag, className, text) {
|
|
22
|
+
const node = document.createElement(tag);
|
|
23
|
+
if (className) node.className = className;
|
|
24
|
+
if (text != null) node.textContent = text;
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isBytes = (v) => v instanceof ArrayBuffer || ArrayBuffer.isView(v);
|
|
29
|
+
|
|
30
|
+
// A URL source → its filename, for a label with no catalog to ask. A byte
|
|
31
|
+
// value has no filename — callers check `isBytes` first and never reach this
|
|
32
|
+
// for one. Not a source to fetch, never a source to warn about: `isNoImageSource`
|
|
33
|
+
// values (unset/"") read as "No image" rather than a broken link.
|
|
34
|
+
export function imageLabel(source) {
|
|
35
|
+
if (typeof source !== "string" || !source) return "No image";
|
|
36
|
+
let path = source;
|
|
37
|
+
try { path = new URL(source).pathname; } catch { /* not a URL — use the raw string */ }
|
|
38
|
+
const file = path.split("/").filter(Boolean).pop();
|
|
39
|
+
return file || source;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Point (or unpoint) the live preview. A byte-valued param has no URL to hand
|
|
43
|
+
// the browser, so the image stays hidden rather than trying to render it or
|
|
44
|
+
// showing a broken-image glyph — same rule an empty/unset value gets. `onerror`
|
|
45
|
+
// covers the other broken-image case: a URL that 404s or that CORS refuses.
|
|
46
|
+
function paintPreview(img, source) {
|
|
47
|
+
if (typeof source === "string" && source) {
|
|
48
|
+
img.hidden = false;
|
|
49
|
+
img.src = source;
|
|
50
|
+
} else {
|
|
51
|
+
img.hidden = true;
|
|
52
|
+
img.removeAttribute("src");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function makeImage(node, params, { onChange, onCommit, info, imageCatalog } = {}) {
|
|
57
|
+
const allow = Array.isArray(node.allow) && node.allow.length ? node.allow : IMAGE_ALLOW_DEFAULT;
|
|
58
|
+
const wrap = el("div", "slider");
|
|
59
|
+
const row = el("div", "row");
|
|
60
|
+
const label = el("label", "", node.label ?? node.key);
|
|
61
|
+
attachInfo(label, node.description, info);
|
|
62
|
+
row.append(label);
|
|
63
|
+
wrap.append(row);
|
|
64
|
+
|
|
65
|
+
const preview = document.createElement("img");
|
|
66
|
+
preview.className = "image-preview";
|
|
67
|
+
preview.alt = "";
|
|
68
|
+
preview.hidden = true;
|
|
69
|
+
// A URL that fails to load (404, CORS, revoked link) must degrade to hidden,
|
|
70
|
+
// not the browser's broken-image glyph.
|
|
71
|
+
preview.addEventListener("error", () => { preview.hidden = true; });
|
|
72
|
+
wrap.append(preview);
|
|
73
|
+
|
|
74
|
+
if (!imageCatalog) {
|
|
75
|
+
// Degraded path: a URL field. Unlike `text`, it does NOT write on every
|
|
76
|
+
// keystroke — a half-typed URL is a guaranteed failed fetch, and the
|
|
77
|
+
// rebuild loop would chase every one of them.
|
|
78
|
+
const field = document.createElement("input");
|
|
79
|
+
field.type = "text";
|
|
80
|
+
field.className = "text-input";
|
|
81
|
+
const paintField = () => {
|
|
82
|
+
const v = params[node.key];
|
|
83
|
+
// Bytes cannot round-trip through a text field — `String(arrayBuffer)`
|
|
84
|
+
// is "[object ArrayBuffer]", not a value anyone typed. Show an honest
|
|
85
|
+
// placeholder instead of that, and leave the field free to type a
|
|
86
|
+
// replacement URL over it.
|
|
87
|
+
field.value = isBytes(v) ? "" : String(v ?? "");
|
|
88
|
+
field.placeholder = isBytes(v) ? "Uploaded image" : "";
|
|
89
|
+
field.classList.remove("warn");
|
|
90
|
+
paintPreview(preview, v);
|
|
91
|
+
};
|
|
92
|
+
field.addEventListener("change", () => {
|
|
93
|
+
if (!imageSourceAllowed(field.value, allow)) { field.classList.add("warn"); return; }
|
|
94
|
+
field.classList.remove("warn");
|
|
95
|
+
params[node.key] = field.value;
|
|
96
|
+
onChange?.();
|
|
97
|
+
onCommit?.();
|
|
98
|
+
});
|
|
99
|
+
paintField();
|
|
100
|
+
wrap.append(field);
|
|
101
|
+
return { el: wrap, sync: paintField };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const btn = el("button", "image-btn");
|
|
105
|
+
btn.type = "button";
|
|
106
|
+
const thumb = document.createElement("img");
|
|
107
|
+
thumb.className = "image-btn-thumb";
|
|
108
|
+
thumb.alt = "";
|
|
109
|
+
thumb.hidden = true;
|
|
110
|
+
thumb.addEventListener("error", () => { thumb.hidden = true; });
|
|
111
|
+
const iname = el("span", "iname");
|
|
112
|
+
btn.append(thumb, iname);
|
|
113
|
+
btn.insertAdjacentHTML("beforeend",
|
|
114
|
+
'<svg class="caret" width="8" height="7" viewBox="0 0 8 7" aria-hidden="true"><polygon points="0,0 8,0 4,7" fill="currentColor"/></svg>');
|
|
115
|
+
wrap.append(btn);
|
|
116
|
+
|
|
117
|
+
// The value alone cannot describe a byte-valued source, and even a URL's
|
|
118
|
+
// filename is a guess a catalog can improve on. `describe` is optional and
|
|
119
|
+
// may be async, so the label is painted twice: an immediate honest guess,
|
|
120
|
+
// then the catalog's answer when it lands (same two-pass shape as font.js's
|
|
121
|
+
// `paint`, `paintSeq` included so a stale describe() can't win a race
|
|
122
|
+
// against a newer one).
|
|
123
|
+
let paintSeq = 0;
|
|
124
|
+
const paint = () => {
|
|
125
|
+
const src = params[node.key];
|
|
126
|
+
const seq = ++paintSeq;
|
|
127
|
+
paintPreview(preview, src);
|
|
128
|
+
paintPreview(thumb, src);
|
|
129
|
+
const show = ({ label: text, width, height }) => {
|
|
130
|
+
if (seq !== paintSeq) return; // a newer paint already won
|
|
131
|
+
iname.textContent = width && height ? `${text} (${width}×${height})` : text;
|
|
132
|
+
};
|
|
133
|
+
show(isBytes(src) ? { label: "Uploaded image" } : { label: imageLabel(src) });
|
|
134
|
+
if (typeof imageCatalog.describe !== "function") return;
|
|
135
|
+
Promise.resolve()
|
|
136
|
+
.then(() => imageCatalog.describe(src))
|
|
137
|
+
.then((d) => {
|
|
138
|
+
if (!d) return;
|
|
139
|
+
show({ label: d.label ?? (isBytes(src) ? "Uploaded image" : imageLabel(src)), width: d.width, height: d.height });
|
|
140
|
+
})
|
|
141
|
+
.catch(() => { /* a failed lookup keeps the immediate label */ });
|
|
142
|
+
};
|
|
143
|
+
paint();
|
|
144
|
+
|
|
145
|
+
// The picker registers itself through setImagePicker (see below); with no
|
|
146
|
+
// picker in the bundle the button is inert rather than broken.
|
|
147
|
+
//
|
|
148
|
+
// The handle is kept because the picker is a TAKEOVER: it appends itself to
|
|
149
|
+
// the rail, outside the panel root, so tearing the panel down does not take it
|
|
150
|
+
// with it. Without dispose() the element — and the `document` keydown listener
|
|
151
|
+
// that only close() unhooks — would outlive the panel holding a stale `params`.
|
|
152
|
+
let picker = null;
|
|
153
|
+
btn.addEventListener("click", () => {
|
|
154
|
+
picker = openImagePicker?.({ node, params, allow, imageCatalog, anchor: wrap, onPicked: () => { paint(); onChange?.(); onCommit?.(); } }) ?? null;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return { el: wrap, sync: paint, dispose: () => { picker?.close(); picker = null; } };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Assigned by image-picker.js, which widgets/index.js imports for the side
|
|
161
|
+
// effect. Kept as a mutable binding rather than a static import so this file
|
|
162
|
+
// stays usable — and testable — without dragging the whole picker in.
|
|
163
|
+
export let openImagePicker = null;
|
|
164
|
+
export const setImagePicker = (fn) => { openImagePicker = fn; };
|
|
@@ -5,12 +5,15 @@ import { makeText } from "./text.js";
|
|
|
5
5
|
import { makeCheckbox } from "./checkbox.js";
|
|
6
6
|
import { makeSelect, makeRadio } from "./select.js";
|
|
7
7
|
import { makeFont } from "./font.js";
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
import { makeImage } from "./image.js";
|
|
9
|
+
// Side-effect imports: font-picker.js / image-picker.js call setFontPicker() /
|
|
10
|
+
// setImagePicker() at module scope, so each widget's button finds a picker to
|
|
11
|
+
// open. They live HERE and not in font.js/image.js because the dependency has
|
|
12
|
+
// to run picker → widget and never back — those files must stay importable
|
|
13
|
+
// (and testable) without dragging the whole DOM-heavy picker in. See the note
|
|
14
|
+
// at the bottom of font.js.
|
|
13
15
|
import "../font-picker.js";
|
|
16
|
+
import "../image-picker.js";
|
|
14
17
|
|
|
15
18
|
export const WIDGET_FACTORIES = {
|
|
16
19
|
slider: makeNumeric,
|
|
@@ -21,4 +24,5 @@ export const WIDGET_FACTORIES = {
|
|
|
21
24
|
select: makeSelect,
|
|
22
25
|
radio: makeRadio,
|
|
23
26
|
font: makeFont,
|
|
27
|
+
image: makeImage,
|
|
24
28
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// affect what's visible. Pure — no DOM, no real geometry (reuses the geometry-free
|
|
4
4
|
// probe kernel). Errs toward RELEVANT_ALL whenever it can't analyze a build.
|
|
5
5
|
import { createProbeKernel } from "./geometry/probe.js";
|
|
6
|
+
import { byteAwareReplacer } from "./geometry/solid-hash.js";
|
|
6
7
|
import { viewSubParts } from "./part-model.js";
|
|
7
8
|
import { resolveDerived } from "./derive.js";
|
|
8
9
|
|
|
@@ -125,7 +126,11 @@ export function subPartReadKeys(part, view, params) {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
// Stable string of the given param keys' current values — the cache-validity key
|
|
128
|
-
// for one sub-part. Sorted so key order never affects the result.
|
|
129
|
+
// for one sub-part. Sorted so key order never affects the result. `byteAwareReplacer`
|
|
130
|
+
// substitutes a content fingerprint for a byte-valued param (an ArrayBuffer/typed-array
|
|
131
|
+
// image source — see its own header) so JSON.stringify's default handling doesn't
|
|
132
|
+
// collapse every image to the same "{}" (cache never invalidates) or expand a typed
|
|
133
|
+
// array to one JSON number per byte (see solid-hash.js).
|
|
129
134
|
export function relevanceHash(keys, params) {
|
|
130
|
-
return JSON.stringify(keys.slice().sort().map((k) => [k, params[k]]));
|
|
135
|
+
return JSON.stringify(keys.slice().sort().map((k) => [k, params[k]]), byteAwareReplacer);
|
|
131
136
|
}
|
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/src/index.js
CHANGED
|
Binary file
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Reference part for the `images` field / `type: "image"` control / `k.heightfield`
|
|
2
|
+
// (docs/AUTHORING-PARTS.md "Image controls"): a depth map becomes a printable relief
|
|
3
|
+
// plate. `relief` is a `type: "image"` control — pick a replacement PNG from the
|
|
4
|
+
// panel, or leave it empty and the bundled `assets/relief-demo.png` (a synthetic
|
|
5
|
+
// concentric-ripple depth map) is used, so the part builds with no network access
|
|
6
|
+
// and `partforge measure`/CI never need to fetch anything. `pitch` trades sampling
|
|
7
|
+
// detail against triangle count — and therefore STEP size, since a fine pitch on a
|
|
8
|
+
// high-frequency image produces many non-coplanar faces (see heightfieldMesh's own
|
|
9
|
+
// STEP-size warning in the OCCT backend).
|
|
10
|
+
//
|
|
11
|
+
// DEMO_RELIEF_RANGE: the bundled asset's luminance only spans ~39–75% of the
|
|
12
|
+
// 16-bit sample range (measured: 25443–48830 of 65535) — the ripple formula that
|
|
13
|
+
// generated it decays toward its 50%-gray baseline away from the first ring, so
|
|
14
|
+
// most pixels sit close to mid-gray. `k.heightfield`'s default `range: [0, 1]` is
|
|
15
|
+
// an IDENTITY map (raw sample value straight to 0..1), not an auto-normalize, so
|
|
16
|
+
// left alone the demo would use well under half of `maxZ`. This stretches the
|
|
17
|
+
// default asset's own measured extent to the full 0..1 span so the shipped demo
|
|
18
|
+
// shows the full relief amplitude. Applied only when the bundled default is in
|
|
19
|
+
// use (`p.relief` empty) — a picked custom image's tonal range is unknown ahead
|
|
20
|
+
// of build time, so it gets the identity range instead.
|
|
21
|
+
export const DEMO_RELIEF_RANGE = [25443 / 65535, 48830 / 65535];
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
meta: { title: "Relief plate", units: "mm", background: 0x15181d },
|
|
25
|
+
parameters: [
|
|
26
|
+
{
|
|
27
|
+
id: "image",
|
|
28
|
+
title: "Image",
|
|
29
|
+
description: "The depth map the relief is sampled from. Bright = high, dark = low, unless inverted.",
|
|
30
|
+
controls: [
|
|
31
|
+
{ key: "relief", type: "image", label: "Depth map",
|
|
32
|
+
description: "Pick a PNG from the catalog or paste a URL. Empty falls back to the bundled sample ripple." },
|
|
33
|
+
{ key: "invert", type: "checkbox", label: "Invert",
|
|
34
|
+
description: "Swap which end of the image is raised — bright becomes low, dark becomes high." },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "plate",
|
|
39
|
+
title: "Plate",
|
|
40
|
+
description: "Footprint and relief depth of the printed plate.",
|
|
41
|
+
advanced: [
|
|
42
|
+
{ key: "w", label: "Width", unit: "mm", min: 20, max: 200, step: 1,
|
|
43
|
+
description: "Plate footprint along X." },
|
|
44
|
+
{ key: "d", label: "Depth", unit: "mm", min: 20, max: 200, step: 1,
|
|
45
|
+
description: "Plate footprint along Y." },
|
|
46
|
+
{ key: "base", label: "Base", unit: "mm", min: 0.5, max: 10, step: 0.1,
|
|
47
|
+
description: "Solid slab thickness under the relief — keep it thick enough to print flat and stay rigid." },
|
|
48
|
+
{ key: "maxZ", label: "Relief height", unit: "mm", min: 0.2, max: 10, step: 0.1,
|
|
49
|
+
description: "How far the tallest sample rises above the base." },
|
|
50
|
+
{ key: "pitch", label: "Detail", unit: "mm", min: 0.2, max: 2, step: 0.1,
|
|
51
|
+
description: "Grid spacing of the height sampling. Smaller is crisper but costs more triangles — see the file header." },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
defaults: { relief: "", invert: 0, w: 60, d: 60, base: 1.5, maxZ: 3, pitch: 0.5 },
|
|
56
|
+
// The default is the bundled asset, so the part builds offline; a picked value
|
|
57
|
+
// (a URL or catalog source from the `type: "image"` control) replaces it.
|
|
58
|
+
images: (p) => ({
|
|
59
|
+
relief: p.relief || new URL("./assets/relief-demo.png", import.meta.url),
|
|
60
|
+
}),
|
|
61
|
+
parts: {
|
|
62
|
+
plate: {
|
|
63
|
+
label: "Relief plate",
|
|
64
|
+
views: ["relief"],
|
|
65
|
+
export: { name: "relief" },
|
|
66
|
+
build: (k, p) => k.heightfield("relief", {
|
|
67
|
+
w: p.w, d: p.d, base: p.base, maxZ: p.maxZ, pitch: p.pitch, invert: p.invert,
|
|
68
|
+
...(p.relief ? {} : { range: DEMO_RELIEF_RANGE }),
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
views: { relief: { label: "Relief" } },
|
|
73
|
+
// Self-verification: a heightfield solid is watertight and hole-free by
|
|
74
|
+
// construction (grid + skirt + cap, no cuts) — this pins that invariant rather
|
|
75
|
+
// than asserting anything image-specific. bbox bounds catch a runaway parameter;
|
|
76
|
+
// fdm-pla opts into the bed-fit gate for a plate meant to actually be printed.
|
|
77
|
+
verify: {
|
|
78
|
+
process: "fdm-pla",
|
|
79
|
+
expect: {
|
|
80
|
+
plate: { watertight: true, holes: 0, bbox: "<=[200,200,20]" },
|
|
81
|
+
_view: { overlaps: 0 },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
package/src/testing/manifold.js
CHANGED
|
@@ -6,11 +6,12 @@ import { createManifoldKernel } from "../framework/geometry/manifold-backend.js"
|
|
|
6
6
|
import { resolveFonts } from "../framework/fonts.js";
|
|
7
7
|
import { normalizeOpentype, parseFont } from "../framework/geometry/opentype-interop.js";
|
|
8
8
|
import { ensureImports } from "../framework/imports.js";
|
|
9
|
+
import { ensureImages } from "../framework/images.js";
|
|
9
10
|
import { ensureVectors } from "../framework/vectors.js";
|
|
10
11
|
import { nodeAssetSources } from "./assets.js";
|
|
11
12
|
import { tessellateStepAssets } from "./step-mesh.js";
|
|
12
13
|
|
|
13
|
-
export async function bootManifoldKernel({ quality = "preview", fonts, imports, importMeshes, vectors } = {}) {
|
|
14
|
+
export async function bootManifoldKernel({ quality = "preview", fonts, imports, importMeshes, images, vectors } = {}) {
|
|
14
15
|
const wasm = await Module();
|
|
15
16
|
wasm.setup();
|
|
16
17
|
const kernel = createManifoldKernel(wasm, { quality });
|
|
@@ -25,6 +26,11 @@ export async function bootManifoldKernel({ quality = "preview", fonts, imports,
|
|
|
25
26
|
const meshes = importMeshes ?? (stepEntries.length ? await tessellateStepAssets(stepEntries) : null);
|
|
26
27
|
await ensureImports(kernel, decl, meshes);
|
|
27
28
|
}
|
|
29
|
+
// Third asset sibling: register declared images the same way as fonts/imports
|
|
30
|
+
// above, so a part using `k.heightfield` builds headlessly instead of hitting
|
|
31
|
+
// `heightfield: unknown image "…"` — file: sources need the same Node mapping
|
|
32
|
+
// (global fetch can't read them) that fonts/imports get from nodeAssetSources.
|
|
33
|
+
if (images && Object.keys(images).length) await ensureImages(kernel, nodeAssetSources(images));
|
|
28
34
|
if (vectors) await ensureVectors(kernel, nodeAssetSources(vectors));
|
|
29
35
|
return kernel;
|
|
30
36
|
}
|
package/src/testing/occt.js
CHANGED
|
@@ -8,10 +8,11 @@ import { createOcctKernel } from "../framework/geometry/occt-backend.js";
|
|
|
8
8
|
import { resolveFonts } from "../framework/fonts.js";
|
|
9
9
|
import { normalizeOpentype, parseFont } from "../framework/geometry/opentype-interop.js";
|
|
10
10
|
import { ensureImports } from "../framework/imports.js";
|
|
11
|
+
import { ensureImages } from "../framework/images.js";
|
|
11
12
|
import { ensureVectors } from "../framework/vectors.js";
|
|
12
13
|
import { nodeAssetSources } from "./assets.js";
|
|
13
14
|
|
|
14
|
-
export async function bootOcctKernel({ fonts, imports, importMeshes, vectors } = {}) {
|
|
15
|
+
export async function bootOcctKernel({ fonts, imports, importMeshes, images, vectors } = {}) {
|
|
15
16
|
const require = createRequire(import.meta.url);
|
|
16
17
|
globalThis.require = globalThis.require ?? require;
|
|
17
18
|
globalThis.__dirname = globalThis.__dirname ?? path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -23,6 +24,8 @@ export async function bootOcctKernel({ fonts, imports, importMeshes, vectors } =
|
|
|
23
24
|
if (fonts) { const opentype = normalizeOpentype(await import("opentype.js"));
|
|
24
25
|
for (const [name, buf] of await resolveFonts(nodeAssetSources(fonts))) kernel._fonts.set(name, parseFont(opentype, buf, name)); }
|
|
25
26
|
if (imports) await ensureImports(kernel, nodeAssetSources(imports), importMeshes ?? null);
|
|
27
|
+
// Third asset sibling: see bootManifoldKernel's matching comment.
|
|
28
|
+
if (images && Object.keys(images).length) await ensureImages(kernel, nodeAssetSources(images));
|
|
26
29
|
if (vectors) await ensureVectors(kernel, nodeAssetSources(vectors));
|
|
27
30
|
return kernel;
|
|
28
31
|
}
|
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.
|
|
@@ -443,3 +454,16 @@ export function viewSubParts(
|
|
|
443
454
|
view: string,
|
|
444
455
|
params: Record<string, ParamValue>,
|
|
445
456
|
): string[];
|
|
457
|
+
|
|
458
|
+
export interface ImageToPngOptions {
|
|
459
|
+
/** Long-edge cap in px; an image already under this is not upscaled. Default 1024. */
|
|
460
|
+
maxSize?: number;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Convert any image the browser can decode into a PNG `Blob`, downsampling the
|
|
465
|
+
* long edge to `maxSize` on the way. Main-thread only (uses `createImageBitmap`
|
|
466
|
+
* and a canvas) — for a host normalising uploads before storing them, since a
|
|
467
|
+
* part's `images` field decodes PNG only. Never call from a part's `build`.
|
|
468
|
+
*/
|
|
469
|
+
export function imageToPng(fileOrBlob: Blob | File, options?: ImageToPngOptions): Promise<Blob>;
|
package/types/kernel.d.ts
CHANGED
|
@@ -481,6 +481,31 @@ export interface Vector2dOptions {
|
|
|
481
481
|
/** Anything `k.hull`/`k.hullChain` accepts as one input. */
|
|
482
482
|
export type HullInput = Shape2D | Contour;
|
|
483
483
|
|
|
484
|
+
/** An inline depth-map grid, row-major, 0..65535 per sample. */
|
|
485
|
+
export interface HeightfieldGrid {
|
|
486
|
+
width: number;
|
|
487
|
+
height: number;
|
|
488
|
+
data: Uint16Array;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** k.heightfield — a depth map as a relief solid. */
|
|
492
|
+
export interface HeightfieldOptions {
|
|
493
|
+
w: number;
|
|
494
|
+
d: number;
|
|
495
|
+
/** Slab thickness under the relief (mm, > 0). Default 1. */
|
|
496
|
+
base?: number;
|
|
497
|
+
/** Relief height above `base` at a full-scale (1.0) sample. Default 1. */
|
|
498
|
+
maxZ?: number;
|
|
499
|
+
/** Sample spacing (mm, > 0); clamped to a vertex budget with a build warning. Default 0.5. */
|
|
500
|
+
pitch?: number;
|
|
501
|
+
/** Flip sampled value as `1 - v`, applied after `range`. */
|
|
502
|
+
invert?: boolean;
|
|
503
|
+
/** Remap with clamped ends: `range[0]` -> 0, `range[1]` -> 1. Default [0, 1]. */
|
|
504
|
+
range?: [number, number];
|
|
505
|
+
/** Footprint placement in XY only — the base always sits at z = 0. Default "center". */
|
|
506
|
+
origin?: "center" | "corner";
|
|
507
|
+
}
|
|
508
|
+
|
|
484
509
|
// --- the kernel -------------------------------------------------------------
|
|
485
510
|
|
|
486
511
|
/**
|
|
@@ -541,6 +566,13 @@ export interface GeometryKernel {
|
|
|
541
566
|
* side-channel (not a part author's calling surface).
|
|
542
567
|
*/
|
|
543
568
|
import(name: string): Solid;
|
|
569
|
+
/**
|
|
570
|
+
* A depth map as a relief solid. `nameOrGrid` is a name declared in the part's
|
|
571
|
+
* `images` field, or an inline grid — the name path is registered pre-build by
|
|
572
|
+
* the framework via the underscore-prefixed `_registerImage` side-channel (not
|
|
573
|
+
* a part author's calling surface).
|
|
574
|
+
*/
|
|
575
|
+
heightfield(nameOrGrid: string | HeightfieldGrid, opts: HeightfieldOptions): Solid;
|
|
544
576
|
|
|
545
577
|
// Backend-optional: the sub-part cache brackets and WASM lifetime hooks. Every
|
|
546
578
|
// framework caller reaches these through `?.`, so a third-party backend may
|
package/types/part.d.ts
CHANGED
|
@@ -257,6 +257,21 @@ export type FontSource =
|
|
|
257
257
|
|
|
258
258
|
type FontSourceValue = string | ArrayBuffer | ArrayBufferView | { default: string };
|
|
259
259
|
|
|
260
|
+
// --- images -----------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* One entry of a part's `images` map: raw bytes, a URL string, or a thunk
|
|
264
|
+
* returning either (a Vite `() => import("./x.png")` resolves to
|
|
265
|
+
* `{ default: url }`). Resolved to a decoded luminance grid before the
|
|
266
|
+
* synchronous `build` runs — the source `k.heightfield()` samples.
|
|
267
|
+
*/
|
|
268
|
+
export type ImageSource =
|
|
269
|
+
| string
|
|
270
|
+
| ArrayBuffer
|
|
271
|
+
| ArrayBufferView
|
|
272
|
+
| (() => ImageSourceValue | Promise<ImageSourceValue>);
|
|
273
|
+
|
|
274
|
+
type ImageSourceValue = string | ArrayBuffer | ArrayBufferView | { default: string };
|
|
260
275
|
// --- imports and vectors ------------------------------------------------------
|
|
261
276
|
|
|
262
277
|
/**
|
|
@@ -604,6 +619,12 @@ export interface PartDefinition<P = ResolvedParams, D = Derived> {
|
|
|
604
619
|
defaults: Defaults;
|
|
605
620
|
/** Outline fonts a part's `k.text2d()` calls need, as `{ name: source }`. */
|
|
606
621
|
fonts?: Record<string, FontSource>;
|
|
622
|
+
/**
|
|
623
|
+
* Depth-map images a part's `k.heightfield()` calls need, as
|
|
624
|
+
* `{ name: source }` — or a function of the resolved params returning that
|
|
625
|
+
* map, which is what lets a `type: "image"` control drive the source.
|
|
626
|
+
*/
|
|
627
|
+
images?: Record<string, ImageSource> | ((p: P) => Record<string, ImageSource>);
|
|
607
628
|
/** STEP/STL/3MF files a part's `k.import()` calls need, as `{ name: source }`. */
|
|
608
629
|
imports?: Record<string, ImportSource>;
|
|
609
630
|
/** Vector artwork a part's `k.vector2d()` calls place, as `{ name: source }`. */
|