partforge 0.151.0 → 0.152.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.
@@ -393,7 +393,9 @@ meta: { title: "…", units: "mm", environment: "studio" },
393
393
  marked tintable are normally coloured this way (anodizing, plastic, paint);
394
394
  the others have an intrinsic colour (brass) but still accept one. (A viewer
395
395
  user PICKING an untintable preset drops an authored tint for that pick — see
396
- the runtime section below.)
396
+ the runtime section below.) On a TEXTURED preset (`oak`, `walnut`, `plywood`,
397
+ `pine-plywood`) `color` multiplies the scanned wood and dulls its grain — leave
398
+ it unset unless you mean a stain (lint warns: `material-tint-textured`).
397
399
  - Overrides — each clamped to its range: `roughness` (0–1), `metalness` (0–1),
398
400
  `clearcoat` (0–1), `clearcoatRoughness` (0–1), `anisotropy` (0–1), `textureScale` (0.01–1000).
399
401
  Transmission, index of refraction, sheen and iridescence come only from a preset.
@@ -434,10 +436,10 @@ modelled in its print orientation, not the material. Wood, carbon fibre and SLS
434
436
  fixed to the sub-part, so they never slide when the camera or an animation
435
437
  moves.
436
438
 
437
- **Laser-cut wood.** In realistic mode a laser `sheetPart` in a wood — `plywood`, `oak` or
439
+ **Laser-cut wood.** In realistic mode a laser `sheetPart` in a wood — `plywood`, `pine-plywood`, `oak` or
438
440
  `walnut`, named or its stock's default (so a plywood sheet that names no material burns too)
439
441
  — shows what the laser did: its cut edges are charred, darker on thicker stock (on
440
- `plywood` the plies show through), and its engraving and score lines are scorched, while its
442
+ `plywood` and `pine-plywood` the plies show through), and its engraving and score lines are scorched, while its
441
443
  faces stay wood. There is nothing to set, and it is still one material: the char follows
442
444
  from the sheet and the wood it is drawn in. It needs the sheet's own frame, so a sheet part
443
445
  with a custom `build` or a `views` pose of its own shows plain wood. The CAD view and every export are
@@ -451,7 +453,7 @@ on oak shrinks the grain to a 0.2 mm tile, i.e. invisible noise):
451
453
  | --- | --- | --- | --- |
452
454
  | layer lines | `pla-print`, `petg-print` | layer height | 0.2 |
453
455
  | SLS grain | `nylon-sls` | grain size | 0.15 |
454
- | wood | `oak`, `walnut`, `plywood` | size of one texture tile (the grain repeats every this many mm) | 250 (`oak`), 400 (`walnut`), 150 (`plywood`) |
456
+ | wood | `oak`, `walnut`, `plywood`, `pine-plywood` | size of one texture tile (the grain repeats every this many mm) | 250 (`oak`), 400 (`walnut`), 150 (`plywood`), 1000 (`pine-plywood`, sized for whole 4 × 8 sheets) |
455
457
  | carbon weave | `carbon-fiber` | size of one texture tile | 48 |
456
458
 
457
459
  Presets without a pattern ignore it.
@@ -488,6 +490,7 @@ the viewer's blue-grey.
488
490
  | `oak` | Oak | — | Light oak with open grain. |
489
491
  | `walnut` | Walnut | — | Dark oiled walnut. |
490
492
  | `plywood` | Birch plywood | — | Birch plywood sheet: a pale, fine-grained face; its plies show on laser-cut edges. |
493
+ | `pine-plywood` | Sanded pine plywood | — | Hardware-store 4 × 8 sheet plywood (sanded pine/fir face): warm, bold grain; plies show on laser-cut edges. |
491
494
  | `carbon-fiber` | Carbon fibre | — | 2x2 twill carbon fibre under clear coat. |
492
495
 
493
496
  **Environments** (`meta.environment`, default `studio`; viewers can switch):
@@ -2173,8 +2176,8 @@ plate is `sheetPart`.
2173
2176
  import { routedPart, roundedRectProfile } from "partforge/geometry";
2174
2177
 
2175
2178
  plinth: routedPart({
2176
- label: "Hardwood base", views: { assembly: true, base: true }, display: { color: 0x6e3d23 },
2177
- material: "walnut", // stock label: groups pieces in the kit
2179
+ label: "Plywood base", views: { assembly: true, base: true }, display: { material: "pine-plywood" },
2180
+ material: "pine plywood", // stock label: groups pieces in the kit
2178
2181
  thickness: (p) => p.baseThickness, // the MEASURED board, from a control
2179
2182
  profile: (k, p) => k.shape2d(roundedRectProfile(p.baseLength, p.baseWidth, 10)), // the THROUGH cut
2180
2183
  pockets: (k, p, d) => [
@@ -3965,7 +3968,10 @@ default to their stock"), `unknown-environment`
3965
3968
  (`meta.environment` not known; realistic mode uses `studio`),
3966
3969
  `material-key-unknown` (a `display` key that is not colour, opacity, material
3967
3970
  or one of the six overrides; ignored), `material-override-clamped` (an override
3968
- outside its range, or not a number; clamped).
3971
+ outside its range, or not a number; clamped), `material-tint-textured` (a
3972
+ `display.color` on a textured preset that is not tintable — `oak`, `walnut`,
3973
+ `plywood`, `pine-plywood`: the colour multiplies the scanned wood and dulls its
3974
+ grain; drop it unless a stain is intended).
3969
3975
 
3970
3976
  **Geometry imports** — `import-unknown-name` (a build calls `k.import` with a
3971
3977
  name the part's `imports` field doesn't declare — this throws at build time;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.151.0",
3
+ "version": "0.152.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",
@@ -27,10 +27,27 @@ export const MATERIAL_RULES = [
27
27
  const fallback = stock ? `in realistic mode \`${stock}\`${why}` : "a PLA print in realistic mode";
28
28
  return warn("unknown-material",
29
29
  `sub-part "${name}" names material ${JSON.stringify(value)}, which is not in the library`,
30
- `${near ? `Did you mean "${near}"? ` : ""}The viewer draws it as if it named no material instead (${fallback}). Known materials: ${listedPresets().join(", ")}.`,
30
+ `${near ? `Did you mean "${near}"? ` : ""}It is drawn as if it named none (${fallback}). Known materials: ${listedPresets().join(", ")}.`,
31
31
  `parts.${name}.display.material`);
32
32
  }),
33
33
  },
34
+ {
35
+ // A `color` on a scanned-texture preset that is not tintable MULTIPLIES the scan
36
+ // (physical.js), so the grain goes muddy — usually an agent "helping" a wood look
37
+ // with a brown. A warning, not an error: a deliberate stain is legitimate.
38
+ id: "material-tint-textured",
39
+ run: ({ part }) => subParts(part).flatMap(([name, sp]) => {
40
+ const d = sp.display;
41
+ if (!d || typeof d !== "object" || d.color == null || typeof d.material !== "string") return [];
42
+ if (!Object.hasOwn(PRESETS, d.material)) return [];
43
+ const preset = PRESETS[d.material];
44
+ if (!preset.textures || preset.tintable) return [];
45
+ return [warn("material-tint-textured",
46
+ `sub-part "${name}" sets display.color on "${d.material}", a textured material: the colour multiplies the scanned ${preset.label.toLowerCase()} and dulls its grain`,
47
+ `Drop display.color to show ${d.material}'s own colour and grain — the preset already carries it, in the CAD view too. Keep color only for a deliberate stain.`,
48
+ `parts.${name}.display.color`)];
49
+ }),
50
+ },
34
51
  {
35
52
  id: "unknown-environment",
36
53
  run: ({ part }) => {
@@ -69,6 +69,9 @@ the sliced primary and gain-map buffers now start with SOI (`FF D8`) and end wit
69
69
  | `pattern-plywood-color.jpg` | Poly Haven `plywood` (colour; API key `Diffuse`) | https://polyhaven.com/a/plywood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/plywood/plywood_diff_1k.jpg | 2026-09-30 | 1K JPG, sRGB colour -> `node scripts/bake-environments.mjs --texture plywood_diff_1k.jpg pattern-plywood-color.jpg --modulate 1.85,0.9 --flatten 0.55 --quality 85` (lightened 1.85x and saturation 0.9 from the scan's mid brown to a pale birch; grain contrast 0.55, since lightening doubles it). One continuous veneer, fine even grain running across the image (`grain: "u"`). Chosen 2026-09-30 from five approved CC0 candidates: ambientCG Wood090A, Wood090B, Wood091A and Wood091B (https://ambientcg.com/a/<id>) are edge-glued strip panels with visible parallel joints, not a veneer face; Wood087/088/089 (checked earlier the same day) show plywood edges. |
70
70
  | `pattern-plywood-normal.jpg` | Poly Haven `plywood` (OpenGL normal map; API key `nor_gl`) | https://polyhaven.com/a/plywood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/plywood/plywood_nor_gl_1k.jpg | 2026-09-30 | 1K JPG, OpenGL (+Y up) tangent normals, kept as data -> `node scripts/bake-environments.mjs --texture plywood_nor_gl_1k.jpg pattern-plywood-normal.jpg --data --quality 95` (4:4:4 chroma, no grayscale/tint). The map leans slightly (mean red 118/255, a ~4° tilt along the grain) as the source does; kept as authored. |
71
71
  | `pattern-plywood-rough.jpg` | Poly Haven `plywood` (roughness; API key `Rough`) | https://polyhaven.com/a/plywood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/plywood/plywood_rough_1k.jpg | 2026-09-30 | 1K JPG, grayscale data -> `node scripts/bake-environments.mjs --texture plywood_rough_1k.jpg pattern-plywood-rough.jpg --gray` (mean 191/255 = 0.749, the preset's `roughnessMean`). |
72
+ | `pattern-pine-plywood-color.jpg` | ambientCG "Wood 048" (Color, `Wood048_1K-JPG_Color.jpg` from the 1K-JPG zip) | https://ambientcg.com/a/Wood048 — file https://ambientcg.com/get?file=Wood048_1K-JPG.zip | 2026-10-15 | 1K JPG, sRGB colour -> `node scripts/bake-environments.mjs --texture Wood048_1K-JPG_Color.jpg pattern-pine-plywood-color.jpg --modulate 1.25,1.3 --flatten 0.9 --quality 85` (lightened 1.25x and saturation 1.3 from the scan's dull olive-tan toward a sanded pine/fir sheet's warm yellow; grain contrast 0.9 keeps the bold figure). Mean `224,183,125` (`0xe0b77d`, the preset's `color`). Scanned tile 800 x 800 mm (ambientCG `dimensionX/Y`). One continuous softwood face with bold cathedral/flame figure running across the image (`grain: "u"`), seamless. Chosen 2026-10-15 as the hardware-store sheet look (the birch `plywood` set is pale and fine-grained). Rejected: ambientCG Wood047 (closest rotary-fir figure, but an 80 x 40 cm 2:1 scan the square bake would crop in half), Wood050 (same family, pinkish cast), Wood096 and Wood031 (knotty board, not a sanded face); Poly Haven `coated_pine`/`coated_pine_02` (glossy varnished knotty pine), `kitchen_wood` (grey, fine), `okoume_veneer`/`angli_veneer` (pink-brown, near-featureless hardwood); the plywood/Chipboard rows ambientCG tags `plywood` show plywood edges or particle board, not a face. |
73
+ | `pattern-pine-plywood-normal.jpg` | ambientCG "Wood 048" (NormalGL, `Wood048_1K-JPG_NormalGL.jpg`) | https://ambientcg.com/a/Wood048 — file https://ambientcg.com/get?file=Wood048_1K-JPG.zip | 2026-10-15 | 1K JPG, OpenGL (+Y up) tangent normals, kept as data -> `node scripts/bake-environments.mjs --texture Wood048_1K-JPG_NormalGL.jpg pattern-pine-plywood-normal.jpg --data --quality 88` (4:4:4 chroma, no grayscale/tint). q88, not the other wood sets' q95: this map carries about twice birch's relief (stdev 6.8 vs 3.4 /255), so its 8x8 blocks are far below the grain, and q95 (~640 KB) and q90 (~412 KB) both broke the 400 KB pattern budget; q88 is 37.7 dB PSNR against the source. |
74
+ | `pattern-pine-plywood-rough.jpg` | ambientCG "Wood 048" (Roughness, `Wood048_1K-JPG_Roughness.jpg`) | https://ambientcg.com/a/Wood048 — file https://ambientcg.com/get?file=Wood048_1K-JPG.zip | 2026-10-15 | 1K JPG, grayscale data -> `node scripts/bake-environments.mjs --texture Wood048_1K-JPG_Roughness.jpg pattern-pine-plywood-rough.jpg --gray` (mean 141/255 = 0.552, the preset's `roughnessMean`). |
72
75
  | `ground-maple-color.jpg` | Poly Haven "Oak Veneer 01" (colour; API key `Diffuse`) | https://polyhaven.com/a/oak_veneer_01 — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/oak_veneer_01/oak_veneer_01_diff_1k.jpg | 2026-09-24 | Workshop floor, "unfinished maple": `node scripts/bake-environments.mjs --texture oak_veneer_01_diff_1k.jpg ground-maple-color.jpg --modulate 1.5,0.7 --quality 85` (lightened 1.5x, saturation 0.7). |
73
76
  | `ground-maple-normal.jpg` | Poly Haven "Oak Veneer 01" (OpenGL normal map; API key `nor_gl`) | https://polyhaven.com/a/oak_veneer_01 — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/oak_veneer_01/oak_veneer_01_nor_gl_1k.jpg | 2026-09-24 | Workshop floor relief: `node scripts/bake-environments.mjs --texture oak_veneer_01_nor_gl_1k.jpg ground-maple-normal.jpg --data --quality 92`. A separate file from `pattern-oak-normal.jpg` so the floor and the oak material never share (and dispose) one texture. |
74
77
  | `ground-pei.jpg` | None — procedural | n/a | n/a | Gold-brown fine-grain speckle, no external source. First attempt tinted ambientCG "Plastic 010"'s colour map (near-uniform, stdev < 1/channel — visibly flat, no grain, when actually viewed) gold-brown; that colour map carries no usable grain at all (its roughness map does, which is why `ground-pei-rough.jpg` below is still Plastic010's roughness). Checked ambientCG alternatives (Metal049A — also flat; Granite006A — real speckle but reads as rock, not powder-coat) and found nothing closer. Generated in-baker instead, same procedural precedent as `pattern-carbon.jpg`: per-pixel triangular-distributed noise around a `168,134,82` bronze-gold base (asymmetric per channel so flecks warm rather than grey), then a 0.5px blur to turn single-pixel noise into soft grains -> `node scripts/bake-environments.mjs --speckle ground-pei.jpg --tint 168,134,82`. (Re-baked 2026-09-24 from `196,154,82`: darker and less saturated, closer to a Bambu textured-PEI plate's muted bronze-gold.) |
@@ -80,7 +83,8 @@ the sliced primary and gain-map buffers now start with SOI (`FF D8`) and end wit
80
83
 
81
84
  ambientCG publishes every asset under CC0 (https://ambientcg.com/, "CC0 Public Domain
82
85
  license", checked 2026-09-24; https://ambientcg.com/license, "Creative Commons CC0 1.0
83
- Universal License", checked again 2026-09-30 for the plywood candidates). The wood sets
86
+ Universal License", checked again 2026-09-30 for the plywood candidates and 2026-10-15 for
87
+ `pattern-pine-plywood-*`, Wood048). The wood sets
84
88
  (`pattern-oak-*`, `pattern-walnut-*`, `pattern-plywood-*`, `ground-maple-*`) are Poly Haven
85
89
  textures, CC0 like its HDRIs above (https://polyhaven.com/license, checked 2026-09-24, and
86
90
  again 2026-09-30 for `plywood`); the exact 1K JPG URLs came
@@ -92,5 +96,5 @@ The presets tile them smaller than the scanned size — see the comment on `oak`
92
96
 
93
97
  Environments stay under 1.5 MB (largest: `env-workshop.jpg` at ~398 KB); ground/pattern
94
98
  textures stay under 400 KB (largest: `ground-concrete-normal.jpg` at ~380 KB; the largest
95
- pattern map is `pattern-plywood-normal.jpg` at ~337 KB). All committed
99
+ pattern map is `pattern-pine-plywood-normal.jpg` at ~353 KB). All committed
96
100
  outputs are well inside both ceilings — see `test/framework/materials-assets.test.js`.
@@ -28,6 +28,9 @@ const URLS = {
28
28
  "pattern-plywood-color.jpg": new URL("./assets/pattern-plywood-color.jpg", import.meta.url).href,
29
29
  "pattern-plywood-normal.jpg": new URL("./assets/pattern-plywood-normal.jpg", import.meta.url).href,
30
30
  "pattern-plywood-rough.jpg": new URL("./assets/pattern-plywood-rough.jpg", import.meta.url).href,
31
+ "pattern-pine-plywood-color.jpg": new URL("./assets/pattern-pine-plywood-color.jpg", import.meta.url).href,
32
+ "pattern-pine-plywood-normal.jpg": new URL("./assets/pattern-pine-plywood-normal.jpg", import.meta.url).href,
33
+ "pattern-pine-plywood-rough.jpg": new URL("./assets/pattern-pine-plywood-rough.jpg", import.meta.url).href,
31
34
  "pattern-carbon.jpg": new URL("./assets/pattern-carbon.jpg", import.meta.url).href,
32
35
  };
33
36
 
@@ -90,6 +90,14 @@ export const PRESETS = {
90
90
  plywood: P("plywood", "Birch plywood", "natural", "Birch plywood sheet: a pale, fine-grained face; its plies show on laser-cut edges.",
91
91
  { color: 0xe1c1a1, metalness: 0, roughness: 0.6, pattern: "wood", textureScale: 150, laminated: true,
92
92
  textures: { color: "pattern-plywood-color.jpg", normal: "pattern-plywood-normal.jpg", roughness: "pattern-plywood-rough.jpg", roughnessMean: 0.749, normalScale: 1, grain: "u" } }),
93
+ // Hardware-store 4 x 8 sheet plywood, sanded pine/fir face (ambientCG Wood048). Its
94
+ // textureScale is set for whole sheets, not hand-sized parts: one tile per metre, so the
95
+ // bold cathedral figure does not visibly repeat across a 1220 mm panel (the scan itself
96
+ // is 800 mm; 1000 is the top of OVERRIDE_RANGES.textureScale). Plies show on laser-cut
97
+ // walls only — the ply bands ride the laser burn pass (sheet-look.js burnsFor).
98
+ "pine-plywood": P("pine-plywood", "Sanded pine plywood", "natural", "Hardware-store 4 × 8 sheet plywood (sanded pine/fir face): warm, bold grain; plies show on laser-cut edges.",
99
+ { color: 0xe0b77d, metalness: 0, roughness: 0.6, pattern: "wood", textureScale: 1000, laminated: true,
100
+ textures: { color: "pattern-pine-plywood-color.jpg", normal: "pattern-pine-plywood-normal.jpg", roughness: "pattern-pine-plywood-rough.jpg", roughnessMean: 0.552, normalScale: 1, grain: "u" } }),
93
101
  "carbon-fiber": P("carbon-fiber", "Carbon fibre", "natural", "2x2 twill carbon fibre under clear coat.",
94
102
  { color: 0x1b1c1e, metalness: 0.2, roughness: 0.35, clearcoat: 1, clearcoatRoughness: 0.05, pattern: "carbon", textureScale: 48 }),
95
103
  };
@@ -32,11 +32,11 @@ const withHole = (k) =>
32
32
  // can diff them pixel for pixel — and one row of sheets that name no material, drawn as
33
33
  // their stock (sheet-look.js STOCK_LOOKS).
34
34
  const SW = 30;
35
- const WOODS = ["plywood", "oak", "walnut"];
35
+ const WOODS = ["plywood", "pine-plywood", "oak", "walnut"];
36
36
  const CONTROLS_Y = -WOODS.length * PITCH;
37
37
  const LASER_SWATCHES = [
38
38
  ...WOODS.flatMap((material, row) => [3, 6, 9].map((t, col) => ({
39
- key: `${material}_${t}`, label: `${PRESETS[material].label}, ${t} mm`, at: [col * PITCH, -row * PITCH], t, display: { material },
39
+ key: `${material.replaceAll("-", "_")}_${t}`, label: `${PRESETS[material].label}, ${t} mm`, at: [col * PITCH, -row * PITCH], t, display: { material },
40
40
  }))),
41
41
  { key: "acrylic_3", label: "Clear acrylic, 3 mm", at: [0, CONTROLS_Y], t: 3, display: { material: "clear-acrylic" }, stock: "clear acrylic" },
42
42
  { key: "oak_standing", label: "Oak, 3 mm, standing", at: [PITCH, CONTROLS_Y], t: 3, display: { material: "oak" }, standing: true },