partforge 0.96.0 → 0.98.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 (52) hide show
  1. package/bin/cli.js +128 -9
  2. package/docs/AUTHORING-PARTS.md +371 -5
  3. package/docs/ERROR-PATTERNS.md +25 -1
  4. package/docs/KERNEL-CONTRACT.md +1 -0
  5. package/docs/VECTOR-FORMAT.md +23 -17
  6. package/package.json +9 -1
  7. package/src/app-relief.js +16 -0
  8. package/src/framework/app.css +30 -0
  9. package/src/framework/backend-select.js +7 -2
  10. package/src/framework/font-source.js +18 -1
  11. package/src/framework/geometry/heightfield.js +129 -0
  12. package/src/framework/geometry/kernel.js +3 -0
  13. package/src/framework/geometry/manifold-backend.js +61 -0
  14. package/src/framework/geometry/occt-backend.js +148 -1
  15. package/src/framework/geometry/op-options.js +10 -0
  16. package/src/framework/geometry/png-decode.js +107 -0
  17. package/src/framework/geometry/solid-hash.js +96 -0
  18. package/src/framework/image-source.js +76 -0
  19. package/src/framework/images.js +66 -0
  20. package/src/framework/ingest/image-ingest.js +41 -0
  21. package/src/framework/ingest/node-dom.js +69 -0
  22. package/src/framework/ingest/registry.js +57 -0
  23. package/src/framework/ingest/sniff.js +96 -0
  24. package/src/framework/jobs.js +107 -6
  25. package/src/framework/lint/index.js +2 -1
  26. package/src/framework/lint/rules-images.js +109 -0
  27. package/src/framework/lint/rules-vector.js +68 -3
  28. package/src/framework/measure/measure-mode.js +2 -1
  29. package/src/framework/mount.js +14 -1
  30. package/src/framework/oracle/verify.js +6 -1
  31. package/src/framework/panel/image-picker.js +152 -0
  32. package/src/framework/panel/render.js +2 -0
  33. package/src/framework/panel/widget-specs.js +4 -0
  34. package/src/framework/panel/widgets/file-drop.js +321 -0
  35. package/src/framework/panel/widgets/font.js +51 -12
  36. package/src/framework/panel/widgets/image.js +178 -0
  37. package/src/framework/panel/widgets/index.js +11 -5
  38. package/src/framework/panel/widgets/vector.js +77 -0
  39. package/src/framework/param-deps.js +7 -2
  40. package/src/framework/vector-source.js +127 -0
  41. package/src/framework/vectors.js +9 -0
  42. package/src/ingest.js +1 -0
  43. package/src/parts/assets/relief-demo.png +0 -0
  44. package/src/parts/emblem.js +2 -1
  45. package/src/parts/relief.js +84 -0
  46. package/src/relief-worker.js +3 -0
  47. package/src/testing/manifold.js +7 -1
  48. package/src/testing/occt.js +4 -1
  49. package/types/index.d.ts +67 -0
  50. package/types/ingest.d.ts +13 -0
  51. package/types/kernel.d.ts +32 -0
  52. package/types/part.d.ts +21 -0
package/types/index.d.ts CHANGED
@@ -101,6 +101,50 @@ export interface MountElements {
101
101
  };
102
102
  }
103
103
 
104
+ /** Which control family a dropped file belongs to. */
105
+ export type AssetKind = "image" | "vector" | "font";
106
+
107
+ /** One selectable face in a `FontCatalog` family. */
108
+ export interface FontVariant {
109
+ variant: string;
110
+ label: string;
111
+ /** What the picker writes into `params`. */
112
+ url: string;
113
+ bytes?: number;
114
+ }
115
+
116
+ export interface FontFamily {
117
+ id: string;
118
+ family: string;
119
+ category?: string;
120
+ variants: FontVariant[];
121
+ /** A name-only subset used to draw the list row. */
122
+ menuUrl?: string;
123
+ }
124
+
125
+ /** Backs `type: "font"` controls. Supplied by the host; partforge ships none. */
126
+ export interface FontCatalog {
127
+ search: (query: string, opts: { limit?: number }) => Promise<FontFamily[]>;
128
+ /** Reverse lookup, so a hashed-filename URL can still be named in the UI. */
129
+ describe?: (source: string) => { family: string; variant: string } | null;
130
+ }
131
+
132
+ export interface ImageAsset {
133
+ id: string;
134
+ label: string;
135
+ /** What the picker writes into `params`. */
136
+ url: string;
137
+ width?: number;
138
+ height?: number;
139
+ thumbUrl?: string;
140
+ }
141
+
142
+ /** Backs `type: "image"` controls. Supplied by the host; partforge ships none. */
143
+ export interface ImageCatalog {
144
+ search: (query: string, opts: { limit?: number }) => Promise<ImageAsset[]>;
145
+ describe?: (source: string) => { label: string; width: number; height: number } | null;
146
+ }
147
+
104
148
  export interface MountOptions {
105
149
  /**
106
150
  * Spawns a geometry worker. Called once per backend with `name` as the
@@ -143,6 +187,28 @@ export interface MountOptions {
143
187
  * part cannot support is dropped, never fatal.
144
188
  */
145
189
  viewerState?: ViewerState | null;
190
+ /**
191
+ * A provider backing every `type: "font"` control in the part. partforge
192
+ * ships none — without one, a font control renders as a plain URL field.
193
+ */
194
+ fontCatalog?: FontCatalog;
195
+ /**
196
+ * A provider backing every `type: "image"` control in the part. Without one,
197
+ * an image control degrades to a plain URL field.
198
+ */
199
+ imageCatalog?: ImageCatalog;
200
+ /**
201
+ * The upload hook for the drop target shared by the `"image"`, `"vector"`
202
+ * and `"font"` controls. Called with the CONVERTED artifact — a PNG, a
203
+ * partforge-vector document serialized as JSON, or the original file for a
204
+ * font — never the user's raw drop, and must resolve to a non-empty source
205
+ * string (an `https:` URL, or a host-defined `pfc-asset:` token) which is
206
+ * written into the param. Anything else, or a rejection, is reported through
207
+ * the control's own error line and never written. Omit it and the converted
208
+ * artifact lands in the param directly — the path a host that cannot fetch
209
+ * URLs needs, not a degraded fallback.
210
+ */
211
+ onAssetUpload?: (blob: Blob, info: { kind: AssetKind; filename: string }) => Promise<string>;
146
212
  /** @deprecated alias for `elements.viewer`. */
147
213
  container?: HTMLElement | null;
148
214
  /** @deprecated alias for `elements.controls`. */
@@ -454,3 +520,4 @@ export function viewSubParts(
454
520
  view: string,
455
521
  params: Record<string, ParamValue>,
456
522
  ): string[];
523
+
package/types/ingest.d.ts CHANGED
@@ -116,3 +116,16 @@ export interface IngestSvgOptions {
116
116
  * element is `fill="none"` with no stroke, hidden, or empty).
117
117
  */
118
118
  export function ingestSvg(svgText: string, opts?: IngestSvgOptions): VectorDocument;
119
+
120
+ export interface ImageToPngOptions {
121
+ /** Long-edge cap in px; an image already under this is not upscaled. Default 1024. */
122
+ maxSize?: number;
123
+ }
124
+
125
+ /**
126
+ * Convert any image the browser can decode into a PNG `Blob`, downsampling the
127
+ * long edge to `maxSize` on the way. Main-thread only (uses `createImageBitmap`
128
+ * and a canvas) — for a host normalising uploads before storing them, since a
129
+ * part's `images` field decodes PNG only. Never call from a part's `build`.
130
+ */
131
+ export function imageToPng(fileOrBlob: Blob | File, options?: ImageToPngOptions): Promise<Blob>;
package/types/kernel.d.ts CHANGED
@@ -481,6 +481,31 @@ export interface Vector2dOptions {
481
481
  /** Anything `k.hull`/`k.hullChain` accepts as one input. */
482
482
  export type HullInput = Shape2D | Contour;
483
483
 
484
+ /** An inline depth-map grid, row-major, 0..65535 per sample. */
485
+ export interface HeightfieldGrid {
486
+ width: number;
487
+ height: number;
488
+ data: Uint16Array;
489
+ }
490
+
491
+ /** k.heightfield — a depth map as a relief solid. */
492
+ export interface HeightfieldOptions {
493
+ w: number;
494
+ d: number;
495
+ /** Slab thickness under the relief (mm, > 0). Default 1. */
496
+ base?: number;
497
+ /** Relief height above `base` at a full-scale (1.0) sample. Default 1. */
498
+ maxZ?: number;
499
+ /** Sample spacing (mm, > 0); clamped to a vertex budget with a build warning. Default 0.5. */
500
+ pitch?: number;
501
+ /** Flip sampled value as `1 - v`, applied after `range`. */
502
+ invert?: boolean;
503
+ /** Remap with clamped ends: `range[0]` -> 0, `range[1]` -> 1. Default [0, 1]. */
504
+ range?: [number, number];
505
+ /** Footprint placement in XY only — the base always sits at z = 0. Default "center". */
506
+ origin?: "center" | "corner";
507
+ }
508
+
484
509
  // --- the kernel -------------------------------------------------------------
485
510
 
486
511
  /**
@@ -541,6 +566,13 @@ export interface GeometryKernel {
541
566
  * side-channel (not a part author's calling surface).
542
567
  */
543
568
  import(name: string): Solid;
569
+ /**
570
+ * A depth map as a relief solid. `nameOrGrid` is a name declared in the part's
571
+ * `images` field, or an inline grid — the name path is registered pre-build by
572
+ * the framework via the underscore-prefixed `_registerImage` side-channel (not
573
+ * a part author's calling surface).
574
+ */
575
+ heightfield(nameOrGrid: string | HeightfieldGrid, opts: HeightfieldOptions): Solid;
544
576
 
545
577
  // Backend-optional: the sub-part cache brackets and WASM lifetime hooks. Every
546
578
  // framework caller reaches these through `?.`, so a third-party backend may
package/types/part.d.ts CHANGED
@@ -257,6 +257,21 @@ export type FontSource =
257
257
 
258
258
  type FontSourceValue = string | ArrayBuffer | ArrayBufferView | { default: string };
259
259
 
260
+ // --- images -----------------------------------------------------------------
261
+
262
+ /**
263
+ * One entry of a part's `images` map: raw bytes, a URL string, or a thunk
264
+ * returning either (a Vite `() => import("./x.png")` resolves to
265
+ * `{ default: url }`). Resolved to a decoded luminance grid before the
266
+ * synchronous `build` runs — the source `k.heightfield()` samples.
267
+ */
268
+ export type ImageSource =
269
+ | string
270
+ | ArrayBuffer
271
+ | ArrayBufferView
272
+ | (() => ImageSourceValue | Promise<ImageSourceValue>);
273
+
274
+ type ImageSourceValue = string | ArrayBuffer | ArrayBufferView | { default: string };
260
275
  // --- imports and vectors ------------------------------------------------------
261
276
 
262
277
  /**
@@ -604,6 +619,12 @@ export interface PartDefinition<P = ResolvedParams, D = Derived> {
604
619
  defaults: Defaults;
605
620
  /** Outline fonts a part's `k.text2d()` calls need, as `{ name: source }`. */
606
621
  fonts?: Record<string, FontSource>;
622
+ /**
623
+ * Depth-map images a part's `k.heightfield()` calls need, as
624
+ * `{ name: source }` — or a function of the resolved params returning that
625
+ * map, which is what lets a `type: "image"` control drive the source.
626
+ */
627
+ images?: Record<string, ImageSource> | ((p: P) => Record<string, ImageSource>);
607
628
  /** STEP/STL/3MF files a part's `k.import()` calls need, as `{ name: source }`. */
608
629
  imports?: Record<string, ImportSource>;
609
630
  /** Vector artwork a part's `k.vector2d()` calls place, as `{ name: source }`. */