partforge 0.97.0 → 0.99.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 +115 -9
- package/docs/AUTHORING-PARTS.md +206 -13
- package/docs/ERROR-PATTERNS.md +9 -3
- package/docs/VECTOR-FORMAT.md +23 -17
- package/package.json +9 -1
- package/src/framework/font-source.js +18 -1
- package/src/framework/image-source.js +12 -8
- package/src/framework/images.js +1 -1
- package/src/framework/{image-ingest.js → ingest/image-ingest.js} +2 -2
- package/src/framework/ingest/node-dom.js +69 -0
- package/src/framework/ingest/registry.js +57 -0
- package/src/framework/ingest/sniff.js +96 -0
- package/src/framework/jobs.js +52 -6
- package/src/framework/lint/rules-vector.js +68 -3
- package/src/framework/mount.js +13 -1
- package/src/framework/panel/render.js +1 -0
- package/src/framework/panel/widget-specs.js +2 -0
- package/src/framework/panel/widgets/file-drop.js +321 -0
- package/src/framework/panel/widgets/font.js +51 -12
- package/src/framework/panel/widgets/image.js +17 -3
- package/src/framework/panel/widgets/index.js +2 -0
- package/src/framework/panel/widgets/vector.js +77 -0
- package/src/framework/vector-source.js +127 -0
- package/src/framework/vectors.js +9 -0
- package/src/index.js +0 -1
- package/src/ingest.js +1 -0
- package/src/parts/emblem.js +2 -1
- package/types/index.d.ts +66 -12
- package/types/ingest.d.ts +13 -0
package/bin/cli.js
CHANGED
|
@@ -13,8 +13,6 @@ import { fontsFor } from "../src/framework/fonts.js";
|
|
|
13
13
|
import { imagesFor } from "../src/framework/images.js";
|
|
14
14
|
import { isNoImageSource } from "../src/framework/image-source.js";
|
|
15
15
|
import { viewAnimations, evaluate, cueAt } from "../src/framework/animation.js";
|
|
16
|
-
import { bootOcctKernel } from "../src/testing/occt.js";
|
|
17
|
-
import { bootManifoldKernel } from "../src/testing/manifold.js";
|
|
18
16
|
import { measure } from "../src/framework/oracle/measure.js";
|
|
19
17
|
import { verify } from "../src/framework/oracle/verify.js";
|
|
20
18
|
import { renderViews } from "../src/testing/render.js";
|
|
@@ -25,11 +23,15 @@ import {
|
|
|
25
23
|
import { savePickToken, loadPickToken, clearPickToken, pickTokenPath } from "../src/framework/pick-request/token-store.js";
|
|
26
24
|
import { matchPattern } from "../src/testing/error-patterns.js";
|
|
27
25
|
import { lintPart } from "../src/lint.js";
|
|
28
|
-
import {
|
|
26
|
+
import { sniffMediaType } from "../src/framework/ingest/sniff.js";
|
|
27
|
+
import { ASSET_KINDS, rowFor, convertFor } from "../src/framework/ingest/registry.js";
|
|
28
|
+
import { installNodeDom } from "../src/framework/ingest/node-dom.js";
|
|
29
|
+
import * as opentypeNamespace from "opentype.js";
|
|
30
|
+
import { normalizeOpentype, parseFont } from "../src/framework/geometry/opentype-interop.js";
|
|
29
31
|
|
|
30
32
|
const die = (msg) => { console.error(msg); process.exit(1); };
|
|
31
33
|
|
|
32
|
-
const USAGE = "usage: partforge <lint|measure|render|pick-serve|pick> …";
|
|
34
|
+
const USAGE = "usage: partforge <lint|measure|render|pick-serve|pick|ingest> …";
|
|
33
35
|
|
|
34
36
|
// Crash contract (issue #27): with --json, a thrown error becomes structured
|
|
35
37
|
// stdout JSON; either way the message is matched against ERROR-PATTERNS.md and
|
|
@@ -96,6 +98,24 @@ const readSources = (partPath) => {
|
|
|
96
98
|
}
|
|
97
99
|
};
|
|
98
100
|
|
|
101
|
+
// vectors.js, testing/occt.js and testing/manifold.js are imported DYNAMICALLY
|
|
102
|
+
// here rather than statically at the top of this file — not for laziness'
|
|
103
|
+
// sake, but because all three transitively reach paper-core.js (vectors.js ->
|
|
104
|
+
// geometry/vector-format.js -> geometry/contour-ops.js -> geometry/paper-bridge.js
|
|
105
|
+
// -> "paper/dist/paper-core.js"; testing/occt.js and testing/manifold.js the
|
|
106
|
+
// same, via their *-backend.js's own contour-ops.js use). paper-core does a
|
|
107
|
+
// canvas + 2D context probe at MODULE LOAD time (svg-ingest.js's header) and
|
|
108
|
+
// caches what it finds about its environment for the lifetime of the process —
|
|
109
|
+
// load it before `ingest`'s installNodeDom() has set up a DOM and every later
|
|
110
|
+
// SVG conversion fails deep inside paper's importSVG with an opaque "Cannot
|
|
111
|
+
// read properties of undefined (reading 'body')", even after the DOM exists.
|
|
112
|
+
// A static import here runs before `commands[cmd](args)` is ever called, no
|
|
113
|
+
// matter which verb was requested, so it would poison `ingest` even though
|
|
114
|
+
// `ingest` itself never touches these three. Deferring the import to the
|
|
115
|
+
// commands that actually need them keeps every other verb's behavior
|
|
116
|
+
// unchanged and keeps paper-core.js's first load in `ingest`'s hands.
|
|
117
|
+
const importVectors = () => import("../src/framework/vectors.js");
|
|
118
|
+
|
|
99
119
|
// Pass the part's declared fonts through, mirroring the worker path (jobs.js) —
|
|
100
120
|
// otherwise a part using a named font builds in the browser but dies headlessly
|
|
101
121
|
// with `text2d: unknown font …`. A function-form `fonts` is resolved against
|
|
@@ -110,14 +130,20 @@ const readSources = (partPath) => {
|
|
|
110
130
|
// is dropped rather than handed to ensureImages, mirroring jobs.js's own filter —
|
|
111
131
|
// `k.heightfield` throws unknown-image for a name never registered; a part
|
|
112
132
|
// that wants to build with no image branches around the call itself.
|
|
113
|
-
const bootKernel = (part, params = {}) => {
|
|
133
|
+
const bootKernel = async (part, params = {}) => {
|
|
114
134
|
const p = { ...(part.defaults ?? {}), ...params };
|
|
115
135
|
const imagesDecl = part.images ? (imagesFor(part, p) ?? {}) : undefined;
|
|
116
136
|
const images = imagesDecl &&
|
|
117
137
|
Object.fromEntries(Object.entries(imagesDecl).filter(([, src]) => !isNoImageSource(src)));
|
|
118
|
-
const
|
|
138
|
+
const { vectorsFor } = await importVectors();
|
|
139
|
+
const opts = { fonts: fontsFor(part, p), imports: part.imports, images, vectors: vectorsFor(part, p) };
|
|
119
140
|
const backend = process.env.PARTFORGE_BACKEND || detectBackend(part); // env: crash()'s NEEDS_OCCT retry
|
|
120
|
-
|
|
141
|
+
if (backend === "occt") {
|
|
142
|
+
const { bootOcctKernel } = await import("../src/testing/occt.js");
|
|
143
|
+
return bootOcctKernel(opts);
|
|
144
|
+
}
|
|
145
|
+
const { bootManifoldKernel } = await import("../src/testing/manifold.js");
|
|
146
|
+
return bootManifoldKernel(opts);
|
|
121
147
|
};
|
|
122
148
|
|
|
123
149
|
const commands = {
|
|
@@ -133,7 +159,11 @@ const commands = {
|
|
|
133
159
|
const part = await loadPart(partPath, usage);
|
|
134
160
|
const params = flags.params ? JSON.parse(flags.params) : undefined;
|
|
135
161
|
const sources = readSources(partPath);
|
|
136
|
-
|
|
162
|
+
// Same `p` derivation as bootKernel — needed here only to resolve a
|
|
163
|
+
// function-form `vectors` against params before handing it to lint.
|
|
164
|
+
const p = { ...(part.defaults ?? {}), ...params };
|
|
165
|
+
const { resolveVectorDocs, vectorsFor } = await importVectors();
|
|
166
|
+
const vectorDocs = Object.fromEntries(await resolveVectorDocs(vectorsFor(part, p)));
|
|
137
167
|
const report = lintPart(part, { params, sources, vectorDocs });
|
|
138
168
|
if (!flags.json) printLint(report);
|
|
139
169
|
if (flags.out) {
|
|
@@ -163,7 +193,11 @@ const commands = {
|
|
|
163
193
|
// milliseconds with a precise message rather than after a WASM boot and a
|
|
164
194
|
// downstream error that doesn't name the cause. Warnings never gate measure.
|
|
165
195
|
if (!flags["no-lint"]) {
|
|
166
|
-
|
|
196
|
+
// Same `p` derivation as bootKernel below — measure has no --params
|
|
197
|
+
// flag, so this is just the part's own defaults.
|
|
198
|
+
const p = { ...(part.defaults ?? {}) };
|
|
199
|
+
const { resolveVectorDocs, vectorsFor } = await importVectors();
|
|
200
|
+
const vectorDocs = Object.fromEntries(await resolveVectorDocs(vectorsFor(part, p)));
|
|
167
201
|
const lint = lintPart(part, { sources: readSources(partPath), vectorDocs });
|
|
168
202
|
if (!lint.ok) {
|
|
169
203
|
if (flags.json) console.log(JSON.stringify({ ok: false, lint }, null, 2));
|
|
@@ -371,6 +405,78 @@ const commands = {
|
|
|
371
405
|
console.log(formatPickResult(out));
|
|
372
406
|
process.exit(out.status === "done" ? 0 : 1);
|
|
373
407
|
},
|
|
408
|
+
|
|
409
|
+
// Command-line surface over the same drop-target machinery the control
|
|
410
|
+
// panel uses (registry.js's ASSET_KINDS/rowFor/convertFor) — so an agent
|
|
411
|
+
// handed a raw SVG, font, or PNG can get it into a part's asset tree without
|
|
412
|
+
// a browser. --out is REQUIRED, not defaulted: this command writes a file,
|
|
413
|
+
// and for the pass-through cases (PNG, font) a computed "next to the input"
|
|
414
|
+
// default would land on the exact same path as the input itself — a
|
|
415
|
+
// surprising same-path overwrite is worse than making the destination
|
|
416
|
+
// explicit every time.
|
|
417
|
+
async ingest(args) {
|
|
418
|
+
const usage = "usage: partforge ingest <file> --out <output-file> [--strokes ignore]";
|
|
419
|
+
const { values: flags, positionals: [inPath] } = parse(args, {
|
|
420
|
+
out: { type: "string" },
|
|
421
|
+
strokes: { type: "string" },
|
|
422
|
+
}, usage);
|
|
423
|
+
try {
|
|
424
|
+
if (!inPath) die(usage);
|
|
425
|
+
if (!flags.out) die(`ingest writes a file, so it needs an explicit destination — pass --out\n${usage}`);
|
|
426
|
+
const resolvedIn = resolve(process.cwd(), inPath);
|
|
427
|
+
const bytes = readFileSync(resolvedIn);
|
|
428
|
+
const mediaType = sniffMediaType(bytes);
|
|
429
|
+
// Which slot, if any, this media type belongs to — same lookup registry.js's
|
|
430
|
+
// own kindAccepting() does internally, but that helper isn't exported (the
|
|
431
|
+
// panel never needs it: a drop target already knows its own kind).
|
|
432
|
+
const kind = mediaType ? ASSET_KINDS.find((k) => rowFor(k).accepts.includes(mediaType)) : undefined;
|
|
433
|
+
if (!kind) {
|
|
434
|
+
throw new Error(`ingest: unrecognised file "${inPath}"${mediaType ? ` (looks like ${mediaType}, but no asset slot accepts it)` : ""} — expected a PNG, JPEG, WebP, SVG, TTF, or OTF`);
|
|
435
|
+
}
|
|
436
|
+
const outPath = resolve(process.cwd(), flags.out);
|
|
437
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
438
|
+
|
|
439
|
+
if (kind === "vector") {
|
|
440
|
+
// Must install the DOM BEFORE the dynamic import below: paper-core
|
|
441
|
+
// (loaded by svg-ingest.js, resolved through convertFor's thunk)
|
|
442
|
+
// builds a canvas and asks for a 2D context at MODULE LOAD time.
|
|
443
|
+
await installNodeDom();
|
|
444
|
+
const ingestSvg = await convertFor(kind, mediaType);
|
|
445
|
+
const doc = ingestSvg(bytes.toString("utf8"), {
|
|
446
|
+
strokes: flags.strokes ?? "outline",
|
|
447
|
+
source: basename(inPath),
|
|
448
|
+
});
|
|
449
|
+
writeFileSync(outPath, `${JSON.stringify(doc, null, 2)}\n`);
|
|
450
|
+
} else if (kind === "font") {
|
|
451
|
+
// Identity + validation, no DOM: fonts have no converter (registry.js's
|
|
452
|
+
// "font" row declares `convert: null`) — used as-is, so the only thing
|
|
453
|
+
// to do here is prove opentype.js can actually read it.
|
|
454
|
+
parseFont(normalizeOpentype(opentypeNamespace), bytes, basename(inPath));
|
|
455
|
+
writeFileSync(outPath, bytes);
|
|
456
|
+
} else if (mediaType === "image/png") {
|
|
457
|
+
// Already the target format: pass through unchanged. "Validation"
|
|
458
|
+
// here is the classification above (sniffMediaType's real signature
|
|
459
|
+
// check, the same gate the drop target uses) — NOT a full structural
|
|
460
|
+
// PNG decode: decodePng (src/framework/geometry/png-decode.js) rejects
|
|
461
|
+
// anything short of a complete IHDR/IDAT/IEND file, which is stricter
|
|
462
|
+
// than what a PNG-slot asset needs to satisfy and would refuse
|
|
463
|
+
// otherwise-fine files a real decoder tolerates (trailing junk after
|
|
464
|
+
// IEND, unusual-but-legal ancillary chunk ordering, ...).
|
|
465
|
+
writeFileSync(outPath, bytes);
|
|
466
|
+
} else {
|
|
467
|
+
// JPEG/WebP/other raster: converting to PNG needs createImageBitmap and
|
|
468
|
+
// a real canvas encoder (image-ingest.js's imageToPng) — happy-dom has
|
|
469
|
+
// no canvas raster backend, so this cannot run headlessly. Do not add
|
|
470
|
+
// an image-processing dependency to work around that; point at the
|
|
471
|
+
// browser path instead.
|
|
472
|
+
throw new Error(`ingest: ${mediaType} images can't be converted headlessly — happy-dom has no canvas raster backend, so imageToPng only runs in a browser. Drop "${inPath}" onto the app's Image control instead, or convert it to PNG yourself first.`);
|
|
473
|
+
}
|
|
474
|
+
console.log(`wrote ${outPath}`);
|
|
475
|
+
process.exit(0);
|
|
476
|
+
} catch (e) {
|
|
477
|
+
crash("ingest", e, false);
|
|
478
|
+
}
|
|
479
|
+
},
|
|
374
480
|
};
|
|
375
481
|
|
|
376
482
|
function printMeasure(r) {
|
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -523,6 +523,26 @@ magic vectors. Three habits:
|
|
|
523
523
|
body.cutAll([a, b, c]) // and k.union([base, f1, f2]) for additive batches
|
|
524
524
|
```
|
|
525
525
|
|
|
526
|
+
- **Size cut tools so no two of them land on exactly the same surface.** Booleans are
|
|
527
|
+
cheap right up until two operands share a coincident face, at which point OCCT has to
|
|
528
|
+
classify a surface belonging to both and the search degenerates — seconds become
|
|
529
|
+
minutes, with no error and no warning. Manifold's mesh CSG is unaffected, so the trap
|
|
530
|
+
is invisible until a STEP export (the one format pinned to OCCT) or an OCCT-routed
|
|
531
|
+
build. The classic is a threaded cap: a bore sized straight off the thread's root
|
|
532
|
+
diameter puts the bore wall and the thread root on the same cylinder.
|
|
533
|
+
|
|
534
|
+
```js
|
|
535
|
+
// ✗ the bore wall lands exactly on the thread root
|
|
536
|
+
const boreD = threadRootD;
|
|
537
|
+
cap.cutAll([k.cylinder({ d: boreD, h }), thread]);
|
|
538
|
+
// ✓ a deliberate gap, far below a printable layer
|
|
539
|
+
const boreD = threadRootD - 2 * 0.05;
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
The same applies to a cut that stops exactly flush with a face — overshoot it instead,
|
|
543
|
+
which is why cut tools throughout this guide carry `+ 0.4` / `- 0.2` slop. See
|
|
544
|
+
[boolean-coincident-faces-hang](ERROR-PATTERNS.md#boolean-coincident-faces-hang).
|
|
545
|
+
|
|
526
546
|
The bare `rotate(deg, center, axis)` remains available as the low-level primitive for
|
|
527
547
|
anything `rotateX/Y/Z`/`rotateAbout` can't express, but prefer the vocabulary above.
|
|
528
548
|
|
|
@@ -711,8 +731,9 @@ Every control accepts `key`, `type`, `label`, `description`, `hidden`, `when` an
|
|
|
711
731
|
| `"checkbox"` | an on/off box: ticked writes `on`, cleared writes `0` | `on` (default `1`) |
|
|
712
732
|
| `"select"` | a dropdown | `options` |
|
|
713
733
|
| `"radio"` | a segmented button row | `options` |
|
|
714
|
-
| `"font"` | a typeface picker
|
|
715
|
-
| `"image"` | an image picker
|
|
734
|
+
| `"font"` | a typeface picker (or a URL field with no catalog), plus a drop target | `allow`, `preview` |
|
|
735
|
+
| `"image"` | an image picker (or a URL field with no catalog), plus a drop target | `allow` |
|
|
736
|
+
| `"vector"` | a URL field plus a drop target — no catalog exists | — |
|
|
716
737
|
|
|
717
738
|
Numeric controls always show the number box: drag the slider *or* type an exact
|
|
718
739
|
value. Typed values may be finer than `step` and clamp to `[min, max]` on commit.
|
|
@@ -1579,6 +1600,32 @@ resolve — with a URL they stay silent until the bytes arrive. And the object i
|
|
|
1579
1600
|
on every resolve, so a malformed one fails with the same message its fetched twin would;
|
|
1580
1601
|
it is read and never written, so `build` stays pure.
|
|
1581
1602
|
|
|
1603
|
+
**`vectors` may also be a function of params — and must be, for a `type: "vector"`
|
|
1604
|
+
control.** Exactly like `fonts` and `images`, the field takes either a static
|
|
1605
|
+
`{ name: source }` object (everything above) or a function called with the resolved
|
|
1606
|
+
params, so a picked or dropped source can reach the build:
|
|
1607
|
+
|
|
1608
|
+
```js
|
|
1609
|
+
parameters: [{ id: "art", title: "Artwork", controls: [{ key: "art", type: "vector" }] }],
|
|
1610
|
+
defaults: { art: "" }, // empty = no artwork yet; the build must cope
|
|
1611
|
+
vectors: (p) => (p.art ? { badge: p.art } : {}),
|
|
1612
|
+
build: (k, p) => {
|
|
1613
|
+
const plate = k.box({ w: 60, d: 40, h: p.plate_t });
|
|
1614
|
+
if (!p.art) return plate; // nothing dropped yet
|
|
1615
|
+
return plate.union(k.vector2d("badge", { width: 30 })
|
|
1616
|
+
.extrude({ h: 1 }).translate([0, 0, p.plate_t]));
|
|
1617
|
+
},
|
|
1618
|
+
```
|
|
1619
|
+
|
|
1620
|
+
A static object provably cannot read a param, so a `type: "vector"` control beside
|
|
1621
|
+
one is inert — the picker changes a param and the artwork never moves.
|
|
1622
|
+
`vector-control-not-in-vectors` (lint) catches both halves of that mistake: a static
|
|
1623
|
+
`vectors` beside a vector control, and a function-form `vectors` that never returns
|
|
1624
|
+
the picked value. An empty `p.art` declares **no** artwork for that name (the same
|
|
1625
|
+
"unset, not refused" rule `fonts`/`images` use) — the job skips it with a progress
|
|
1626
|
+
note rather than fetching `""`, so a build that guards on it, as above, still runs
|
|
1627
|
+
with nothing dropped.
|
|
1628
|
+
|
|
1582
1629
|
**Sizing is against the tight geometric bounding box, not a `viewBox`.** Icon sets pad
|
|
1583
1630
|
their `viewBox` inconsistently, so sizing relative to `viewBox` makes two icons declared at
|
|
1584
1631
|
the same nominal size look different on the plate. `width`/`height`/`fit` instead measure
|
|
@@ -1862,7 +1909,7 @@ gates, and the geometry a regression test pins can never disagree about how a
|
|
|
1862
1909
|
given file decodes — a second format would mean a second decode path, and a
|
|
1863
1910
|
second place for the three to drift apart. The escape hatch is
|
|
1864
1911
|
`imageToPng(fileOrBlob, { maxSize = 1024 }) → Promise<Blob>`, exported from
|
|
1865
|
-
`"partforge"` (main-thread only — it draws through a `<canvas>`, never import
|
|
1912
|
+
`"partforge/ingest"` (main-thread only — it draws through a `<canvas>`, never import
|
|
1866
1913
|
it from a part or a worker): convert any format the browser can decode into a
|
|
1867
1914
|
PNG before it reaches `images`, in a host's upload handler. It downsamples to
|
|
1868
1915
|
`maxSize` on the long edge on the way, since `pitch` caps useful resolution
|
|
@@ -1888,16 +1935,19 @@ OCCT time.
|
|
|
1888
1935
|
|
|
1889
1936
|
**Bytes in params — the sandbox path.** A `type: "image"` control's value may
|
|
1890
1937
|
also be raw PNG bytes (an `ArrayBuffer`/typed array) rather than a URL string —
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1938
|
+
either dropped onto the control (see "Getting files into a part", below) or
|
|
1939
|
+
placed there directly by a host that cannot fetch URLs (the partforge-cloud
|
|
1940
|
+
sandbox is the motivating case). Byte values **bypass the `allow` check
|
|
1894
1941
|
entirely**, for every `allow` list, including the default — not a hole, but
|
|
1895
1942
|
the deliberate consequence of what a byte value in `params` can mean: a URL
|
|
1896
1943
|
cannot carry megabytes, so an `ArrayBuffer` arriving there cannot have come
|
|
1897
|
-
from a pasted link or a shared URL
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1944
|
+
from a pasted link or a shared URL. That plausibility argument isn't the
|
|
1945
|
+
load-bearing one, though — the structural fact is that `asset-resolve.js`'s
|
|
1946
|
+
resolver (shared by `images`/`fonts`/`vectors`) calls `fetch` only for a
|
|
1947
|
+
`string`/`URL` source, so bytes are consumed directly and can never become a
|
|
1948
|
+
request no matter how they arrived in `params`. `allow` exists to keep a
|
|
1949
|
+
shared link from turning into an arbitrary fetch — a concern that structurally
|
|
1950
|
+
cannot apply to a byte-valued param.
|
|
1901
1951
|
|
|
1902
1952
|
**Linting:** `npx partforge lint` adds an "Image controls" group of static
|
|
1903
1953
|
checks — `image-control-not-in-images`, `heightfield-unknown-image`,
|
|
@@ -1911,6 +1961,121 @@ function-form caveat: a `verify` case or animation frame that changes the
|
|
|
1911
1961
|
image-control param still builds against the base-params source, because the
|
|
1912
1962
|
CLI boots its kernel once.
|
|
1913
1963
|
|
|
1964
|
+
## Getting files into a part
|
|
1965
|
+
|
|
1966
|
+
`"image"`, `"vector"` and `"font"` controls each carry a drop target, on top of
|
|
1967
|
+
the URL/catalog paths documented for them above — a file dropped, pasted, or
|
|
1968
|
+
picked (a native file-input dialog behind a click, for a mouse/keyboard user
|
|
1969
|
+
with no drag-and-drop) lands in the same param a URL or a picker selection
|
|
1970
|
+
would. There is no drop target for `imports` (STEP/STL/3MF) — those are
|
|
1971
|
+
reference geometry, checked in or fetched, not something a panel field takes a
|
|
1972
|
+
file for. `src/framework/panel/widgets/file-drop.js` is the shared
|
|
1973
|
+
implementation behind all three; this section documents its contract, not its
|
|
1974
|
+
code.
|
|
1975
|
+
|
|
1976
|
+
**What each control's drop target accepts.** A dropped file is classified by
|
|
1977
|
+
its actual bytes — never its extension or the browser's claimed MIME type —
|
|
1978
|
+
against a fixed sniff table (`src/framework/ingest/sniff.js`), because a
|
|
1979
|
+
file's claimed type is exactly the input a mislabelled or hostile upload would
|
|
1980
|
+
travel as:
|
|
1981
|
+
|
|
1982
|
+
| Control | Accepts | Lands as |
|
|
1983
|
+
|---|---|---|
|
|
1984
|
+
| `"image"` | PNG, JPEG, WebP | a PNG (JPEG/WebP re-encoded through a `<canvas>`; PNG passes through unchanged) |
|
|
1985
|
+
| `"vector"` | SVG | a parsed `partforge-vector` document (paper.js — the same conversion `partforge/ingest`'s `ingestSvg` performs) |
|
|
1986
|
+
| `"font"` | TTF, OTF | the file itself — nothing to convert |
|
|
1987
|
+
|
|
1988
|
+
A file that doesn't match its own control's list is refused with a message
|
|
1989
|
+
naming what it actually is; when another control's slot *would* take it (an
|
|
1990
|
+
SVG dropped on the Image control), the message names that control instead of
|
|
1991
|
+
just saying no. A file over 25 MB is refused before being read into memory to
|
|
1992
|
+
classify at all.
|
|
1993
|
+
|
|
1994
|
+
**Where the result goes — `onAssetUpload`, or straight into the param.** After
|
|
1995
|
+
conversion, the drop widget looks for `onAssetUpload` (a `mount()` option —
|
|
1996
|
+
see "Wiring a part into a runnable app", above, for its full signature):
|
|
1997
|
+
|
|
1998
|
+
- **With the hook**, it hands over the converted artifact — a PNG blob, the
|
|
1999
|
+
partforge-vector document serialized as a JSON blob, or the original font
|
|
2000
|
+
file — and writes whatever source string the hook resolves to into the
|
|
2001
|
+
param, exactly as if that string had been typed into the URL field or
|
|
2002
|
+
chosen from a catalog.
|
|
2003
|
+
- **Without it**, the artifact itself becomes the param value directly: raw
|
|
2004
|
+
bytes (an `ArrayBuffer`) for `"image"`/`"font"`, and the **parsed
|
|
2005
|
+
`partforge-vector` document object** — not its serialized bytes — for
|
|
2006
|
+
`"vector"`, because `vectors.js`'s resolver already accepts an in-tree
|
|
2007
|
+
parsed object directly (`asParsedFile`, see "Vector geometry", above);
|
|
2008
|
+
serializing it only for the resolver to re-parse would be pure waste. This
|
|
2009
|
+
is the path a host that cannot fetch URLs needs — the partforge-cloud
|
|
2010
|
+
sandbox is the motivating case — and it is a first-class destination, not a
|
|
2011
|
+
degraded fallback for a host that hasn't wired anything up: nothing about
|
|
2012
|
+
presets, undo, the params hash, or `when` cares which shape a control's
|
|
2013
|
+
value takes.
|
|
2014
|
+
|
|
2015
|
+
**The `allow` list, and what bypasses it.** All three controls take the same
|
|
2016
|
+
`allow` list already documented for `"font"` in the control-types table above
|
|
2017
|
+
(`"https"` — the default; `"gstatic"`, font-only, `https://fonts.gstatic.com`
|
|
2018
|
+
exactly; `"asset"`, a `pfc-asset://` token the host has stored for this part),
|
|
2019
|
+
gating what a **param-supplied** value may be. It never restricts a source an
|
|
2020
|
+
author writes into `images`/`fonts`/`vectors` themselves — that's code, not
|
|
2021
|
+
user input.
|
|
2022
|
+
|
|
2023
|
+
A value that lands with no `onAssetUpload` hook — bytes for image/font, the
|
|
2024
|
+
parsed document object for vector — bypasses `allow` entirely, for every allow
|
|
2025
|
+
list including the default. **This rests on a structural fact, not on "a URL
|
|
2026
|
+
can't carry megabytes."** That plausibility argument is true, but it isn't
|
|
2027
|
+
what the check actually rests on: `asset-resolve.js`'s resolver — the code
|
|
2028
|
+
every `fonts`/`images`/`vectors` declaration resolves through, no matter which
|
|
2029
|
+
control produced the value — calls `fetch` **only** for a `string`/`URL`
|
|
2030
|
+
source. Bytes and parsed objects are consumed directly and never reach that
|
|
2031
|
+
branch, so neither can become an outbound request no matter how it got into
|
|
2032
|
+
`params` — which is exactly the class of harm `allow` exists to gate, and
|
|
2033
|
+
still holds even if a host someday finds a way to put a few bytes of base64 on
|
|
2034
|
+
a share link.
|
|
2035
|
+
|
|
2036
|
+
**Read that fact in the resolver's own order, though.** The resolver unwraps a
|
|
2037
|
+
`{ default: … }` module namespace **before** it dispatches on shape, so
|
|
2038
|
+
"object ⇒ never fetched" is not true of every object: `{ default:
|
|
2039
|
+
"http://…" }` unwraps to a plain string and is fetched. The vector check
|
|
2040
|
+
therefore unwraps first and judges what the resolver will actually see — a
|
|
2041
|
+
string or `URL` gets the full `allow` treatment however it was wrapped, a
|
|
2042
|
+
thunk is refused outright (its return value cannot be known at check time),
|
|
2043
|
+
and only a value that genuinely does not unwrap to a fetchable source is
|
|
2044
|
+
exempt.
|
|
2045
|
+
|
|
2046
|
+
**`npx partforge ingest`** runs the same classify/convert step outside a
|
|
2047
|
+
browser, for an agent (or a script) handed a raw file with nowhere to drop it:
|
|
2048
|
+
|
|
2049
|
+
```bash
|
|
2050
|
+
npx partforge ingest logo.svg --out logo.vector.json
|
|
2051
|
+
npx partforge ingest scan.ttf --out src/parts/assets/scan.ttf
|
|
2052
|
+
```
|
|
2053
|
+
|
|
2054
|
+
`--out` is required, always — for a pass-through case (PNG, font) a computed
|
|
2055
|
+
"beside the input" default would land on the exact same path as the input
|
|
2056
|
+
itself, and a surprising same-path overwrite is worse than an explicit
|
|
2057
|
+
destination every time. The four cases the drop widget's classification can
|
|
2058
|
+
produce, resolved to a file instead of a param:
|
|
2059
|
+
|
|
2060
|
+
| Input | Result |
|
|
2061
|
+
|---|---|
|
|
2062
|
+
| SVG | converted to a `partforge-vector` JSON document (`--strokes outline`, the default, turns strokes into filled geometry; `--strokes ignore` drops them) |
|
|
2063
|
+
| PNG | passed through unchanged — validated by the same magic-byte sniff the drop target uses, not a full structural PNG decode |
|
|
2064
|
+
| JPEG / WebP / other raster | refused, naming the browser path instead |
|
|
2065
|
+
| TTF / OTF | parsed with opentype.js to prove it's readable, then copied through unchanged |
|
|
2066
|
+
|
|
2067
|
+
**Headless raster conversion is out of scope, not merely unimplemented.** SVG
|
|
2068
|
+
conversion runs headlessly by installing `happy-dom` — an **optional peer
|
|
2069
|
+
dependency** (`npm install happy-dom`) — as a stand-in DOM and importing the
|
|
2070
|
+
same paper.js converter the drop widget uses; paper.js never touches the
|
|
2071
|
+
raster context for its geometry work, so a no-op `getContext` stub
|
|
2072
|
+
(`src/framework/ingest/node-dom.js`) is enough to satisfy it. Converting
|
|
2073
|
+
JPEG/WebP to PNG has no equivalent escape: it needs `createImageBitmap` and a
|
|
2074
|
+
real `<canvas>` to encode through, and happy-dom implements no canvas raster
|
|
2075
|
+
backend at all — there's no stub for a missing pixel pipeline the way there is
|
|
2076
|
+
for missing DOM structure. Convert those in a browser (drop the file onto the
|
|
2077
|
+
app's Image control) or with your own tooling before ingesting.
|
|
2078
|
+
|
|
1914
2079
|
## Host jobs: extending the worker
|
|
1915
2080
|
|
|
1916
2081
|
The worker's job loop handles a closed set of message types (`generate`, the exports,
|
|
@@ -2131,6 +2296,21 @@ yourself (e.g. to download from a different origin) instead of partforge's own D
|
|
|
2131
2296
|
where `ImageAsset` is `{ id, label, url, width, height, thumbUrl }`. With no
|
|
2132
2297
|
provider a `type: "image"` control degrades to a URL field.
|
|
2133
2298
|
|
|
2299
|
+
- `onAssetUpload(blob, { kind, filename }) → Promise<string>` — the drop target
|
|
2300
|
+
shared by `"image"`/`"vector"`/`"font"` controls (see "Getting files into a
|
|
2301
|
+
part", below) calls this with the converted artifact after a drop, paste, or
|
|
2302
|
+
file-picker choice, and writes whatever it resolves to into the param.
|
|
2303
|
+
`blob` is the CONVERTED artifact — a PNG, a partforge-vector JSON blob, or the
|
|
2304
|
+
original file for a font — never the user's raw drop; `kind` is `"image"`,
|
|
2305
|
+
`"vector"`, or `"font"`. Must resolve to a non-empty source string (an
|
|
2306
|
+
`https:` URL or a host-defined `pfc-asset:` token); anything else is treated
|
|
2307
|
+
as a contract violation and reported through the control's own error line,
|
|
2308
|
+
not written into the param. Omit it and the converted bytes land straight in
|
|
2309
|
+
the param instead — the path a host that cannot fetch URLs (the
|
|
2310
|
+
partforge-cloud sandbox) needs, not a degraded fallback. A rejection is
|
|
2311
|
+
likewise reported through the control's error line, and the widget keeps the
|
|
2312
|
+
converted artifact so a retry costs a network call, not a re-decode/re-parse.
|
|
2313
|
+
|
|
2134
2314
|
**Showcase capture (the mount handle).** The handle can also render the user's *current*
|
|
2135
2315
|
framing offscreen at a resolution independent of the window size and devicePixelRatio —
|
|
2136
2316
|
for gallery/preview images, where grabbing the live canvas would be capped at the viewer
|
|
@@ -2709,11 +2889,19 @@ the named file's `units` is `"artwork"` — unlike `k.text2d`'s cap-height `size
|
|
|
2709
2889
|
artwork units carry no physical meaning, so there is no safe default to fall
|
|
2710
2890
|
back on; an `"mm"` file's coordinates already are millimetres, so a size is
|
|
2711
2891
|
genuinely optional there), `vector-unknown-shape` (a `k.vector2d(name, { shape })`
|
|
2712
|
-
call names a shape the file's `shapes` object doesn't contain)
|
|
2892
|
+
call names a shape the file's `shapes` object doesn't contain), and
|
|
2893
|
+
`vector-control-not-in-vectors` (a `type: "vector"` control whose key never reaches
|
|
2894
|
+
`vectors:` — either because `vectors` is a static object, which cannot read a param
|
|
2895
|
+
at all, or because the function form never returns the picked value; the picker then
|
|
2896
|
+
changes a param and nothing else) (all errors). `vector-unknown-name` runs only for a
|
|
2897
|
+
static `vectors` object, whose names are statically knowable; for the function form
|
|
2898
|
+
`vector-control-not-in-vectors` asks the answerable question instead, by calling the
|
|
2899
|
+
function with a sentinel — the same complementary split `heightfield-unknown-image`
|
|
2900
|
+
and `image-control-not-in-images` use.
|
|
2713
2901
|
`vector-size-missing` and `vector-unknown-shape` need `vectorDocs` (above) to
|
|
2714
2902
|
read the file's `units`/`shapes` — without it, both stay silent rather than
|
|
2715
|
-
fire on every correct millimetre file or guess at shape names. All three
|
|
2716
|
-
the argument values the probe resolves under the part's default params, the
|
|
2903
|
+
fire on every correct millimetre file or guess at shape names. All three call
|
|
2904
|
+
rules judge the argument values the probe resolves under the part's default params, the
|
|
2717
2905
|
same basis `import-unknown-name` uses; a call that only goes wrong for
|
|
2718
2906
|
non-default params still fails correctly at build time.
|
|
2719
2907
|
|
|
@@ -3182,6 +3370,11 @@ symptom first** — it maps error text → cause → fix. The invariants, one li
|
|
|
3182
3370
|
- **Keep geometry backend-agnostic** (kernel calls only); only STEP requires OCCT
|
|
3183
3371
|
([probe-routed-to-occt](ERROR-PATTERNS.md#probe-routed-to-occt),
|
|
3184
3372
|
[occt-holes-watertight-na](ERROR-PATTERNS.md#occt-holes-watertight-na)).
|
|
3373
|
+
- **Never let two cut tools share an exactly coincident face** — a bore whose radius
|
|
3374
|
+
equals a thread's root radius, a cut ending flush with a face. Give them 0.05-0.1 mm
|
|
3375
|
+
of deliberate clearance, or overshoot the cut. Mesh CSG shrugs; OCCT's boolean
|
|
3376
|
+
degenerates, so the part previews instantly and the STEP export runs for minutes
|
|
3377
|
+
([boolean-coincident-faces-hang](ERROR-PATTERNS.md#boolean-coincident-faces-hang)).
|
|
3185
3378
|
|
|
3186
3379
|
---
|
|
3187
3380
|
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -61,6 +61,12 @@ The framework itself rebuilds each sub-part fresh per job and applies `place` on
|
|
|
61
61
|
- **Cause:** OCCT fillet/chamfer cost scales with the number of selected edges, and an `inPlane` rim selector on a many-point extruded profile selects every polygon edge (hundreds for a gear), so one op call costs seconds — and re-runs on every parameter change while that path is active.
|
|
62
62
|
- **Fix:** Use `extrude`'s `bevel` option instead of `chamfer` — same geometry, stays on the fast Manifold backend. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Beveling profile rims: extrude's bevel option".
|
|
63
63
|
|
|
64
|
+
## boolean-coincident-faces-hang
|
|
65
|
+
|
|
66
|
+
- **Symptom:** A part previews instantly but a STEP export (or any OCCT-path build) of one sub-part runs for minutes and never finishes, with no error, no warning, and no progress. Cutting each tool on its own is fast; only the combination hangs. Threaded parts are the usual victims.
|
|
67
|
+
- **Cause:** Two cut tools in the same `cutAll` (or a tool and the body) share an *exactly* coincident face — most often a bore whose radius equals a thread's root radius, so the bore wall and the thread root lie on the same cylinder. OCCT's boolean has to classify a surface that is simultaneously on both operands, and the intersection search degenerates. Manifold's mesh CSG does not care, which is why the preview is fine and only the exact kernel suffers. Measured on one real part: bore alone 0.4 s, thread alone 5.4 s, both together did not finish in fifteen minutes; moving the bore 0.05 mm brought the pair to 12.6 s.
|
|
68
|
+
- **Fix:** Give the surfaces a deliberate clearance instead of letting them land on the same number. Derive one from the other with an explicit gap — `const boreD = threadRootD - 2 * boreClearance;` with `boreClearance` around 0.05-0.1 mm — rather than reusing the same expression for both. The gap is far below a printable layer, so the fit is unchanged. The same rule covers a cut that ends exactly flush with a face (overshoot it by a few tenths, as the surrounding examples do with `+ 0.4` / `- 0.2`) and two tools that abut exactly end-to-end.
|
|
69
|
+
|
|
64
70
|
## chamfer-rescue-bisection
|
|
65
71
|
|
|
66
72
|
- **Symptom:** `partforge: chamfer` warning saying the distance `over-ran the geometry — reduced to` a smaller one (or `has no valid distance`), with an attempt count and elapsed seconds, alongside slow builds.
|
|
@@ -673,7 +679,7 @@ between the Manifold preview and the OCCT STEP export.
|
|
|
673
679
|
|
|
674
680
|
- **Symptom:** `vector2d: "` followed by the declared `vectors` name and a validation complaint — a bad `format` or `version`, a contour with no `kind` or an unknown one, a malformed `"path"` contour or segment (missing `start`, too few segments, an `arc` with no `through`, a `cubic` missing `c1`/`c2`, a non-numeric coordinate), a primitive with a bad `center`/`r`/`width`/`height`, a shape that is neither a region array nor a `{ role, regions }` object, an unknown `role`, a `bbox` that disagrees with the geometry, or (a different message, same `vector2d: "<name>"` lead) `vector2d: "<name>" is not valid JSON — <parse error>` — thrown while resolving a part's `vectors`, before `build` even runs.
|
|
675
681
|
- **Cause:** The stored document isn't a well-formed `partforge-vector` file. The single most common case for the "is not valid JSON" variant: `vectors` points at the raw `.svg` file instead of an ingested `.vector.json` — an SVG document is not JSON at all, so it fails to parse before validation ever gets a chance to name a more specific problem.
|
|
676
|
-
- **Fix:** If the message says "is not valid JSON," check the source points at the ingested `<name>.vector.json`, not the original `.svg` — re-ingest with `partforge/ingest` (or `
|
|
682
|
+
- **Fix:** If the message says "is not valid JSON," check the source points at the ingested `<name>.vector.json`, not the original `.svg` — re-ingest with `partforge/ingest` (or `npx partforge ingest <file.svg> --out <file.vector.json>`) if you don't have it yet. Otherwise the message names the shape, the 1-indexed region, the role (`outer` / `hole n`), and where applicable the 1-indexed segment, so the fix is a single edit. Several specific cases have their own entries below (vector-units-missing, vector-stale-regions-array, vector-rect-radius-too-large). See [docs/VECTOR-FORMAT.md](VECTOR-FORMAT.md) for the full schema and what each field means.
|
|
677
683
|
|
|
678
684
|
## vector-units-missing
|
|
679
685
|
|
|
@@ -769,9 +775,9 @@ between the Manifold preview and the OCCT STEP export.
|
|
|
769
775
|
|
|
770
776
|
## images-only-png-supported
|
|
771
777
|
|
|
772
|
-
- **Symptom:** `images: only PNG is supported — convert with imageToPng() from "partforge" before storing, or have the host normalize on upload` thrown while resolving a part's `images`.
|
|
778
|
+
- **Symptom:** `images: only PNG is supported — convert with imageToPng() from "partforge/ingest" before storing, or have the host normalize on upload` thrown while resolving a part's `images`.
|
|
773
779
|
- **Cause:** The image resolver checks the first four bytes against the PNG magic number before decoding; a JPEG, WEBP, or any other format fails that check immediately; a heightfield source is a depth map and needs a single well-defined decode path, so no other format is attempted.
|
|
774
|
-
- **Fix:** Convert the source to PNG before it reaches `images` — call `imageToPng()` (exported from `"partforge"`, browser-only: it draws through a `<canvas>`) in the host's upload/panel handler, or pre-convert with any image tool. Bytes that are already PNG (the 4-byte signature `89 50 4E 47`) skip this check entirely.
|
|
780
|
+
- **Fix:** Convert the source to PNG before it reaches `images` — call `imageToPng()` (exported from `"partforge/ingest"`, browser-only: it draws through a `<canvas>`) in the host's upload/panel handler, or pre-convert with any image tool. Bytes that are already PNG (the 4-byte signature `89 50 4E 47`) skip this check entirely.
|
|
775
781
|
|
|
776
782
|
## png-interlaced-unsupported
|
|
777
783
|
|
package/docs/VECTOR-FORMAT.md
CHANGED
|
@@ -10,8 +10,10 @@ There are two ways a file gets here, and this document leads with the first:
|
|
|
10
10
|
a triangular keyway. Coordinates are millimetres, and they place exactly where
|
|
11
11
|
you drew them. This is the path an agent should reach for when the geometry is
|
|
12
12
|
*drawn* rather than computed.
|
|
13
|
-
- **Ingested.** Convert an existing `.svg` once
|
|
14
|
-
`partforge/ingest`,
|
|
13
|
+
- **Ingested.** Convert an existing `.svg` once — in a browser with
|
|
14
|
+
`partforge/ingest`, or headlessly with `npx partforge ingest <file.svg> --out
|
|
15
|
+
<file.vector.json>` (needs `happy-dom`, an optional peer dependency: `npm
|
|
16
|
+
install happy-dom`) — and check the resulting JSON in beside the part. The
|
|
15
17
|
artwork keeps its own unitless coordinates and gets sized at every call site.
|
|
16
18
|
|
|
17
19
|
Both produce the same format, load through the same validator, and behave
|
|
@@ -19,14 +21,15 @@ identically downstream.
|
|
|
19
21
|
|
|
20
22
|
**Why this document is normative and not merely helpful.** Ingest needs a real
|
|
21
23
|
DOM (it resolves `<use>`, `<defs>`, CSS, and bakes ancestor transforms, all of
|
|
22
|
-
which require one)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
which require one) — `partforge measure|render|lint` read a part's
|
|
25
|
+
already-stored JSON, but neither of ingest's two entry points *creates* one
|
|
26
|
+
without a DOM somewhere: a real browser for `partforge/ingest`, or a headless
|
|
27
|
+
one (`happy-dom`) for the CLI. That trade was accepted only because this file
|
|
28
|
+
is complete enough that someone — or some agent — with neither a browser nor
|
|
29
|
+
happy-dom installed, and no access to partforge's source, can still write a
|
|
30
|
+
compliant converter from it alone. Everything below is written to hold that
|
|
31
|
+
property. The `ingest` verb in `bin/cli.js` is the reference implementation,
|
|
32
|
+
described in §6.
|
|
30
33
|
|
|
31
34
|
## 1. A worked authored example
|
|
32
35
|
|
|
@@ -494,8 +497,9 @@ Shapes are named, regions and segments are 1-indexed, and the role (`outer` /
|
|
|
494
497
|
One filled circle, and one **stroked, open** polyline — deliberately, so this one
|
|
495
498
|
file exercises both of ingest's geometry paths (a fill, and a stroke that has to
|
|
496
499
|
be outlined into a filled shape; see §6). Ingesting it
|
|
497
|
-
(`
|
|
498
|
-
|
|
500
|
+
(`npx partforge ingest src/parts/assets/emblem.svg --out
|
|
501
|
+
src/parts/assets/emblem.vector.json`) produces that file, checked in beside it.
|
|
502
|
+
Here it is with the
|
|
499
503
|
`note` field elided for brevity and the coordinate arrays put on one line —
|
|
500
504
|
nothing else is changed:
|
|
501
505
|
|
|
@@ -662,12 +666,14 @@ followed by the one already-written reference to check your output against.
|
|
|
662
666
|
file stays diffable and so a stored `bbox` matches a later recomputation from
|
|
663
667
|
the *rounded* coordinates rather than drifting past the tolerance.
|
|
664
668
|
|
|
665
|
-
`
|
|
666
|
-
|
|
667
|
-
`ingestSvg()` (paper.js's
|
|
669
|
+
The `ingest` verb in `bin/cli.js` (`npx partforge ingest <file.svg> --out
|
|
670
|
+
<file.vector.json>`) is the worked reference implementation of exactly this
|
|
671
|
+
pipeline — it runs `partforge/ingest`'s real `ingestSvg()` (paper.js's
|
|
672
|
+
`importSVG` for steps 1–2 and 4–5, this repo's own
|
|
668
673
|
`contour-offset.js`/`stroke-outline.js` for step 3, and its own `arc-fit.js` for
|
|
669
|
-
step 7) inside a headless DOM (`happy-dom`,
|
|
670
|
-
|
|
674
|
+
step 7) inside a headless DOM (`happy-dom`, an optional peer dependency —
|
|
675
|
+
`src/framework/ingest/node-dom.js` installs it), specifically so that
|
|
676
|
+
repository's own fixtures — including the worked example in §4 — are
|
|
671
677
|
reproducible instead of being hand-maintained blobs, and so there is a second
|
|
672
678
|
thing (besides this document) to check a from-scratch converter's output
|
|
673
679
|
against: ingest the same SVG both ways and diff the JSON.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "partforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.0",
|
|
4
4
|
"description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -99,6 +99,14 @@
|
|
|
99
99
|
"check": "node scripts/check-app.mjs",
|
|
100
100
|
"offset-rates": "node scripts/offset-rates.mjs"
|
|
101
101
|
},
|
|
102
|
+
"peerDependencies": {
|
|
103
|
+
"happy-dom": ">=20"
|
|
104
|
+
},
|
|
105
|
+
"peerDependenciesMeta": {
|
|
106
|
+
"happy-dom": {
|
|
107
|
+
"optional": true
|
|
108
|
+
}
|
|
109
|
+
},
|
|
102
110
|
"dependencies": {
|
|
103
111
|
"dompurify": "^3.4.11",
|
|
104
112
|
"fflate": "^0.8.3",
|