partforge 0.92.0 → 0.94.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 +6 -3
- package/docs/AUTHORING-PARTS.md +211 -1
- package/docs/ERROR-PATTERNS.md +97 -0
- package/docs/KERNEL-CONTRACT.md +1 -0
- package/docs/VECTOR-FORMAT.md +746 -0
- package/package.json +9 -1
- package/src/app-emblem.js +15 -0
- package/src/emblem-worker.js +3 -0
- package/src/framework/asset-resolve.js +29 -7
- package/src/framework/geometry/arc-fit.js +146 -0
- package/src/framework/geometry/contour-offset.js +5 -0
- package/src/framework/geometry/curve-fill.js +57 -7
- package/src/framework/geometry/kernel-front.js +46 -0
- package/src/framework/geometry/kernel.js +1 -1
- package/src/framework/geometry/probe.js +1 -1
- package/src/framework/geometry/stroke-outline.js +119 -0
- package/src/framework/geometry/vector-format.js +334 -0
- package/src/framework/geometry/vector2d.js +96 -0
- package/src/framework/ingest/svg-ingest.js +212 -0
- package/src/framework/jobs.js +11 -0
- package/src/framework/lint/index.js +28 -3
- package/src/framework/lint/rules-vector.js +112 -0
- package/src/framework/vectors.js +205 -0
- package/src/framework/worker.js +46 -1
- package/src/ingest.js +8 -0
- package/src/parts/assets/emblem.svg +10 -0
- package/src/parts/assets/emblem.vector.json +110 -0
- package/src/parts/assets/plate.vector.json +27 -0
- package/src/parts/emblem.js +114 -0
- package/src/testing/manifold.js +3 -1
- package/src/testing/occt.js +3 -1
- package/types/ingest.d.ts +118 -0
- package/types/kernel.d.ts +28 -0
- package/types/part.d.ts +65 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The k.vector2d reference part — for BOTH paths the format supports: ingested
|
|
2
|
+
// artwork (`emblem`, `units: "artwork"`, sized per call site) and an authored
|
|
3
|
+
// millimetre drawing (`plate`, `units: "mm"`, placed exactly as drawn). The two
|
|
4
|
+
// vectors share one build, composed together with an ordinary boolean.
|
|
5
|
+
//
|
|
6
|
+
// The two entries also demonstrate the two SOURCE forms, deliberately:
|
|
7
|
+
//
|
|
8
|
+
// `emblem` is `new URL(..., import.meta.url)` — the form import-demo.js uses
|
|
9
|
+
// for its STL. Vite turns it into a bundled asset URL; in Node it is a file:
|
|
10
|
+
// URL src/testing/assets.js reads off disk. Right for ingested output, which is
|
|
11
|
+
// generated, large, and not meant to be read by hand.
|
|
12
|
+
//
|
|
13
|
+
// `plate` is the file's parsed CONTENTS, imported directly. Right for artwork
|
|
14
|
+
// that is hand-authored and meant to STAY hand-editable: the numbers live in a
|
|
15
|
+
// .json a reader can open, and nothing has to fetch anything to see them —
|
|
16
|
+
// which is also what lets lint read the file before the first build has run.
|
|
17
|
+
// The `with { type: "json" }` attribute is required: Node refuses a JSON import
|
|
18
|
+
// without it, and a bare `() => import("./assets/plate.vector.json")` would
|
|
19
|
+
// work under Vite and fail in the CLI.
|
|
20
|
+
//
|
|
21
|
+
// The source artwork lives beside it as emblem.svg, and the .json is regenerated
|
|
22
|
+
// with `node scripts/ingest-svg.mjs src/parts/assets/emblem.svg`. plate.vector.json
|
|
23
|
+
// is hand-authored — no ingest step, no source SVG — and is kept legible enough
|
|
24
|
+
// to serve as documentation's worked example of a multi-shape, role-composed file.
|
|
25
|
+
import plate from "./assets/plate.vector.json" with { type: "json" };
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
meta: { title: "Emblem", units: "mm", background: 0x15181d },
|
|
29
|
+
vectors: {
|
|
30
|
+
emblem: new URL("./assets/emblem.vector.json", import.meta.url),
|
|
31
|
+
plate,
|
|
32
|
+
},
|
|
33
|
+
parameters: [
|
|
34
|
+
{
|
|
35
|
+
id: "plate",
|
|
36
|
+
title: "Plate",
|
|
37
|
+
description: "The backing plate the artwork is embossed on. Its outline — including the bolt "
|
|
38
|
+
+ "holes and keyway — is drawn in plate.vector.json, not parameterized.",
|
|
39
|
+
advanced: [
|
|
40
|
+
{ key: "plate_t", label: "Thickness", unit: "mm", min: 1, max: 10, step: 0.5, description: "Plate thickness." },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "art",
|
|
45
|
+
title: "Artwork",
|
|
46
|
+
description: "The embossed vector art. `emblem.svg` carries a filled circle and a stroked bar, so both of ingest's geometry paths are exercised.",
|
|
47
|
+
advanced: [
|
|
48
|
+
{ key: "emblem_w", label: "Emblem width", unit: "mm", min: 8, max: 70, step: 1,
|
|
49
|
+
description: "Width of the artwork's **tight bounding box** in mm — not its `viewBox`. Stroke thickness scales with it." },
|
|
50
|
+
{ key: "emboss", label: "Emboss height", unit: "mm", min: 0.4, max: 4, step: 0.2,
|
|
51
|
+
description: "How far the artwork stands proud of the plate." },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
defaults: { plate_t: 3, emblem_w: 30, emboss: 1 },
|
|
56
|
+
parts: {
|
|
57
|
+
plate: {
|
|
58
|
+
label: "Plate",
|
|
59
|
+
views: ["plate"],
|
|
60
|
+
export: { name: "emblem-plate" },
|
|
61
|
+
// No shape named and no size: the file's own roles compose it (body minus
|
|
62
|
+
// holes minus keyway), and units "mm" places it exactly as drawn. A size
|
|
63
|
+
// HERE would be safe — the composed call measures the whole document at
|
|
64
|
+
// once — but it would also be pointless. What is not safe is fetching
|
|
65
|
+
// body/holes/keyway separately and sizing each call: each would scale
|
|
66
|
+
// against ITS OWN bounds, and the drawing's shared frame would be gone
|
|
67
|
+
// (ERROR-PATTERNS.md#vector-mm-shapes-misscaled). Millimetres place as
|
|
68
|
+
// authored; that is the whole point of the units mode.
|
|
69
|
+
// The keyway sits clear of the artwork at the default `emblem_w` (30) —
|
|
70
|
+
// confirmed by measurement, not eyeballed: their 2-D footprints have zero
|
|
71
|
+
// intersection. That clearance is deliberate, not incidental: a much
|
|
72
|
+
// larger `emblem_w` would grow the emboss until it overlaps the keyway's
|
|
73
|
+
// footprint again, and the union below would then cap it from above —
|
|
74
|
+
// a through-slot the drawing marks `role: "subtract"` quietly becoming a
|
|
75
|
+
// blind pocket. This is exactly the failure mode the `holes` gate below
|
|
76
|
+
// exists to catch, which is why that gate is only asserted at defaults.
|
|
77
|
+
build: (k, p) => k
|
|
78
|
+
.vector2d("plate")
|
|
79
|
+
.extrude({ h: p.plate_t })
|
|
80
|
+
.union(k.vector2d("emblem", { width: p.emblem_w }).extrude({ h: p.emboss }).translate([0, 0, p.plate_t])),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
views: { plate: { label: "Plate" } },
|
|
84
|
+
verify: {
|
|
85
|
+
expect: {
|
|
86
|
+
plate: {
|
|
87
|
+
// Tight in all three axes, by the same +1 mm convention as
|
|
88
|
+
// import-demo.js's gates: verify evaluates the `defaults` case only
|
|
89
|
+
// (this part declares no presets), so bbox is checked against one
|
|
90
|
+
// deterministic value (40 x 24 x 4) forever, not a swept range —
|
|
91
|
+
// z = plate_t + emboss = 3 + 1 = 4 at these defaults specifically,
|
|
92
|
+
// not the schema's wider plate_t/emboss envelope. Revisit this bound
|
|
93
|
+
// if a preset is ever added that sweeps plate_t or emboss.
|
|
94
|
+
bbox: "<=[41,25,5]",
|
|
95
|
+
// Measured at defaults: 3013 mm^3 (`npx partforge measure`). The bare
|
|
96
|
+
// plate (no emboss union) is 2748 mm^3 — comfortably under this bound —
|
|
97
|
+
// so a silently-vanished emboss union fails here. Complemented by the
|
|
98
|
+
// `holes` gate below for the opposite failure (a cut that stops working
|
|
99
|
+
// raises volume, not lowers it, so this bound alone can't catch that).
|
|
100
|
+
volume: ">=2900",
|
|
101
|
+
watertight: true,
|
|
102
|
+
// Three through-holes: the two bolt circles, plus the keyway triangle —
|
|
103
|
+
// all cut clean through the extruded plate and, at this part's default
|
|
104
|
+
// `emblem_w`, none of them sit under the artwork's emboss (see the
|
|
105
|
+
// build comment above for why that placement matters). Confirmed with
|
|
106
|
+
// `npx partforge measure`, and falsified by temporarily flipping
|
|
107
|
+
// "holes"/"keyway" to role "add" in plate.vector.json (which drops
|
|
108
|
+
// this to 0, proving the gate can fail).
|
|
109
|
+
holes: 3,
|
|
110
|
+
},
|
|
111
|
+
_view: { overlaps: 0 },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
package/src/testing/manifold.js
CHANGED
|
@@ -6,10 +6,11 @@ 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 { ensureVectors } from "../framework/vectors.js";
|
|
9
10
|
import { nodeAssetSources } from "./assets.js";
|
|
10
11
|
import { tessellateStepAssets } from "./step-mesh.js";
|
|
11
12
|
|
|
12
|
-
export async function bootManifoldKernel({ quality = "preview", fonts, imports, importMeshes } = {}) {
|
|
13
|
+
export async function bootManifoldKernel({ quality = "preview", fonts, imports, importMeshes, vectors } = {}) {
|
|
13
14
|
const wasm = await Module();
|
|
14
15
|
wasm.setup();
|
|
15
16
|
const kernel = createManifoldKernel(wasm, { quality });
|
|
@@ -24,5 +25,6 @@ export async function bootManifoldKernel({ quality = "preview", fonts, imports,
|
|
|
24
25
|
const meshes = importMeshes ?? (stepEntries.length ? await tessellateStepAssets(stepEntries) : null);
|
|
25
26
|
await ensureImports(kernel, decl, meshes);
|
|
26
27
|
}
|
|
28
|
+
if (vectors) await ensureVectors(kernel, nodeAssetSources(vectors));
|
|
27
29
|
return kernel;
|
|
28
30
|
}
|
package/src/testing/occt.js
CHANGED
|
@@ -8,9 +8,10 @@ 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 { ensureVectors } from "../framework/vectors.js";
|
|
11
12
|
import { nodeAssetSources } from "./assets.js";
|
|
12
13
|
|
|
13
|
-
export async function bootOcctKernel({ fonts, imports, importMeshes } = {}) {
|
|
14
|
+
export async function bootOcctKernel({ fonts, imports, importMeshes, vectors } = {}) {
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
globalThis.require = globalThis.require ?? require;
|
|
16
17
|
globalThis.__dirname = globalThis.__dirname ?? path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -22,5 +23,6 @@ export async function bootOcctKernel({ fonts, imports, importMeshes } = {}) {
|
|
|
22
23
|
if (fonts) { const opentype = normalizeOpentype(await import("opentype.js"));
|
|
23
24
|
for (const [name, buf] of await resolveFonts(nodeAssetSources(fonts))) kernel._fonts.set(name, parseFont(opentype, buf, name)); }
|
|
24
25
|
if (imports) await ensureImports(kernel, nodeAssetSources(imports), importMeshes ?? null);
|
|
26
|
+
if (vectors) await ensureVectors(kernel, nodeAssetSources(vectors));
|
|
25
27
|
return kernel;
|
|
26
28
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// partforge/ingest — SVG -> the partforge-vector JSON format (docs/VECTOR-FORMAT.md).
|
|
2
|
+
//
|
|
3
|
+
// DOM-dependent and main-thread-only: this entry is deliberately NOT reachable
|
|
4
|
+
// from the geometry worker, and is never re-exported from the main entry or
|
|
5
|
+
// from `partforge/geometry`. A host runs it once per artwork, in a browser,
|
|
6
|
+
// and stores the result — the same division of labour as `fontCatalog`.
|
|
7
|
+
// partforge does not write files.
|
|
8
|
+
//
|
|
9
|
+
// These declarations describe the FORMAT, not just what `ingestSvg` happens to
|
|
10
|
+
// emit — the same documents are hand-authored, so `bbox` and `source` are
|
|
11
|
+
// optional here even though ingest always writes both.
|
|
12
|
+
|
|
13
|
+
/** Coordinate meaning. `"mm"` places as authored; `"artwork"` requires a size at every call site. */
|
|
14
|
+
export type VectorUnits = "mm" | "artwork";
|
|
15
|
+
|
|
16
|
+
/** Whether a shape adds material to the composed result or is cut from it. `"add"` is the default. */
|
|
17
|
+
export type VectorRole = "add" | "subtract";
|
|
18
|
+
|
|
19
|
+
/** The document's tight bounding box. A cache, not an authority — placement recomputes it. */
|
|
20
|
+
export interface VectorBbox {
|
|
21
|
+
minX: number;
|
|
22
|
+
minY: number;
|
|
23
|
+
maxX: number;
|
|
24
|
+
maxY: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A parsed `partforge-vector` JSON document — see docs/VECTOR-FORMAT.md. */
|
|
28
|
+
export interface VectorDocument {
|
|
29
|
+
format: "partforge-vector";
|
|
30
|
+
version: number;
|
|
31
|
+
units: VectorUnits;
|
|
32
|
+
/** Free text; ignored on load. Ingest writes the format's own one-paragraph summary. */
|
|
33
|
+
note?: string;
|
|
34
|
+
/** Provenance only — typically the original filename. Not validated or used at load/build time. */
|
|
35
|
+
source?: string | null;
|
|
36
|
+
/** Optional: an author need not compute analytic curve extrema, but a stale value is a named error. */
|
|
37
|
+
bbox?: VectorBbox;
|
|
38
|
+
/** Name -> shape. At least one shape, and at least one of them must have role `"add"`. */
|
|
39
|
+
shapes: Record<string, VectorShape>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A named shape: either a bare region list (role `"add"`) or `{ role, regions }`.
|
|
44
|
+
* Both forms exist because `"add"` is an honest default — a painted region adds
|
|
45
|
+
* material, which is what every file written before roles existed already meant.
|
|
46
|
+
*/
|
|
47
|
+
export type VectorShape = VectorRegion[] | { role?: VectorRole; regions: VectorRegion[] };
|
|
48
|
+
|
|
49
|
+
/** One filled region: an `outer` boundary with `holes` subtracted from it. */
|
|
50
|
+
export interface VectorRegion {
|
|
51
|
+
outer: VectorContour;
|
|
52
|
+
holes?: VectorContour[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One closed contour: the explicit `"path"` form, or one of the three primitives. */
|
|
56
|
+
export type VectorContour = VectorPath | VectorCircle | VectorRect | VectorPolygon;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Segments run head-to-tail from `start`, and the contour closes IMPLICITLY from
|
|
60
|
+
* the last segment's `to` back to `start`. At least one segment, and at least two
|
|
61
|
+
* if they are all `"line"` — a single straight edge and its closure are the same
|
|
62
|
+
* line, so they bound nothing, while a single `"arc"` or `"cubic"` bounds area
|
|
63
|
+
* against the closing chord.
|
|
64
|
+
*/
|
|
65
|
+
export interface VectorPath {
|
|
66
|
+
kind: "path";
|
|
67
|
+
start: [number, number];
|
|
68
|
+
segments: VectorSegment[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Expands to two 180-degree arcs, counter-clockwise. */
|
|
72
|
+
export interface VectorCircle {
|
|
73
|
+
kind: "circle";
|
|
74
|
+
center: [number, number];
|
|
75
|
+
r: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Axis-aligned, counter-clockwise. `radius` rounds all four corners; at most half the shorter side. */
|
|
79
|
+
export interface VectorRect {
|
|
80
|
+
kind: "rect";
|
|
81
|
+
center: [number, number];
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
radius?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** At least 3 points, joined by straight edges in the author's own order. */
|
|
88
|
+
export interface VectorPolygon {
|
|
89
|
+
kind: "polygon";
|
|
90
|
+
points: Array<[number, number]>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type VectorSegment =
|
|
94
|
+
| { kind: "line"; to: [number, number] }
|
|
95
|
+
| { kind: "arc"; to: [number, number]; through: [number, number] }
|
|
96
|
+
| { kind: "cubic"; to: [number, number]; c1: [number, number]; c2: [number, number] };
|
|
97
|
+
|
|
98
|
+
export interface IngestSvgOptions {
|
|
99
|
+
/**
|
|
100
|
+
* `"outline"` (default) turns strokes into filled geometry; `"ignore"` drops
|
|
101
|
+
* stroke geometry entirely and keeps only fills. There is no equivalent
|
|
102
|
+
* option on `k.vector2d` — once ingested, there is no stroke left to ignore.
|
|
103
|
+
*/
|
|
104
|
+
strokes?: "outline" | "ignore";
|
|
105
|
+
/** Provenance only — typically the original filename. Stored verbatim as `source`; not validated or used at load/build time. */
|
|
106
|
+
source?: string | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convert an SVG document (as text) into the `partforge-vector` JSON format a
|
|
111
|
+
* part's `k.vector2d()` calls can place. DOM-required — call this in a browser,
|
|
112
|
+
* store the returned document (e.g. as `<name>.vector.json` beside the part),
|
|
113
|
+
* and reference it from the part's `vectors` field. The result is always one
|
|
114
|
+
* shape named `"artwork"` in `"artwork"` units, with `bbox` and `source` written.
|
|
115
|
+
* Throws if the SVG can't be parsed, or if it contains no painted geometry (every
|
|
116
|
+
* element is `fill="none"` with no stroke, hidden, or empty).
|
|
117
|
+
*/
|
|
118
|
+
export function ingestSvg(svgText: string, opts?: IngestSvgOptions): VectorDocument;
|
package/types/kernel.d.ts
CHANGED
|
@@ -455,6 +455,29 @@ export interface Text2dOptions {
|
|
|
455
455
|
kerning?: boolean;
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
/** Horizontal alignment of a `vector2d` placement. */
|
|
459
|
+
export type Vector2dAlign = "center" | "left" | "right";
|
|
460
|
+
/** Vertical alignment of a `vector2d` placement. */
|
|
461
|
+
export type Vector2dValign = "middle" | "top" | "bottom";
|
|
462
|
+
|
|
463
|
+
export interface Vector2dOptions {
|
|
464
|
+
/**
|
|
465
|
+
* Name of one shape in the file, returned whatever its `role`. Omit for the
|
|
466
|
+
* composed result: every `"add"` shape unioned, minus every `"subtract"` one.
|
|
467
|
+
*/
|
|
468
|
+
shape?: string;
|
|
469
|
+
/** Target width in mm. At most one of `width`/`height`/`fit`; required for `units: "artwork"`. */
|
|
470
|
+
width?: number;
|
|
471
|
+
/** Target height in mm. At most one of `width`/`height`/`fit`; required for `units: "artwork"`. */
|
|
472
|
+
height?: number;
|
|
473
|
+
/** Target size in mm for the larger extent. At most one of `width`/`height`/`fit`; required for `units: "artwork"`. */
|
|
474
|
+
fit?: number;
|
|
475
|
+
/** Defaults to `"center"` for `units: "artwork"`, and to no horizontal translate for `units: "mm"`. */
|
|
476
|
+
align?: Vector2dAlign;
|
|
477
|
+
/** Defaults to `"middle"` for `units: "artwork"`, and to no vertical translate for `units: "mm"`. */
|
|
478
|
+
valign?: Vector2dValign;
|
|
479
|
+
}
|
|
480
|
+
|
|
458
481
|
/** Anything `k.hull`/`k.hullChain` accepts as one input. */
|
|
459
482
|
export type HullInput = Shape2D | Contour;
|
|
460
483
|
|
|
@@ -501,6 +524,11 @@ export interface GeometryKernel {
|
|
|
501
524
|
shape2d(profile: ProfileInput): Shape2D;
|
|
502
525
|
/** Render outline-font text as a `Shape2D`. */
|
|
503
526
|
text2d(string: string, opts?: Text2dOptions): Shape2D;
|
|
527
|
+
/**
|
|
528
|
+
* Place a declared vector file as a `Shape2D`. `name` is a key in the part's
|
|
529
|
+
* `vectors` field (`partforge-vector` JSON, not raw `.svg`).
|
|
530
|
+
*/
|
|
531
|
+
vector2d(name: string, opts?: Vector2dOptions): Shape2D;
|
|
504
532
|
/** Convex hull of all inputs → a convex (faceted) `Shape2D`. */
|
|
505
533
|
hull(inputs: HullInput[]): Shape2D;
|
|
506
534
|
/** Swept hull over an ordered sequence (>= 2 inputs). */
|
package/types/part.d.ts
CHANGED
|
@@ -257,6 +257,57 @@ export type FontSource =
|
|
|
257
257
|
|
|
258
258
|
type FontSourceValue = string | ArrayBuffer | ArrayBufferView | { default: string };
|
|
259
259
|
|
|
260
|
+
// --- imports and vectors ------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* One entry of a part's `imports` map: the STEP/STL/3MF file a `k.import()` call
|
|
264
|
+
* names. Same source grammar and preload timing as {@link FontSource}.
|
|
265
|
+
*/
|
|
266
|
+
export type ImportSource = FontSource;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* One entry of a part's `vectors` map: a `partforge-vector` file for `k.vector2d()`
|
|
270
|
+
* to place — never a raw `.svg`, which nothing in the geometry worker can read.
|
|
271
|
+
*
|
|
272
|
+
* Beyond the bytes/URL/thunk forms every asset source accepts, a vector source may
|
|
273
|
+
* be the file's ALREADY-PARSED contents: the object a `.vector.json` yields when
|
|
274
|
+
* something has imported or fetched it. That is the form to reach for when the
|
|
275
|
+
* artwork lives in the part's own tree and is meant to stay readable and editable,
|
|
276
|
+
* rather than sitting behind an opaque asset token.
|
|
277
|
+
*
|
|
278
|
+
* The object is read, never written, and is validated on every resolve — so a
|
|
279
|
+
* malformed one fails with the same message its on-disk twin would produce.
|
|
280
|
+
*/
|
|
281
|
+
export type VectorSource =
|
|
282
|
+
| string
|
|
283
|
+
| ArrayBuffer
|
|
284
|
+
| ArrayBufferView
|
|
285
|
+
| VectorDocument
|
|
286
|
+
| (() => VectorSourceValue | Promise<VectorSourceValue>);
|
|
287
|
+
|
|
288
|
+
type VectorSourceValue =
|
|
289
|
+
| string
|
|
290
|
+
| ArrayBuffer
|
|
291
|
+
| ArrayBufferView
|
|
292
|
+
| VectorDocument
|
|
293
|
+
| { default: string | VectorDocument };
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The parsed contents of a `partforge-vector` file. `docs/VECTOR-FORMAT.md` is the
|
|
297
|
+
* normative spec; this type is deliberately shallow — it pins the envelope every
|
|
298
|
+
* reader depends on and leaves contour shapes to the runtime validator, which
|
|
299
|
+
* reports far better errors than a structural type mismatch can.
|
|
300
|
+
*/
|
|
301
|
+
export interface VectorDocument {
|
|
302
|
+
format: "partforge-vector";
|
|
303
|
+
version: number;
|
|
304
|
+
units: "mm" | "artwork";
|
|
305
|
+
shapes: Record<string, unknown>;
|
|
306
|
+
source?: unknown;
|
|
307
|
+
bbox?: unknown;
|
|
308
|
+
note?: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
260
311
|
// --- derive -----------------------------------------------------------------
|
|
261
312
|
|
|
262
313
|
/**
|
|
@@ -316,6 +367,11 @@ export interface SubPartDefinition<P = ResolvedParams, D = Derived> {
|
|
|
316
367
|
|
|
317
368
|
export interface ViewDefinition {
|
|
318
369
|
label: string;
|
|
370
|
+
/**
|
|
371
|
+
* Open this view first. With none flagged, the first key wins — see
|
|
372
|
+
* `default-view.js`, which also falls back when the flagged view is empty.
|
|
373
|
+
*/
|
|
374
|
+
default?: boolean;
|
|
319
375
|
/**
|
|
320
376
|
* Named animations belonging to this view — keyframe data driving this view's
|
|
321
377
|
* params and sub-part opacity over time. See `AnimationSpec` below; the
|
|
@@ -548,6 +604,10 @@ export interface PartDefinition<P = ResolvedParams, D = Derived> {
|
|
|
548
604
|
defaults: Defaults;
|
|
549
605
|
/** Outline fonts a part's `k.text2d()` calls need, as `{ name: source }`. */
|
|
550
606
|
fonts?: Record<string, FontSource>;
|
|
607
|
+
/** STEP/STL/3MF files a part's `k.import()` calls need, as `{ name: source }`. */
|
|
608
|
+
imports?: Record<string, ImportSource>;
|
|
609
|
+
/** Vector artwork a part's `k.vector2d()` calls place, as `{ name: source }`. */
|
|
610
|
+
vectors?: Record<string, VectorSource>;
|
|
551
611
|
/** Dependent values computed once per build. */
|
|
552
612
|
derive?: DeriveSpec<P, D>;
|
|
553
613
|
/** Named sub-parts; each builds exactly one solid. */
|
|
@@ -556,4 +616,9 @@ export interface PartDefinition<P = ResolvedParams, D = Derived> {
|
|
|
556
616
|
views: Record<string, ViewDefinition>;
|
|
557
617
|
/** Self-verification, co-located with the schema. */
|
|
558
618
|
verify?: VerifyBlock<P, D>;
|
|
619
|
+
/**
|
|
620
|
+
* Named measurements reported by `measure`/`inspect` — never rendered, never
|
|
621
|
+
* exported. Each entry returns either a solid to measure or plain JSON.
|
|
622
|
+
*/
|
|
623
|
+
probes?: Record<string, (k: GeometryKernel, p: P, d: D) => unknown>;
|
|
559
624
|
}
|