partforge 0.121.1 → 0.123.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 (63) hide show
  1. package/docs/AUTHORING-PARTS.md +164 -3
  2. package/docs/KERNEL-CONTRACT.md +10 -0
  3. package/package.json +3 -1
  4. package/src/app-materials.js +23 -0
  5. package/src/framework/app.css +16 -1
  6. package/src/framework/cutaway.js +12 -0
  7. package/src/framework/geometry/blend-surfaces.js +256 -0
  8. package/src/framework/geometry/creased-normals.js +121 -7
  9. package/src/framework/geometry/manifold-backend.js +56 -15
  10. package/src/framework/geometry/mesh-fillet.js +55 -5
  11. package/src/framework/geometry/threemf.js +32 -3
  12. package/src/framework/jobs.js +2 -1
  13. package/src/framework/lint/index.js +2 -1
  14. package/src/framework/lint/rules-materials.js +56 -0
  15. package/src/framework/materials/assets/SOURCES.md +90 -0
  16. package/src/framework/materials/assets/env-outdoor.jpg +0 -0
  17. package/src/framework/materials/assets/env-print-bed.jpg +0 -0
  18. package/src/framework/materials/assets/env-studio.jpg +0 -0
  19. package/src/framework/materials/assets/env-workshop.jpg +0 -0
  20. package/src/framework/materials/assets/ground-concrete-normal.jpg +0 -0
  21. package/src/framework/materials/assets/ground-concrete-rough.jpg +0 -0
  22. package/src/framework/materials/assets/ground-concrete.jpg +0 -0
  23. package/src/framework/materials/assets/ground-maple-color.jpg +0 -0
  24. package/src/framework/materials/assets/ground-maple-normal.jpg +0 -0
  25. package/src/framework/materials/assets/ground-paper-normal.jpg +0 -0
  26. package/src/framework/materials/assets/ground-paper-rough.jpg +0 -0
  27. package/src/framework/materials/assets/ground-paper.jpg +0 -0
  28. package/src/framework/materials/assets/ground-pei-rough.jpg +0 -0
  29. package/src/framework/materials/assets/ground-pei.jpg +0 -0
  30. package/src/framework/materials/assets/pattern-carbon.jpg +0 -0
  31. package/src/framework/materials/assets/pattern-oak-color.jpg +0 -0
  32. package/src/framework/materials/assets/pattern-oak-normal.jpg +0 -0
  33. package/src/framework/materials/assets/pattern-oak-rough.jpg +0 -0
  34. package/src/framework/materials/assets/pattern-walnut-color.jpg +0 -0
  35. package/src/framework/materials/assets/pattern-walnut-normal.jpg +0 -0
  36. package/src/framework/materials/assets/pattern-walnut-rough.jpg +0 -0
  37. package/src/framework/materials/assets.js +39 -0
  38. package/src/framework/materials/contact-shadow.js +155 -0
  39. package/src/framework/materials/environment.js +157 -0
  40. package/src/framework/materials/environments.js +82 -0
  41. package/src/framework/materials/patterns.js +243 -0
  42. package/src/framework/materials/physical.js +88 -0
  43. package/src/framework/materials/presets.js +89 -0
  44. package/src/framework/materials/print-bed.js +169 -0
  45. package/src/framework/materials/print-frame.js +18 -0
  46. package/src/framework/materials/resolve.js +117 -0
  47. package/src/framework/materials/tonemap-readback.js +36 -0
  48. package/src/framework/materials/uv.js +42 -0
  49. package/src/framework/mount.js +121 -4
  50. package/src/framework/realistic-controls.js +78 -0
  51. package/src/framework/selection/feature-highlight.js +3 -0
  52. package/src/framework/view-state.js +27 -1
  53. package/src/framework/viewer.js +623 -34
  54. package/src/materials-worker.js +3 -0
  55. package/src/parts/bracket.js +2 -1
  56. package/src/parts/hinged-box.js +3 -1
  57. package/src/parts/material-swatches.js +40 -0
  58. package/src/parts/planter.js +2 -1
  59. package/src/parts/propeller.js +2 -1
  60. package/src/parts/scott-label.js +3 -2
  61. package/src/parts/screw.js +2 -1
  62. package/types/index.d.ts +48 -3
  63. package/types/part.d.ts +15 -2
@@ -0,0 +1,3 @@
1
+ import part from "./parts/material-swatches.js";
2
+ import { runWorker } from "./framework/worker.js";
3
+ runWorker(part);
@@ -9,7 +9,7 @@
9
9
  import { roundedRectPolygon, circleProfile } from "partforge/geometry";
10
10
 
11
11
  export default {
12
- meta: { title: "Cross bracket", units: "mm", background: 0x15181d },
12
+ meta: { title: "Cross bracket", units: "mm", background: 0x15181d, environment: "workshop" },
13
13
  parameters: [
14
14
  {
15
15
  id: "size",
@@ -58,6 +58,7 @@ export default {
58
58
  label: "Cross bracket",
59
59
  views: ["bracket"],
60
60
  export: { name: "cross-bracket" },
61
+ display: { material: "powder-coat", color: 0x2b2f36 },
61
62
  build: (k, p) => {
62
63
  const barH = k.shape2d(roundedRectPolygon(p.span, p.bar, p.corner));
63
64
  const barV = k.shape2d(roundedRectPolygon(p.bar, p.span, p.corner));
@@ -6,7 +6,7 @@
6
6
  // before any motion. Also shown: an intro camera + markdown description on
7
7
  // `open`, a looping autoplay `cycle`, and per-step cameras on `assemble`.
8
8
  export default {
9
- meta: { title: "Hinged Box", units: "mm" },
9
+ meta: { title: "Hinged Box", units: "mm", environment: "workshop" },
10
10
  parameters: [
11
11
  {
12
12
  id: "box",
@@ -41,6 +41,7 @@ export default {
41
41
  label: "Base",
42
42
  views: ["box"],
43
43
  export: { name: "base" },
44
+ display: { material: "walnut" },
44
45
  build: (k, p) =>
45
46
  k.box({ min: [0, 0, 0], max: [p.width, p.depth, p.height] })
46
47
  .cut(k.box({ min: [p.wall, p.wall, p.wall], max: [p.width - p.wall, p.depth - p.wall, p.height + 1] })),
@@ -49,6 +50,7 @@ export default {
49
50
  label: "Lid",
50
51
  views: ["box"],
51
52
  export: { name: "lid" },
53
+ display: { material: "walnut" },
52
54
  build: (k, p) => k.box({ min: [0, 0, p.height], max: [p.width, p.depth, p.height + p.wall] }),
53
55
  // Display: swing about the hinge line (rear top edge, axis +X through
54
56
  // [0, depth, height]; negative angle opens upward), then the assembly
@@ -0,0 +1,40 @@
1
+ // Dev-only contact sheet for the material library: one 30 mm sample per preset,
2
+ // laid out in a grid, so a change to a preset or the pattern shader can be
3
+ // judged by eye in every environment (materials.html). Not a reference part.
4
+ import { PRESETS } from "../framework/materials/presets.js";
5
+
6
+ const ids = Object.keys(PRESETS).filter((id) => id !== "default");
7
+ const COLS = 6, PITCH = 40;
8
+
9
+ const withHole = (k) =>
10
+ k.box({ size: [30, 30, 12] }).fillet({ r: 3 }).cut(k.cylinder({ r: 5, h: 20 }).translate([0, 0, -4]));
11
+
12
+ export default {
13
+ meta: { title: "Material swatches", units: "mm", environment: "studio" },
14
+ defaults: {},
15
+ views: { all: { label: "All presets", default: true }, print: { label: "Layer lines" } },
16
+ parts: {
17
+ ...Object.fromEntries(ids.map((id, i) => [id.replaceAll("-", "_"), {
18
+ label: PRESETS[id].label,
19
+ views: ["all"],
20
+ build: (k) => withHole(k).translate([(i % COLS) * PITCH, -Math.floor(i / COLS) * PITCH, 0]),
21
+ display: { material: id },
22
+ }])),
23
+ upright_pla: {
24
+ label: "PLA, displayed upright, printed flat",
25
+ views: ["print"],
26
+ build: (k) => k.box({ size: [30, 12, 40] }),
27
+ // Displayed standing on its Z-tall face; the export pose lies it flat on
28
+ // its printed layer plane, which is what the layer-line orientation check
29
+ // in the "Layer lines" view is for.
30
+ place: (s, { purpose }) => (purpose === "export" ? s.rotateX(90) : s),
31
+ display: { material: "pla-print", color: 0xe0592a },
32
+ },
33
+ flat_pla: {
34
+ label: "PLA, same pose both ways",
35
+ views: ["print"],
36
+ build: (k) => k.box({ size: [30, 12, 40] }).translate([50, 0, 0]),
37
+ display: { material: "pla-print", color: 0xe0592a },
38
+ },
39
+ },
40
+ };
@@ -24,7 +24,7 @@ const ngon = (R, n) => {
24
24
  };
25
25
 
26
26
  export default {
27
- meta: { title: "Faceted Planter", units: "mm", background: 0x15181d },
27
+ meta: { title: "Faceted Planter", units: "mm", background: 0x15181d, environment: "print-bed" },
28
28
  parameters: [
29
29
  {
30
30
  id: "body",
@@ -100,6 +100,7 @@ export default {
100
100
  label: "Planter",
101
101
  views: ["planter"],
102
102
  export: { name: "planter" },
103
+ display: { material: "pla-print", color: 0xc4622d },
103
104
  build: (k, p, d) => {
104
105
  const body = k.prism({ points: d.outerPts, h: p.height, scaleTop: p.taper, twist: p.twist }).label("Faceted wall");
105
106
  // Hollow it. The cavity is built from z=0 sharing the body's exact twist RATE and
@@ -113,7 +113,7 @@ export const bladeSections = (p) =>
113
113
  });
114
114
 
115
115
  export default {
116
- meta: { title: "Propeller", units: "mm", background: 0x15181d },
116
+ meta: { title: "Propeller", units: "mm", background: 0x15181d, environment: "studio" },
117
117
  parameters: [
118
118
  {
119
119
  id: "prop",
@@ -173,6 +173,7 @@ export default {
173
173
  parts: {
174
174
  propeller: {
175
175
  label: "Propeller", views: ["propeller"], export: { name: "propeller" },
176
+ display: { material: "brushed-aluminum" },
176
177
  build: (k, p) => {
177
178
  const sections = bladeSections(p);
178
179
  const bladeUp = p.smooth
@@ -71,6 +71,7 @@ export default {
71
71
  title: "Editable Layered Label",
72
72
  units: "mm",
73
73
  background: 0x171a20,
74
+ environment: "print-bed",
74
75
  },
75
76
 
76
77
  parameters: [
@@ -185,7 +186,7 @@ export default {
185
186
  backing: {
186
187
  label: "Lower outer layer",
187
188
  views: ["backing", "assembly"],
188
- display: { color: 0x1e88e5 },
189
+ display: { material: "pla-print", color: 0x1e88e5 },
189
190
  build: (k, p, d) => {
190
191
  const rawTextProfile = k.text2d(d.textValue, {
191
192
  size: p.textSize,
@@ -209,7 +210,7 @@ export default {
209
210
  lettering: {
210
211
  label: "Editable lettering",
211
212
  views: ["lettering", "assembly"],
212
- display: { color: 0xfdd835 },
213
+ display: { material: "pla-print", color: 0xfdd835 },
213
214
  build: (k, p, d) => {
214
215
  const rawLetteringProfile = k.text2d(d.textValue, {
215
216
  size: p.textSize,
@@ -3,7 +3,7 @@
3
3
  // one screwSweep call yields the whole threaded shank with no boolean against a
4
4
  // core. See docs/AUTHORING-PARTS.md "Helical & threaded features".
5
5
  export default {
6
- meta: { title: "Screw", units: "mm", background: 0x15181d },
6
+ meta: { title: "Screw", units: "mm", background: 0x15181d, environment: "studio" },
7
7
  parameters: [
8
8
  {
9
9
  id: "thread",
@@ -52,6 +52,7 @@ export default {
52
52
  label: "Screw",
53
53
  views: ["screw"],
54
54
  export: { name: "screw" },
55
+ display: { material: "brushed-stainless" },
55
56
  build: (k, p, d) => {
56
57
  // Periodic profile: exactly one pitch tall, first radius == last radius.
57
58
  const shank = k.screwSweep({
package/types/index.d.ts CHANGED
@@ -110,6 +110,10 @@ export interface MountElements {
110
110
  measure?: HTMLElement | null;
111
111
  annotate?: HTMLElement | null;
112
112
  railToggle?: HTMLElement | null;
113
+ /** Realistic-mode toggle (default id `#realistic`). */
114
+ realistic?: HTMLButtonElement | null;
115
+ /** Environment picker, an empty `<select>` the mount fills (default id `#environment`). Shown only while realistic. */
116
+ environment?: HTMLSelectElement | null;
113
117
  };
114
118
  }
115
119
 
@@ -265,6 +269,20 @@ export interface ViewerState {
265
269
  camera: { pos: [number, number, number]; target: [number, number, number] } | null;
266
270
  projection: "perspective" | "orthographic";
267
271
  cutaway: CutawayState | null;
272
+ /** Always reported by `getViewerState()` — the mode being headed for, so a realistic switch still loading reads `"realistic"` (a failed one settles to `"cad"`). Optional on the way back in (a state saved by an older partforge has none). */
273
+ renderMode?: RenderMode;
274
+ /** The realistic environment id, present only when one was CHOSEN (`environment.set`, the picker) — a merely seeded default is left out so the part's own `meta.environment` still applies on remount. An unknown id falls back to `"studio"`. */
275
+ environment?: string;
276
+ }
277
+
278
+ /** The viewer's appearance: the drafting view, or physical materials under an environment. */
279
+ export type RenderMode = "cad" | "realistic";
280
+
281
+ /** What `renderMode.onChange` listeners receive. `busy` while realistic's assets load; `error` when they failed. */
282
+ export interface RenderModeEvent {
283
+ mode: RenderMode;
284
+ busy: boolean;
285
+ error: string | null;
268
286
  }
269
287
 
270
288
  /** Custom controls' transient state, keyed by the param each control owns. Plain JSON. */
@@ -342,6 +360,14 @@ export interface CaptureCurrentOptions {
342
360
  * Default false.
343
361
  */
344
362
  recenter?: boolean;
363
+ /**
364
+ * The look to capture in. Omitted: whatever the live view shows. `"cad"`
365
+ * always renders CAD. `"realistic"` from a CAD view renders realistic only
366
+ * once the current environment's assets have loaded (this call is
367
+ * synchronous and cannot wait); before that it falls back to the live look —
368
+ * `renderViews` is the async path that guarantees realistic.
369
+ */
370
+ renderMode?: RenderMode;
345
371
  }
346
372
 
347
373
  export interface CaptureViewOptions {
@@ -500,13 +526,15 @@ export interface PartRuntime {
500
526
  /**
501
527
  * Canonical-angle captures (fixed poses, framed to the visible assembly,
502
528
  * 1024², grid hidden) — sized for feeding a vision model. Defaults to
503
- * `["iso", "front", "top"]`; unknown names are dropped.
529
+ * `["iso", "front", "top"]`; unknown names are dropped. Always the CAD look,
530
+ * whatever the live render mode — `renderViews` asks for realistic.
504
531
  */
505
532
  captureViews(viewNames?: CanonicalView[] | string[]): Array<{ view: string; dataUrl: string }>;
506
533
  /**
507
534
  * One offscreen render of the user's CURRENT framing at a chosen resolution —
508
535
  * the showcase capture. Returns a `data:image/jpeg;base64,…` string, or `null`
509
- * when disposed or nothing is built yet. Never throws.
536
+ * when disposed or nothing is built yet. Never throws. Follows the live
537
+ * render mode unless `opts.renderMode` pins one.
510
538
  */
511
539
  captureCurrent(opts?: CaptureCurrentOptions): string | null;
512
540
  /** The active view (tab) name. Never null once mounted. */
@@ -527,7 +555,7 @@ export interface PartRuntime {
527
555
  */
528
556
  setActive(active: boolean): void;
529
557
  /**
530
- * Snapshot the camera, projection and cutaway so a REMOUNT can resume them —
558
+ * Snapshot the camera, projection, cutaway, render mode and environment so a REMOUNT can resume them —
531
559
  * pass the result as the next `mount()`'s `viewerState`. Read at teardown
532
560
  * time, so it is the live pose, unlike the camera the viewer persists for a
533
561
  * page reload (which only records the end of an orbit drag, and so misses a
@@ -592,6 +620,23 @@ export interface PartRuntime {
592
620
  annotate: AnnotateRuntime;
593
621
  /** The pick marker's lifetime — hold it on screen and follow it across the canvas. Always present (a no-op stand-in outside `makeHandle` tests). */
594
622
  pickMarker: PickMarkerRuntime;
623
+ /** Realistic render mode (docs "Materials and appearance"). set() resolves to the mode actually in effect — "cad" if assets failed to load. */
624
+ renderMode: {
625
+ get(): RenderMode;
626
+ set(mode: RenderMode): Promise<RenderMode>;
627
+ onChange(cb: (e: RenderModeEvent) => void): () => void;
628
+ };
629
+ /** The realistic environment. set() resolves to the id in effect — a failed switch while realistic reverts, and onChange hears the revert. */
630
+ environment: {
631
+ get(): string;
632
+ set(id: string): Promise<string>;
633
+ onChange(cb: (id: string) => void): () => void;
634
+ list(): Array<{ id: string; label: string }>;
635
+ };
636
+ /** True when any sub-part names a display.material. */
637
+ declaresMaterials: boolean;
638
+ /** Canonical-view renders in a chosen appearance, without switching the live view; "cad" (the default) is exactly captureViews(), CAD in either live mode. */
639
+ renderViews(viewNames?: CanonicalView[] | string[], opts?: { renderMode?: RenderMode }): Promise<Array<{ view: string; dataUrl: string }>>;
595
640
  }
596
641
 
597
642
  /** Mount a full parametric-part app from a `PartDefinition`. */
package/types/part.d.ts CHANGED
@@ -48,6 +48,8 @@ export interface PartMeta {
48
48
  units?: string;
49
49
  /** Scene background as `0xRRGGBB`. */
50
50
  background?: number;
51
+ /** Realistic-mode lighting/ground preset. See docs/AUTHORING-PARTS.md "Materials and appearance". */
52
+ environment?: "studio" | "workshop" | "print-bed" | "outdoor" | string;
51
53
  /** Pin a backend instead of letting the probe route the part. */
52
54
  backend?: BackendName;
53
55
  }
@@ -383,8 +385,19 @@ export interface SubPartDefinition<P = ResolvedParams, D = Derived> {
383
385
  * / `refBboxDelta` gate metrics.
384
386
  */
385
387
  reference?: string;
386
- /** Viewer-only override — `color` is `0xRRGGBB`, `opacity` is 0..1. */
387
- display?: { color?: number; opacity?: number };
388
+ /** Viewer-only appearance. See docs/AUTHORING-PARTS.md "Materials and appearance". */
389
+ display?: {
390
+ color?: number;
391
+ opacity?: number;
392
+ /** A preset id from docs/AUTHORING-PARTS.md "Materials and appearance". */
393
+ material?: string;
394
+ roughness?: number;
395
+ metalness?: number;
396
+ clearcoat?: number;
397
+ clearcoatRoughness?: number;
398
+ anisotropy?: number;
399
+ textureScale?: number;
400
+ };
388
401
  /** Filename / object name on export; defaults to the key. */
389
402
  export?: { name: string };
390
403
  }