partforge 0.41.0 → 0.45.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.
Files changed (74) hide show
  1. package/README.md +31 -10
  2. package/bin/cli.js +138 -27
  3. package/docs/AUTHORING-PARTS.md +164 -17
  4. package/docs/ERROR-PATTERNS.md +6 -0
  5. package/package.json +48 -7
  6. package/skills/partforge/SKILL.md +17 -3
  7. package/src/app-embed-test.js +1 -1
  8. package/src/app-hinged-box.js +12 -0
  9. package/src/framework/animation-controls.js +254 -0
  10. package/src/framework/animation.js +271 -0
  11. package/src/framework/app.css +32 -0
  12. package/src/framework/assembly.js +1 -1
  13. package/src/framework/backend-select.js +25 -0
  14. package/src/framework/camera-tween.js +58 -0
  15. package/src/framework/capture-build.js +59 -0
  16. package/src/framework/chrome.css +16 -0
  17. package/src/framework/controls.js +13 -3
  18. package/src/framework/cutaway-gizmo-scene.js +244 -0
  19. package/src/framework/cutaway-gizmo.js +80 -243
  20. package/src/framework/default-view.js +46 -0
  21. package/src/framework/download.js +7 -2
  22. package/src/framework/export-controller.js +13 -2
  23. package/src/framework/geometry/probe.js +3 -22
  24. package/src/framework/jobs.js +30 -40
  25. package/src/framework/lint/finding.js +4 -0
  26. package/src/framework/lint/index.js +7 -3
  27. package/src/framework/lint/rules-animations.js +441 -0
  28. package/src/framework/lint/rules-place.js +76 -0
  29. package/src/framework/lint/rules-schema.js +22 -0
  30. package/src/framework/lint/rules-shape.js +12 -0
  31. package/src/framework/lint/rules-verify.js +2 -2
  32. package/src/framework/mount.js +147 -20
  33. package/src/{testing → framework/oracle}/build.js +1 -1
  34. package/src/{testing → framework/oracle}/bvh.js +1 -1
  35. package/src/{testing → framework/oracle}/measure.js +1 -1
  36. package/src/{testing → framework/oracle}/min-wall.js +1 -1
  37. package/src/{testing → framework/oracle}/verify.js +3 -3
  38. package/src/framework/param-deps.js +1 -1
  39. package/src/framework/part-model.js +48 -0
  40. package/src/framework/pick-request/client.js +11 -3
  41. package/src/framework/pick-request/endpoint.js +60 -0
  42. package/src/framework/pick-request/index.js +6 -0
  43. package/src/framework/pick-request/server.js +222 -34
  44. package/src/framework/pick-request/token-store.js +31 -0
  45. package/src/framework/pose-fast-path.js +12 -1
  46. package/src/framework/pose-probe-core.js +129 -0
  47. package/src/framework/pose-probe.js +7 -123
  48. package/src/framework/regen-loop.js +10 -3
  49. package/src/framework/safe-name.js +26 -0
  50. package/src/framework/verify-metrics.js +4 -4
  51. package/src/framework/view-state.js +25 -21
  52. package/src/framework/view-tabs.js +35 -7
  53. package/src/framework/viewer-controls.js +5 -26
  54. package/src/framework/viewer-lighting.js +8 -1
  55. package/src/framework/viewer.js +139 -20
  56. package/src/framework/worker.js +5 -1
  57. package/src/hinged-box-worker.js +3 -0
  58. package/src/index.js +1 -1
  59. package/src/parts/hinged-box.js +94 -0
  60. package/src/testing/render.js +19 -8
  61. package/src/testing.js +15 -8
  62. package/types/derive.d.ts +14 -0
  63. package/types/geometry.d.ts +117 -0
  64. package/types/index.d.ts +259 -0
  65. package/types/kernel.d.ts +409 -0
  66. package/types/lint.d.ts +85 -0
  67. package/types/part.d.ts +409 -0
  68. package/types/testing.d.ts +362 -0
  69. package/types/worker.d.ts +21 -0
  70. /package/src/{testing → framework/oracle}/assert-dsl.js +0 -0
  71. /package/src/{testing → framework/oracle}/cases.js +0 -0
  72. /package/src/{testing → framework/oracle}/dfm-profiles.js +0 -0
  73. /package/src/{testing → framework/oracle}/gaps.js +0 -0
  74. /package/src/{testing → framework/oracle}/mesh.js +0 -0
@@ -0,0 +1,85 @@
1
+ // partforge/lint — static PartDefinition validation.
2
+ //
3
+ // Zero runtime dependencies: it never imports a geometry kernel or the DOM
4
+ // viewer, so it runs unchanged in Node, a Web Worker, a sandboxed iframe, and
5
+ // Deno.
6
+
7
+ import type { Derived, PartDefinition, ResolvedParams } from "./part.js";
8
+
9
+ /**
10
+ * `error` = the part is provably broken (it cannot behave as authored, whether
11
+ * or not that surfaces as a throw). `warning` = suspicious or lossy, but the
12
+ * part behaves as authored; a warning never blocks anything. `note` = neither
13
+ * broken nor suspicious — informational context for an authoring agent (e.g.
14
+ * "this animated track rebuilds geometry"); notes never gate `measure` or
15
+ * `--strict`.
16
+ */
17
+ export type FindingSeverity = "error" | "warning" | "note";
18
+
19
+ export interface Finding {
20
+ /** The rule id, e.g. `"features-requires-sliders"`. */
21
+ rule: string;
22
+ severity: FindingSeverity;
23
+ message: string;
24
+ /** One self-contained corrective sentence. Always present. */
25
+ hint: string;
26
+ /**
27
+ * A JS accessor path rooted at the PartDefinition —
28
+ * `parameters[1].features[0]`, `defaults.bore`, `parts.spacer.views[0]`.
29
+ * `""` for findings about the definition as a whole. For navigation only.
30
+ */
31
+ path: string;
32
+ /** A stable ERROR-PATTERNS.md entry id, when one applies. */
33
+ pattern?: string;
34
+ }
35
+
36
+ export interface LintReport {
37
+ /** True when there are no `error` findings. Warnings do not affect it. */
38
+ ok: boolean;
39
+ errors: Finding[];
40
+ warnings: Finding[];
41
+ /** Informational findings. Never affect `ok`, `measure`, or `--strict`. */
42
+ notes: Finding[];
43
+ }
44
+
45
+ /**
46
+ * Lint a PartDefinition. NEVER throws — a rule that throws yields an
47
+ * `internal-rule-error` warning and the run continues.
48
+ *
49
+ * @param part - the default-exported PartDefinition (deliberately `unknown`:
50
+ * lint's whole job is to be handed something that may not be one).
51
+ * @param opts - `params` are layered over `part.defaults` for the probe pass.
52
+ */
53
+ export function lintPart(part: unknown, opts?: { params?: ResolvedParams } | null): LintReport;
54
+
55
+ /** The shared context a rule reads. */
56
+ export interface LintContext {
57
+ part: unknown;
58
+ /** `{ ...part.defaults, ...params }`, or `{}` if building it threw. */
59
+ p: ResolvedParams;
60
+ /** `resolveDerived(part, p)`, or `{}` if it threw. */
61
+ d: Derived;
62
+ /** The message from a throwing `defaults`/`params` read, else `null`. */
63
+ pError: string | null;
64
+ /** The message from a throwing `derive()`, else `null`. */
65
+ deriveError: string | null;
66
+ /** A memoized geometry-free probe run of every `build`. */
67
+ probe(): unknown;
68
+ /** A second, un-memoized probe run — for the determinism diff. */
69
+ probeAgain(): unknown;
70
+ /** `verify.expect` resolved once per lint pass. */
71
+ resolveExpectOnce(): unknown;
72
+ }
73
+
74
+ export interface LintRule {
75
+ id: string;
76
+ run(ctx: LintContext): Finding[];
77
+ }
78
+
79
+ /**
80
+ * The rule registry — one rule object per finding id, so it doubles as the
81
+ * documented rule catalog.
82
+ */
83
+ export const RULES: LintRule[];
84
+
85
+ export type { PartDefinition };
@@ -0,0 +1,409 @@
1
+ // The `PartDefinition` contract — the central type of partforge.
2
+ //
3
+ // Derived from docs/AUTHORING-PARTS.md ("The PartDefinition contract"), from the
4
+ // eight parts in src/parts/, and above all from what the framework actually
5
+ // READS: src/framework/part-model.js (views/enabled/place/defaults/derive),
6
+ // src/framework/derive.js, src/framework/controls.js (the parameter schema),
7
+ // src/framework/lint/rules-{shape,schema}.js (which fields are required vs.
8
+ // ignored), src/framework/export-select.js (`exportable`/`export.name`), and
9
+ // src/framework/oracle/verify.js + src/framework/verify-metrics.js (the `verify`
10
+ // block's metric vocabulary).
11
+
12
+ import type { BackendName, GeometryKernel, Solid } from "./kernel.js";
13
+
14
+ /**
15
+ * A value the control panel can hold. Sliders and number boxes write numbers;
16
+ * `text`/`textarea` controls write strings; toggles write `on` or `0`.
17
+ */
18
+ export type ParamValue = number | string | boolean;
19
+
20
+ /**
21
+ * The flat parameter object a part's `defaults` seeds and the control panel
22
+ * mutates in place.
23
+ */
24
+ export type Defaults = Record<string, ParamValue>;
25
+
26
+ /**
27
+ * The resolved parameters a build sees: `{ ...part.defaults, ...userParams }`.
28
+ *
29
+ * Deliberately loose. Parts index it with computed keys and do arithmetic on
30
+ * every read, and partforge does not (yet) infer a per-part params type from
31
+ * `defaults` — narrowing this to `ParamValue` would make `p.h / 2` an error in
32
+ * every part ever written. Supply the `P` type argument of `PartDefinition` to
33
+ * get a checked params object.
34
+ */
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- see the doc comment
36
+ export type ResolvedParams = Record<string, any>;
37
+
38
+ /** The derived-values object `derive` produces and every build receives as `d`. */
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- part-defined, unconstrained
40
+ export type Derived = Record<string, any>;
41
+
42
+ // --- meta -------------------------------------------------------------------
43
+
44
+ export interface PartMeta {
45
+ /** Names the part in the viewer and in export filenames. Required (lint errors without it). */
46
+ title: string;
47
+ /** Display units; partforge geometry is always millimetres. */
48
+ units?: string;
49
+ /** Scene background as `0xRRGGBB`. */
50
+ background?: number;
51
+ /** Pin a backend instead of letting the probe route the part. */
52
+ backend?: BackendName;
53
+ }
54
+
55
+ // --- the parameter schema ---------------------------------------------------
56
+
57
+ /** Which input a control renders as. Omit for a slider + number box. */
58
+ export type ControlKind = "slider" | "number" | "text" | "textarea";
59
+
60
+ /**
61
+ * One parameter control. The recognised field list mirrors
62
+ * `CONTROL_FIELDS` in src/framework/lint/rules-schema.js — anything else is
63
+ * ignored by the panel and warned about by `partforge lint`.
64
+ */
65
+ export interface ControlDef {
66
+ /** Must exist in `defaults`, or the control is silently dead. */
67
+ key: string;
68
+ label?: string;
69
+ /** Suffix shown beside the value box, e.g. `"mm"`. */
70
+ unit?: string;
71
+ min?: number;
72
+ max?: number;
73
+ step?: number;
74
+ control?: ControlKind;
75
+ /** Omit from the panel; the key still exists and still drives the geometry. */
76
+ hidden?: boolean;
77
+ /** CommonMark shown in a click-open ⓘ popover. */
78
+ description?: string;
79
+ }
80
+
81
+ /**
82
+ * A feature: a checkbox that sets `key` to `on` (or `0`) and reveals its own
83
+ * controls. `sliders` is REQUIRED — the panel reads `feat.sliders.filter(...)`
84
+ * unguarded. A bare on/off control belongs in `toggles` instead.
85
+ */
86
+ export interface FeatureDef {
87
+ key: string;
88
+ label?: string;
89
+ /** The value written when the box is checked. */
90
+ on: number;
91
+ sliders: ControlDef[];
92
+ hidden?: boolean;
93
+ description?: string;
94
+ }
95
+
96
+ /**
97
+ * A standalone on/off checkbox shown below the preset picker, outside the
98
+ * Advanced fold. Checked writes `on` (default `1`); unchecked writes `0`.
99
+ */
100
+ export interface ToggleDef {
101
+ key: string;
102
+ label?: string;
103
+ on?: number;
104
+ hidden?: boolean;
105
+ description?: string;
106
+ }
107
+
108
+ /** Fields every section kind shares. */
109
+ interface SectionBase {
110
+ id?: string;
111
+ title?: string;
112
+ description?: string;
113
+ /** Omit the whole section from the panel. */
114
+ hidden?: boolean;
115
+ }
116
+
117
+ /** A preset picker plus optional toggles and an Advanced block. */
118
+ export interface PresetSection extends SectionBase {
119
+ /** Preset name → the param overrides it applies. */
120
+ presets?: Record<string, Record<string, ParamValue>>;
121
+ toggles?: ToggleDef[];
122
+ /** Controls revealed under "Advanced". */
123
+ advanced?: ControlDef[];
124
+ /** A section with `features` is a feature section; `advanced` is ignored there. */
125
+ features?: undefined;
126
+ }
127
+
128
+ /** A feature-toggle section: each feature is a checkbox plus its own controls. */
129
+ export interface FeatureSection extends SectionBase {
130
+ features: FeatureDef[];
131
+ }
132
+
133
+ export type ParameterSection = PresetSection | FeatureSection;
134
+
135
+ // --- fonts ------------------------------------------------------------------
136
+
137
+ /**
138
+ * One entry of a part's `fonts` map: raw bytes, a URL string, or a thunk
139
+ * returning either (a Vite `() => import("./x.ttf")` resolves to
140
+ * `{ default: url }`). Resolved before the synchronous `build` runs.
141
+ */
142
+ export type FontSource =
143
+ | string
144
+ | ArrayBuffer
145
+ | ArrayBufferView
146
+ | (() => FontSourceValue | Promise<FontSourceValue>);
147
+
148
+ type FontSourceValue = string | ArrayBuffer | ArrayBufferView | { default: string };
149
+
150
+ // --- derive -----------------------------------------------------------------
151
+
152
+ /**
153
+ * A part's `derive`. Either one function computed in a single pass, or named
154
+ * GROUPS run in declaration order — each group receives the params plus the
155
+ * merged outputs of the groups before it. The grouped form is what lets the
156
+ * relevance layer attribute each derived value to just its own group's inputs.
157
+ *
158
+ * A group that reads a key no earlier group produced throws.
159
+ */
160
+ export type DeriveSpec<P = ResolvedParams, D = Derived> =
161
+ | ((p: P) => D | void)
162
+ | Record<string, (p: P, d: D) => D | void>;
163
+
164
+ // --- sub-parts --------------------------------------------------------------
165
+
166
+ export interface PlaceContext<P = ResolvedParams, D = Derived> {
167
+ /** The active view. Display placement must NOT depend on it. */
168
+ view: string;
169
+ purpose: "display" | "export";
170
+ p: P;
171
+ d: D;
172
+ }
173
+
174
+ export interface SubPartDefinition<P = ResolvedParams, D = Derived> {
175
+ /** Display name in tabs/progress; defaults to the key. */
176
+ label?: string;
177
+ /**
178
+ * The canonical solid, built at the origin. The only required function.
179
+ * `onProgress?.("phase")` surfaces per-feature progress during export.
180
+ */
181
+ build: (k: GeometryKernel, p: P, d: D, onProgress?: (phase: string) => void) => Solid;
182
+ /**
183
+ * Optional reposition (default identity), for a part whose display pose
184
+ * differs from its export pose. Any such difference must be a RIGID motion.
185
+ */
186
+ place?: (solid: Solid, ctx: PlaceContext<P, D>) => Solid;
187
+ /** Which views show this sub-part. Every name must exist in `views`. */
188
+ views: string[];
189
+ /** Gate a conditional sub-part. Coerced with `!!`. */
190
+ enabled?: (p: P) => unknown;
191
+ /** `false` = reference/preview-only: shown in the viewer, never exported. */
192
+ exportable?: boolean;
193
+ /** Viewer-only override — `color` is `0xRRGGBB`, `opacity` is 0..1. */
194
+ display?: { color?: number; opacity?: number };
195
+ /** Filename / object name on export; defaults to the key. */
196
+ export?: { name: string };
197
+ }
198
+
199
+ export interface ViewDefinition {
200
+ label: string;
201
+ }
202
+
203
+ // --- the verify block -------------------------------------------------------
204
+
205
+ /** A built-in design-for-manufacturing process profile. */
206
+ export type DfmProfileName = "fdm-pla" | "fdm-petg" | "resin";
207
+
208
+ /** An inline DFM profile, optionally extending a named one. */
209
+ export interface DfmProfile {
210
+ /** Build volume `[x, y, z]` in mm — a hard bbox-fit gate. */
211
+ bed?: [number, number, number];
212
+ /** Minimum wall in mm — a warning, never a gate. */
213
+ minWall?: number;
214
+ /** Carried for a future gap check; not enforced yet. */
215
+ clearance?: number;
216
+ /** Inherit from a named profile and override the rest. */
217
+ base?: DfmProfileName | DfmProfile;
218
+ }
219
+
220
+ /**
221
+ * One assertion in the verify DSL: a bare number/boolean means equality;
222
+ * a string is `">=n"`, `"<=n"`, `">n"`, `"<n"`, a range `"a..b"` (optional
223
+ * `mm`/`cm`/`mm3`/`cm3` suffix), or a componentwise vector `"<=[x,y,z]"` where
224
+ * `*` skips an axis.
225
+ */
226
+ export type AssertionExpr = number | string | boolean;
227
+
228
+ /** An expectation, optionally carrying a part-authored corrective `hint`. */
229
+ export type Expectation = AssertionExpr | { expr: AssertionExpr; hint?: string };
230
+
231
+ /**
232
+ * Per-sub-part expectations. The keys are exactly `SUBPART_METRICS` in
233
+ * src/framework/verify-metrics.js. `holes`/`watertight` are Manifold-only and
234
+ * SKIP on an OCCT part; `minWall` is a warning, never a gate.
235
+ */
236
+ export interface SubPartExpectations {
237
+ holes?: Expectation;
238
+ watertight?: Expectation;
239
+ volume?: Expectation;
240
+ surfaceArea?: Expectation;
241
+ triangleCount?: Expectation;
242
+ bbox?: Expectation;
243
+ centerOfMass?: Expectation;
244
+ boundsMin?: Expectation;
245
+ boundsMax?: Expectation;
246
+ minWall?: Expectation;
247
+ }
248
+
249
+ /**
250
+ * Whole-view expectations, under the reserved `_view` key. The scalar metrics
251
+ * are exactly `VIEW_METRICS`; `contacts`/`clearance` are pair-wise and handled
252
+ * separately by verify.js.
253
+ */
254
+ export interface ViewExpectations {
255
+ bbox?: Expectation;
256
+ volume?: Expectation;
257
+ overlaps?: Expectation;
258
+ centerOfMass?: Expectation;
259
+ boundsMin?: Expectation;
260
+ boundsMax?: Expectation;
261
+ /** Pairs that must touch, as `[["a", "b"], …]`. */
262
+ contacts?: Array<[string, string]>;
263
+ /** Intended free fits, keyed `"a×b"` (order-insensitive). */
264
+ clearance?: Record<string, Expectation>;
265
+ }
266
+
267
+ /** `verify.expect`: sub-part names plus the reserved `_view` key. */
268
+ export interface ExpectMap {
269
+ _view?: ViewExpectations;
270
+ [subPart: string]: SubPartExpectations | ViewExpectations | undefined;
271
+ }
272
+
273
+ export interface VerifyBlock<P = ResolvedParams, D = Derived> {
274
+ /** A named DFM profile or an inline one. */
275
+ process?: DfmProfileName | DfmProfile;
276
+ /** Which cases to check; default is `"defaults"` plus every preset name. */
277
+ cases?: string[];
278
+ /**
279
+ * Design intent, by sub-part name (plus `_view`). Declare it as a pure
280
+ * function of the case's resolved `(p, d)` when a preset legitimately changes
281
+ * an asserted fact.
282
+ */
283
+ expect?: ExpectMap | ((p: P, d: D) => ExpectMap);
284
+ }
285
+
286
+ // --- animations -------------------------------------------------------------
287
+
288
+ /** A timing curve across a step. Mirrors `EASINGS` in src/framework/animation.js. */
289
+ export type Easing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
290
+
291
+ /**
292
+ * `[t, value]` keyframes for one param. `t` is normalized WITHIN the owning
293
+ * step: strictly ascending, from exactly 0 to exactly 1, at least two entries.
294
+ * Values must sit inside the owning control's min/max — the engine applies
295
+ * them unclamped. `partforge lint` enforces all of that.
296
+ */
297
+ export type Keyframes = Array<[number, number]>;
298
+
299
+ /**
300
+ * The seven angles the viewer can frame a part from. Defined here rather than in
301
+ * the app entry so `CameraCue` can be the real union without an import cycle —
302
+ * the app entry re-exports it under its own name.
303
+ */
304
+ export type CanonicalView = "iso" | "front" | "back" | "left" | "right" | "top" | "bottom";
305
+
306
+ /**
307
+ * A camera cue angle. `partforge lint` rejects anything outside the canonical
308
+ * seven (`animation-camera-invalid`), so the type says so too. Cues fire during
309
+ * play only; scrubbing never moves the camera.
310
+ */
311
+ export type CameraCue = CanonicalView;
312
+
313
+ /** One step of a multi-step animation. Steps play in order; prev/next navigate them. */
314
+ export interface AnimationStep {
315
+ /** Shown in the transport bar. Defaults to `"Step <n>"`. */
316
+ label?: string;
317
+ /** Seconds. Step durations are relative — they set each step's share of the timeline. */
318
+ duration: number;
319
+ easing?: Easing;
320
+ /**
321
+ * Param key -> keyframes. A param tracked nowhere keeps its current value.
322
+ *
323
+ * Optional so a step can move only the camera — an establishing shot that
324
+ * holds the pose while the view swings round. `partforge lint` still requires
325
+ * that at least one step in the animation carries tracks, which is a
326
+ * whole-animation rule the type system can't express per step.
327
+ */
328
+ tracks?: Record<string, Keyframes>;
329
+ /** Swing the camera to this angle when the step begins. */
330
+ camera?: CameraCue;
331
+ }
332
+
333
+ /**
334
+ * The fields both animation forms share. Exported so a host can extend it —
335
+ * `AnimationSpec` itself is a union and cannot be `extends`-ed.
336
+ */
337
+ export interface AnimationSpecCommon {
338
+ /** Shown in the transport bar's picker. Defaults to the animation's key. */
339
+ label?: string;
340
+ /** CommonMark, shown behind the ⓘ glyph. */
341
+ description?: string;
342
+ easing?: Easing;
343
+ /** Wrap continuously. Single-step animations only. */
344
+ loop?: boolean;
345
+ /**
346
+ * One mechanism per animation: an angle (an intro cue at t=0), a
347
+ * `[[t, angle], …]` cue list, or per-step `camera` names.
348
+ */
349
+ camera?: CameraCue | Array<[number, CameraCue]>;
350
+ /**
351
+ * Start this animation automatically on first show and again on each view
352
+ * switch, until the user touches the transport. At most one animation per
353
+ * part may set this; `partforge lint` enforces it
354
+ * (`animation-autoplay-invalid`). Not armed when the browser reports
355
+ * `prefers-reduced-motion: reduce`.
356
+ */
357
+ autoplay?: boolean;
358
+ }
359
+
360
+ /**
361
+ * An animation is EITHER single-phase (`tracks` + `duration`) OR stepped
362
+ * (`steps`) — never both, never neither. `partforge lint` enforces that
363
+ * (`animation-tracks-or-steps`), and the union says the same thing, so a block
364
+ * carrying both is rejected before it ever reaches lint.
365
+ */
366
+ export type AnimationSpec =
367
+ | (AnimationSpecCommon & {
368
+ /** Seconds — the whole animation's duration in the single-phase form. */
369
+ duration: number;
370
+ /** Param key -> keyframes. */
371
+ tracks: Record<string, Keyframes>;
372
+ steps?: never;
373
+ })
374
+ | (AnimationSpecCommon & {
375
+ /** The multi-step form; each step carries its own relative `duration`. */
376
+ steps: AnimationStep[];
377
+ tracks?: never;
378
+ duration?: never;
379
+ });
380
+
381
+ // --- the part itself --------------------------------------------------------
382
+
383
+ /**
384
+ * A parametric part: plain data plus pure functions, default-exported from a
385
+ * DOM-free, side-effect-free module. `build` must be a pure function of
386
+ * `(k, p, d)` — the preview kernel memoizes geometry by content hash.
387
+ *
388
+ * @typeParam P - the resolved params shape; defaults to an open record.
389
+ * @typeParam D - the derived-values shape; defaults to an open record.
390
+ */
391
+ export interface PartDefinition<P = ResolvedParams, D = Derived> {
392
+ meta: PartMeta;
393
+ /** The control-panel schema: an array of sections. */
394
+ parameters: ParameterSection[];
395
+ /** Flat starting values — seeds `params` and every control. */
396
+ defaults: Defaults;
397
+ /** Outline fonts a part's `k.text2d()` calls need, as `{ name: source }`. */
398
+ fonts?: Record<string, FontSource>;
399
+ /** Dependent values computed once per build. */
400
+ derive?: DeriveSpec<P, D>;
401
+ /** Named sub-parts; each builds exactly one solid. */
402
+ parts: Record<string, SubPartDefinition<P, D>>;
403
+ /** The view tabs. A view is a set of sub-parts. */
404
+ views: Record<string, ViewDefinition>;
405
+ /** Self-verification, co-located with the schema. */
406
+ verify?: VerifyBlock<P, D>;
407
+ /** Named animations: keyframe data driving existing params over time. */
408
+ animations?: Record<string, AnimationSpec>;
409
+ }