partforge 0.152.0 → 0.153.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/docs/AUTHORING-PARTS.md +20 -6
- package/package.json +1 -1
- package/src/framework/lint/rules-materials.js +1 -1
- package/src/framework/materials/patterns.js +121 -17
- package/src/framework/materials/physical.js +6 -3
- package/src/framework/materials/presets.js +11 -10
- package/src/framework/materials/sheet-look.js +111 -20
- package/src/framework/mount.js +3 -2
- package/src/framework/viewer.js +27 -9
- package/src/parts/material-swatches.js +49 -1
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -403,7 +403,7 @@ meta: { title: "…", units: "mm", environment: "studio" },
|
|
|
403
403
|
- **One material per sub-part.** Something that needs two finishes (a knurled
|
|
404
404
|
grip in rubber on an aluminium body) is two sub-parts.
|
|
405
405
|
|
|
406
|
-
**Sheet parts default to their stock.** A laser `sheetPart` that names no material (or
|
|
406
|
+
**Sheet parts default to their stock.** A laser `sheetPart` or a `routedPart` that names no material (or
|
|
407
407
|
one the library does not know) is not drawn as a PLA print: realistic mode shows it as the
|
|
408
408
|
sheet its stock label names — the first row with a stem that starts a word of the label,
|
|
409
409
|
ignoring case (so `Plexiglas`, `Acrylite` and `poly-carbonate` match; `perplexing` does not):
|
|
@@ -411,10 +411,14 @@ ignoring case (so `Plexiglas`, `Acrylite` and `poly-carbonate` match; `perplexin
|
|
|
411
411
|
| A word of the stock label starts with | Realistic look |
|
|
412
412
|
| --- | --- |
|
|
413
413
|
| `acryl`, `perspex`, `plexi`, `pmma`, `methacryl`, `polycarb`, `lexan`, `makrolon`, `lucite` | `clear-acrylic` |
|
|
414
|
+
| `pine`, `douglas` | `pine-plywood` |
|
|
415
|
+
| `walnut` | `walnut` |
|
|
416
|
+
| `oak` | `oak` |
|
|
414
417
|
| anything else — plywood, birch, basswood, poplar, MDF, hardboard — or no readable label | `plywood` |
|
|
415
418
|
|
|
416
419
|
Stock that is neither wood nor acrylic — felt, leather, card, cork — takes the `plywood`
|
|
417
|
-
look too
|
|
420
|
+
look too (charred edges and all, on a laser sheet), and a species-faced ply — `walnut
|
|
421
|
+
plywood`, `oak ply` — takes that species' solid look, without plies. A `color` tints the look, as it tints any preset (a
|
|
418
422
|
stained plywood, a coloured acrylic); the CAD view still shows the `color` itself. Only a
|
|
419
423
|
string label is read: a stock written as a `(p, d)` function counts as unreadable. Name
|
|
420
424
|
`display.material` to choose the look yourself — for such stock, or any other. Like the
|
|
@@ -445,6 +449,16 @@ from the sheet and the wood it is drawn in. It needs the sheet's own frame, so a
|
|
|
445
449
|
with a custom `build` or a `views` pose of its own shows plain wood. The CAD view and every export are
|
|
446
450
|
unchanged.
|
|
447
451
|
|
|
452
|
+
**Plywood edges.** Anything drawn in `plywood` shows its plies wherever a surface runs across
|
|
453
|
+
them: alternating face-grain and cross plies (side grain on one, darker end grain on the
|
|
454
|
+
other, swapping as the edge turns), each veneer its own tone, with a thin glue line between
|
|
455
|
+
plies. The ply count follows the thickness (3 mm → 3, 6 → 5, 9 → 7, 12 → 9, then one per
|
|
456
|
+
~1.4 mm: 18 mm → 13). A `sheetPart` or `routedPart` stacks them along its own thickness, so a
|
|
457
|
+
CNC-routed plywood board shows clean plies on its profile, pockets and drills, and a laser
|
|
458
|
+
sheet shows them through its char; any other sub-part in plywood stacks them along its
|
|
459
|
+
thinnest bounding-box axis — right for a slab, a guess for a chunky block. Realistic mode
|
|
460
|
+
only, like the burn.
|
|
461
|
+
|
|
448
462
|
**`textureScale` is millimetres, and what it measures depends on the preset's
|
|
449
463
|
pattern** — so a value copied from one preset family is wrong on another (0.2
|
|
450
464
|
on oak shrinks the grain to a 0.2 mm tile, i.e. invisible noise):
|
|
@@ -453,7 +467,7 @@ on oak shrinks the grain to a 0.2 mm tile, i.e. invisible noise):
|
|
|
453
467
|
| --- | --- | --- | --- |
|
|
454
468
|
| layer lines | `pla-print`, `petg-print` | layer height | 0.2 |
|
|
455
469
|
| SLS grain | `nylon-sls` | grain size | 0.15 |
|
|
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`),
|
|
470
|
+
| wood | `oak`, `walnut`, `plywood`, `pine-plywood` | size of one texture tile (the grain repeats every this many mm) | 250 (`oak`), 400 (`walnut`), 150 (`plywood`), 400 (`pine-plywood`; raise it toward 1000 for a whole 4 × 8 sheet with no visible repeat, at the cost of a softer grain) |
|
|
457
471
|
| carbon weave | `carbon-fiber` | size of one texture tile | 48 |
|
|
458
472
|
|
|
459
473
|
Presets without a pattern ignore it.
|
|
@@ -489,8 +503,8 @@ the viewer's blue-grey.
|
|
|
489
503
|
| `rubber` | Rubber | yes | Matte elastomer: gaskets, feet, grips; tint with `color`. |
|
|
490
504
|
| `oak` | Oak | — | Light oak with open grain. |
|
|
491
505
|
| `walnut` | Walnut | — | Dark oiled walnut. |
|
|
492
|
-
| `plywood` | Birch plywood | — | Birch plywood sheet: a pale, fine-grained face; its plies show on
|
|
493
|
-
| `pine-plywood` | Sanded pine plywood | — | Hardware-store 4 × 8 sheet plywood (sanded pine/fir face): warm, bold grain; plies show on
|
|
506
|
+
| `plywood` | Birch plywood | — | Birch plywood sheet: a pale, fine-grained face; its plies show on cut edges. |
|
|
507
|
+
| `pine-plywood` | Sanded pine plywood | — | Hardware-store 4 × 8 sheet plywood (sanded pine/fir face): warm, bold grain; plies show on cut edges. |
|
|
494
508
|
| `carbon-fiber` | Carbon fibre | — | 2x2 twill carbon fibre under clear coat. |
|
|
495
509
|
|
|
496
510
|
**Environments** (`meta.environment`, default `studio`; viewers can switch):
|
|
@@ -3963,7 +3977,7 @@ rigid motion — translate/rotate — never a reshape; an error). A pose the pro
|
|
|
3963
3977
|
|
|
3964
3978
|
**Appearance** (all warnings) — `unknown-material` (a `display.material` the
|
|
3965
3979
|
library does not know; the viewer draws it as if it named none — a PLA print
|
|
3966
|
-
in realistic mode, or for a laser sheet part its stock's look, per "Sheet parts
|
|
3980
|
+
in realistic mode, or for a laser or routed sheet part its stock's look, per "Sheet parts
|
|
3967
3981
|
default to their stock"), `unknown-environment`
|
|
3968
3982
|
(`meta.environment` not known; realistic mode uses `studio`),
|
|
3969
3983
|
`material-key-unknown` (a `display` key that is not colour, opacity, material
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ export const MATERIAL_RULES = [
|
|
|
19
19
|
id: "unknown-material",
|
|
20
20
|
run: ({ part }) => issuesOf(part, "unknown-material").map(({ name, value }) => {
|
|
21
21
|
const near = typeof value === "string" ? suggest(value, listedPresets()) : null;
|
|
22
|
-
// A laser sheet falls back to its stock's look, not to PLA (materials/sheet-look.js) —
|
|
22
|
+
// A laser or routed sheet falls back to its stock's look, not to PLA (materials/sheet-look.js) —
|
|
23
23
|
// read from a string label only: a label written as a function takes the default look.
|
|
24
24
|
const sp = part.parts[name];
|
|
25
25
|
const stock = sheetStockLook(sp);
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// they move with the sub-part under animation and never slide when the camera
|
|
6
6
|
// moves. Layer lines additionally map object space through `pfPrintFrame`
|
|
7
7
|
// (print-frame.js: display→export), so they run the way the part will print.
|
|
8
|
-
// A
|
|
9
|
-
//
|
|
8
|
+
// A laminated wood's plies (applyPlies) compose over the wood pass on its cut walls, and a
|
|
9
|
+
// laser-cut wood sheet's burn (applyBurn) over both, in the sheet's canonical frame
|
|
10
|
+
// (sheet-look.js).
|
|
10
11
|
import * as THREE from "three";
|
|
11
|
-
import { BURN, srgbToLinear } from "./sheet-look.js";
|
|
12
|
+
import { BURN, PLY, srgbToLinear } from "./sheet-look.js";
|
|
12
13
|
|
|
13
14
|
const VERT_DECL = "varying vec3 vPfObjPos;\nvarying vec3 vPfObjNormal;\nvarying vec3 vPfPrintUpView;\nuniform mat4 pfPrintFrame;\n";
|
|
14
15
|
// Wood's triplanar normal map builds its normal in OBJECT space; the fragment
|
|
@@ -358,6 +359,121 @@ export function applyBrushFrame(material) {
|
|
|
358
359
|
return material;
|
|
359
360
|
}
|
|
360
361
|
|
|
362
|
+
// --- plies ------------------------------------------------------------------------
|
|
363
|
+
// A laminated wood (presets.js `laminated`: plywood) shows its plies wherever a surface runs
|
|
364
|
+
// across them — laser cut, router pass, or the side of a plain solid. The viewer hands each
|
|
365
|
+
// such sub-part a LAMINATE frame (sheet-look.js: a canonical sheet's own frame, else
|
|
366
|
+
// boxPlyFrame's), with the plies stacked along its z over [0, pfPlyT] and the face grain
|
|
367
|
+
// along pfPlyGrain. On a wall each ply is drawn from scratch — side grain (the colour map
|
|
368
|
+
// laid along the wall, each ply its own stretch of it) or end grain (darker, rougher, a fine
|
|
369
|
+
// mottle), whichever its grain shows that wall — with its own tone, and a glue line at
|
|
370
|
+
// every inner boundary. Injected where the burn's colour body goes, before
|
|
371
|
+
// <normal_fragment_begin>; applyPlies runs first, so the burn (placed after this) chars the
|
|
372
|
+
// plies it finds and keeps them through its grain ratio. Every snippet sits between
|
|
373
|
+
// `// pf-ply {` and `// } pf-ply`. pfPlyT = 0 (no frame yet) draws the plain wood pass.
|
|
374
|
+
//
|
|
375
|
+
// Box-filtered like layer lines must be: the odd/even ply split and the glue lines are each
|
|
376
|
+
// their wave's integral differenced across fwidth, so plies too thin to draw average out
|
|
377
|
+
// instead of shimmering, and the per-ply tone fades out as the plies do. The texture reads
|
|
378
|
+
// use textureGrad on the CONTINUOUS coordinates, so a ply's jump to its own stretch of the
|
|
379
|
+
// map does not drop a seam of the coarsest mip at each boundary.
|
|
380
|
+
// A JS number as a GLSL float literal.
|
|
381
|
+
const glf = (x) => (Number.isInteger(x) ? x.toFixed(1) : String(x));
|
|
382
|
+
const vec3f = (a) => `vec3(${a.map(glf).join(", ")})`;
|
|
383
|
+
const plyBlock = (glsl) => `// pf-ply {\n${glsl}\n// } pf-ply`;
|
|
384
|
+
|
|
385
|
+
const PLY_FRAG_DECL = plyBlock(`uniform mat4 pfPlyFrame;
|
|
386
|
+
uniform float pfPlyT;
|
|
387
|
+
uniform float pfPlyCount;
|
|
388
|
+
uniform vec2 pfPlyGrain;
|
|
389
|
+
uniform vec3 pfPlyAvg;
|
|
390
|
+
// The fraction of the pixel on odd (cross) plies.
|
|
391
|
+
float pfPlyOddI(float x) { return floor(x * 0.5) + max(0.0, fract(x * 0.5) * 2.0 - 1.0); }
|
|
392
|
+
float pfPlyOdd(float x) {
|
|
393
|
+
float w = max(fwidth(x), 1e-4);
|
|
394
|
+
return clamp((pfPlyOddI(x + 0.5 * w) - pfPlyOddI(x - 0.5 * w)) / w, 0.0, 1.0);
|
|
395
|
+
}
|
|
396
|
+
// The fraction of the pixel on a glue line: pulses of half-width h (in plies) on integers.
|
|
397
|
+
float pfGlueI(float x, float h) { float y = x + h; return floor(y) * 2.0 * h + min(fract(y), 2.0 * h); }
|
|
398
|
+
float pfGlue(float x, float h) {
|
|
399
|
+
float w = max(fwidth(x), 1e-4);
|
|
400
|
+
return clamp((pfGlueI(x + 0.5 * w, h) - pfGlueI(x - 0.5 * w, h)) / w, 0.0, 1.0);
|
|
401
|
+
}`);
|
|
402
|
+
|
|
403
|
+
// `uv` is "xy" when the colour map's figure runs along u (presets.js grain "u"), else "yx".
|
|
404
|
+
const plyFragBody = (uv) => plyBlock(`float pfPlyWall = 0.0;
|
|
405
|
+
if (pfPlyT > 0.0 && pfPlyCount > 0.0) {
|
|
406
|
+
vec3 pfQ = (pfPlyFrame * vec4(vPfObjPos, 1.0)).xyz;
|
|
407
|
+
vec3 pfQn = normalize(mat3(pfPlyFrame) * vPfObjNormal);
|
|
408
|
+
pfPlyWall = 1.0 - smoothstep(${glf(PLY.wallNz[0])}, ${glf(PLY.wallNz[1])}, abs(pfQn.z));
|
|
409
|
+
if (pfPlyWall > 0.0) {
|
|
410
|
+
float pfX = clamp(pfQ.z / pfPlyT, 0.0, 1.0) * pfPlyCount; // in plies
|
|
411
|
+
float pfPly = floor(min(pfX, pfPlyCount - 0.5));
|
|
412
|
+
vec2 pfWn = normalize(pfQn.xy);
|
|
413
|
+
// How squarely each ply's grain runs into this wall: 1 is end grain.
|
|
414
|
+
float pfAlong = abs(dot(pfPlyGrain, pfWn));
|
|
415
|
+
float pfEndFace = smoothstep(0.25, 0.9, pfAlong);
|
|
416
|
+
float pfEndCross = smoothstep(0.25, 0.9, sqrt(max(0.0, 1.0 - pfAlong * pfAlong)));
|
|
417
|
+
// Two wall projections, by which in-plane axis the wall faces (the triplanar rule).
|
|
418
|
+
vec2 pfW = pow(abs(pfWn), vec2(4.0)); pfW /= (pfW.x + pfW.y);
|
|
419
|
+
float pfOff = pfPly * 37.0; // each ply its own stretch of the map
|
|
420
|
+
vec2 pfSa = vec2(pfQ.y, pfQ.z) / pfPatternScale, pfSb = vec2(pfQ.x, pfQ.z) / pfPatternScale;
|
|
421
|
+
vec2 pfDx = vec2(dFdx(pfQ.y), dFdx(pfQ.z)) / pfPatternScale, pfDy = vec2(dFdy(pfQ.y), dFdy(pfQ.z)) / pfPatternScale;
|
|
422
|
+
vec2 pfDxb = vec2(dFdx(pfQ.x), dFdx(pfQ.z)) / pfPatternScale, pfDyb = vec2(dFdy(pfQ.x), dFdy(pfQ.z)) / pfPatternScale;
|
|
423
|
+
vec2 pfO = vec2(0.0, pfOff / pfPatternScale);
|
|
424
|
+
vec3 pfSide = textureGrad(pfPatternMap, (pfSa + pfO).${uv}, pfDx.${uv}, pfDy.${uv}).rgb * pfW.x
|
|
425
|
+
+ textureGrad(pfPatternMap, (pfSb + pfO).${uv}, pfDxb.${uv}, pfDyb.${uv}).rgb * pfW.y;
|
|
426
|
+
// End grain: the same map read ACROSS its figure and finer, kept only as a mottle.
|
|
427
|
+
float pfE = ${glf(PLY.endScale)};
|
|
428
|
+
vec3 pfEndT = textureGrad(pfPatternMap, ((pfSa + pfO) * pfE).${uv === "xy" ? "yx" : "xy"}, (pfDx * pfE).${uv === "xy" ? "yx" : "xy"}, (pfDy * pfE).${uv === "xy" ? "yx" : "xy"}).rgb * pfW.x
|
|
429
|
+
+ textureGrad(pfPatternMap, ((pfSb + pfO) * pfE).${uv === "xy" ? "yx" : "xy"}, (pfDxb * pfE).${uv === "xy" ? "yx" : "xy"}, (pfDyb * pfE).${uv === "xy" ? "yx" : "xy"}).rgb * pfW.y;
|
|
430
|
+
float pfAvgL = max(dot(pfPlyAvg, vec3(0.2126, 0.7152, 0.0722)), 1e-4);
|
|
431
|
+
float pfMott = mix(1.0, dot(pfEndT, vec3(0.2126, 0.7152, 0.0722)) / pfAvgL, ${glf(PLY.endMottle)});
|
|
432
|
+
vec3 pfEnd = pfPlyAvg * ${glf(PLY.endTone)} * ${vec3f(PLY.endWarm)} * pfMott;
|
|
433
|
+
float pfOdd = pfPlyOdd(pfX);
|
|
434
|
+
float pfEndness = mix(pfEndFace, pfEndCross, pfOdd);
|
|
435
|
+
vec3 pfCol = diffuse * mix(pfSide, pfEnd, pfEndness);
|
|
436
|
+
// Each veneer its own tone, fading out as the plies get too thin to tell apart.
|
|
437
|
+
float pfJ = (pfHash(vec3(pfPly, 7.13, 3.71)) * 2.0 - 1.0) * ${glf(PLY.jitter)};
|
|
438
|
+
pfCol *= 1.0 + pfJ * (1.0 - smoothstep(0.3, 0.8, fwidth(pfX)));
|
|
439
|
+
float pfH = 0.5 * ${glf(PLY.glueMm)} * pfPlyCount / pfPlyT;
|
|
440
|
+
float pfG = pfGlue(pfX, pfH) * step(0.5, pfX) * step(pfX, pfPlyCount - 0.5);
|
|
441
|
+
pfCol = mix(pfCol, pfCol * ${glf(PLY.glue)} * ${vec3f(PLY.glueTint)}, pfG);
|
|
442
|
+
diffuseColor.rgb = mix(diffuseColor.rgb, pfCol, pfPlyWall);
|
|
443
|
+
roughnessFactor = mix(roughnessFactor, clamp(roughnessFactor + ${glf(PLY.endRough)} * pfEndness, 0.0, 1.0), pfPlyWall);
|
|
444
|
+
}
|
|
445
|
+
}`);
|
|
446
|
+
|
|
447
|
+
const PLY_FRAG_NORMAL = plyBlock(`normal = normalize(mix(normal, nonPerturbedNormal, pfPlyWall * ${glf(1 - PLY.relief)}));`);
|
|
448
|
+
|
|
449
|
+
// `faceAvg` is the preset's own colour (0xRRGGBB) — its texture's average, never the tint.
|
|
450
|
+
// Wood only, like the burn; the viewer sets the frame uniforms (viewer.js applyPlyFrame).
|
|
451
|
+
export function applyPlies(material, { faceAvg }) {
|
|
452
|
+
const uniforms = material.userData.patternUniforms;
|
|
453
|
+
if (!uniforms?.pfGrainSwap) return material;
|
|
454
|
+
Object.assign(uniforms, {
|
|
455
|
+
pfPlyFrame: { value: new THREE.Matrix4() },
|
|
456
|
+
pfPlyT: { value: 0 },
|
|
457
|
+
pfPlyCount: { value: 0 },
|
|
458
|
+
pfPlyGrain: { value: new THREE.Vector2(1, 0) },
|
|
459
|
+
pfPlyAvg: { value: new THREE.Color(faceAvg) },
|
|
460
|
+
});
|
|
461
|
+
const uv = material.userData.pfGrain === "v" ? "yx" : "xy";
|
|
462
|
+
const prev = material.onBeforeCompile;
|
|
463
|
+
const prevKey = material.customProgramCacheKey;
|
|
464
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
465
|
+
prev?.call(material, shader, renderer); // applyPattern's hook copies `uniforms` — ply uniforms included
|
|
466
|
+
shader.fragmentShader = shader.fragmentShader
|
|
467
|
+
.replace("#include <common>", `#include <common>\n${PLY_FRAG_DECL}`)
|
|
468
|
+
.replace("#include <normal_fragment_begin>", `${plyFragBody(uv)}\n#include <normal_fragment_begin>`)
|
|
469
|
+
.replace("#include <emissivemap_fragment>", `${PLY_FRAG_NORMAL}\n#include <emissivemap_fragment>`);
|
|
470
|
+
};
|
|
471
|
+
const base = prevKey && prevKey !== THREE.Material.prototype.customProgramCacheKey ? prevKey.call(material) : "";
|
|
472
|
+
material.customProgramCacheKey = () => `${base}|pf-ply`;
|
|
473
|
+
material.needsUpdate = true;
|
|
474
|
+
return material;
|
|
475
|
+
}
|
|
476
|
+
|
|
361
477
|
// --- laser burns ------------------------------------------------------------------
|
|
362
478
|
// A laser-cut sheet in a wood shows what the laser did to it (sheet-look.js decides which
|
|
363
479
|
// sub-parts, and mount hands the viewer each one's canonical frame): cut walls charred,
|
|
@@ -378,22 +494,12 @@ export function applyBrushFrame(material) {
|
|
|
378
494
|
// vertex stage is untouched — the pass reads the varyings every pattern already has. With
|
|
379
495
|
// pfSheetT = 0 (no frame delivered yet) it draws plain wood. No noise term: an unfiltered
|
|
380
496
|
// striation would shimmer at part scale (the layer-lines lesson above).
|
|
381
|
-
const glf = (x) => (Number.isInteger(x) ? x.toFixed(1) : String(x));
|
|
382
497
|
const CHARCOAL = srgbToLinear(BURN.charcoal);
|
|
383
498
|
const burnBlock = (glsl) => `// pf-burn {\n${glsl}\n// } pf-burn`;
|
|
384
499
|
|
|
385
500
|
const BURN_FRAG_DECL = burnBlock(`uniform mat4 pfSheetFrame;
|
|
386
501
|
uniform float pfSheetT;
|
|
387
|
-
uniform
|
|
388
|
-
uniform vec3 pfFaceAvg;
|
|
389
|
-
// 1 on a cross ply (an odd one), 0 on a face-grain ply: the square wave box-filtered over
|
|
390
|
-
// the pixel (its integral, differenced across fwidth), so plies too thin to draw average
|
|
391
|
-
// out instead of shimmering.
|
|
392
|
-
float pfCrossPlyI(float x) { return floor(x * 0.5) + max(0.0, fract(x * 0.5) * 2.0 - 1.0); }
|
|
393
|
-
float pfCrossPly(float x) {
|
|
394
|
-
float w = max(fwidth(x), 1e-4);
|
|
395
|
-
return (pfCrossPlyI(x + 0.5 * w) - pfCrossPlyI(x - 0.5 * w)) / w;
|
|
396
|
-
}`);
|
|
502
|
+
uniform vec3 pfFaceAvg;`);
|
|
397
503
|
|
|
398
504
|
const BURN_FRAG_BODY = burnBlock(`float pfBurnWall = 0.0;
|
|
399
505
|
float pfBurnFloor = 0.0;
|
|
@@ -411,11 +517,10 @@ if (pfSheetT > 0.0) {
|
|
|
411
517
|
// went in.
|
|
412
518
|
vec3 pfChar = min(vec3(${CHARCOAL.map((c) => c.toFixed(6)).join(", ")}), ${glf(BURN.charOfDark)} * pfFace);
|
|
413
519
|
vec3 pfEdge = mix(pfFace, pfChar, min(1.0, pfK + ${glf(BURN.exit)} * (1.0 - pfZ)));
|
|
414
|
-
if (pfPlies > 0.0) pfEdge *= mix(1.0, ${glf(BURN.crossPly)}, pfCrossPly(pfZ * pfPlies));
|
|
415
520
|
vec3 pfMark = mix(pfFace, pfChar, pfK - ${glf(BURN.engraveLess)});
|
|
416
521
|
// The grain survives the char (sheet-look.js's burnTexel is the twin): pfEdge and pfMark
|
|
417
522
|
// are what the face AVERAGE chars to, and each texel keeps its ratio to that average,
|
|
418
|
-
// blended in by BURN.grain. At 1, a pure multiply, the average chars exactly as above and
|
|
523
|
+
// blended in by BURN.grain — the plies too, which applyPlies has already drawn. At 1, a pure multiply, the average chars exactly as above and
|
|
419
524
|
// no texel comes out lighter than it went in.
|
|
420
525
|
vec3 pfGrain = mix(vec3(1.0), diffuseColor.rgb / max(pfFace, vec3(1e-4)), ${glf(BURN.grain)});
|
|
421
526
|
pfEdge *= pfGrain;
|
|
@@ -439,7 +544,6 @@ export function applyBurn(material, { faceAvg }) {
|
|
|
439
544
|
Object.assign(uniforms, {
|
|
440
545
|
pfSheetFrame: { value: new THREE.Matrix4() },
|
|
441
546
|
pfSheetT: { value: 0 },
|
|
442
|
-
pfPlies: { value: 0 },
|
|
443
547
|
pfFaceAvg: { value: new THREE.Color(faceAvg) },
|
|
444
548
|
});
|
|
445
549
|
const prev = material.onBeforeCompile;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// disagree about which preset a sub-part is.
|
|
6
6
|
import * as THREE from "three";
|
|
7
7
|
import { cadAppearance, resolveMaterial, hasAppearance } from "./resolve.js";
|
|
8
|
-
import { applyBrushFrame, applyBurn, applyPattern } from "./patterns.js";
|
|
8
|
+
import { applyBrushFrame, applyBurn, applyPattern, applyPlies } from "./patterns.js";
|
|
9
9
|
import { PATTERN_TEXTURES } from "./assets.js";
|
|
10
10
|
|
|
11
11
|
export function buildCadMaterial(display, base) {
|
|
@@ -22,10 +22,13 @@ export function buildCadMaterial(display, base) {
|
|
|
22
22
|
// Realistic-mode material. `loadTexture(fileName)` is injected (the viewer owns
|
|
23
23
|
// a caching TextureLoader), so this stays unit-testable without a network.
|
|
24
24
|
// `burn` is the viewer's per-sub-part decision (sheet-look.js burnsFor): it adds the laser
|
|
25
|
-
// burn pass to a wood and changes no other program.
|
|
25
|
+
// burn pass to a wood and changes no other program. A laminated wood (plywood) adds the ply
|
|
26
|
+
// pass first, so the burn chars the plies it draws.
|
|
26
27
|
export function buildPhysicalMaterial(display, opts = {}) {
|
|
27
28
|
let m = buildPhysical(display, opts);
|
|
28
|
-
|
|
29
|
+
const { preset } = resolveMaterial(display);
|
|
30
|
+
if (preset.laminated) m = applyPlies(m, { faceAvg: preset.color });
|
|
31
|
+
if (opts.burn) m = applyBurn(m, { faceAvg: preset.color });
|
|
29
32
|
// brushed metal picks its brush direction per pixel (patterns.js applyBrushFrame)
|
|
30
33
|
return m.userData.pfAnisotropic ? applyBrushFrame(m) : m;
|
|
31
34
|
}
|
|
@@ -79,24 +79,25 @@ export const PRESETS = {
|
|
|
79
79
|
// times that, a sanded face's relief, so it keeps its own. `grain` is the
|
|
80
80
|
// texture axis the scan's figure runs along — oak's runs up the image ("v"),
|
|
81
81
|
// walnut's and plywood's across it ("u") — so patterns.js can lay it along each sub-part's
|
|
82
|
-
// long axis. `laminated` (plywood)
|
|
83
|
-
//
|
|
82
|
+
// long axis. `laminated` (plywood) adds the ply pass (patterns.js applyPlies): plies on
|
|
83
|
+
// every cut wall, at sheet-look.js's plyCount(t).
|
|
84
84
|
oak: P("oak", "Oak", "natural", "Light oak with open grain.",
|
|
85
85
|
{ color: 0xa17e57, metalness: 0, roughness: 0.65, pattern: "wood", textureScale: 250,
|
|
86
86
|
textures: { color: "pattern-oak-color.jpg", normal: "pattern-oak-normal.jpg", roughness: "pattern-oak-rough.jpg", roughnessMean: 0.53, normalScale: 2.5, grain: "v" } }),
|
|
87
87
|
walnut: P("walnut", "Walnut", "natural", "Dark oiled walnut.",
|
|
88
88
|
{ color: 0x5d2a18, metalness: 0, roughness: 0.5, clearcoat: 0.3, clearcoatRoughness: 0.4, pattern: "wood", textureScale: 400,
|
|
89
89
|
textures: { color: "pattern-walnut-color.jpg", normal: "pattern-walnut-normal.jpg", roughness: "pattern-walnut-rough.jpg", roughnessMean: 0.628, normalScale: 3, grain: "u" } }),
|
|
90
|
-
plywood: P("plywood", "Birch plywood", "natural", "Birch plywood sheet: a pale, fine-grained face; its plies show on
|
|
90
|
+
plywood: P("plywood", "Birch plywood", "natural", "Birch plywood sheet: a pale, fine-grained face; its plies show on 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).
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
// Hardware-store 4 x 8 sheet plywood, sanded pine/fir face (ambientCG Wood048). One tile
|
|
94
|
+
// per 400 mm, half the scan's own 800 mm: at the full scan size (1000 here, once) a 1K map
|
|
95
|
+
// is ~1 mm a texel and a hand-sized part showed a smear, not grain. 400 keeps the bold
|
|
96
|
+
// cathedral figure reading as a sheet face; the cost is a figure that repeats about three
|
|
97
|
+
// times across a full 1220 mm sheet. Like plywood it is
|
|
98
|
+
// `laminated`: the ply pass draws its plies on every cut wall.
|
|
99
|
+
"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 cut edges.",
|
|
100
|
+
{ color: 0xe0b77d, metalness: 0, roughness: 0.6, pattern: "wood", textureScale: 400, laminated: true,
|
|
100
101
|
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" } }),
|
|
101
102
|
"carbon-fiber": P("carbon-fiber", "Carbon fibre", "natural", "2x2 twill carbon fibre under clear coat.",
|
|
102
103
|
{ color: 0x1b1c1e, metalness: 0.2, roughness: 0.35, clearcoat: 1, clearcoatRoughness: 0.05, pattern: "carbon", textureScale: 48 }),
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// author place, a replaced place or a custom build is not trusted with a frame — it draws
|
|
14
14
|
// plain wood, never a frame that could char a face.
|
|
15
15
|
//
|
|
16
|
-
// It also says what a
|
|
16
|
+
// It also says what a sheet part (laser or routed) with no usable material looks like: not a PLA print
|
|
17
17
|
// (resolve.js's default for every other sub-part) but the sheet its stock label names —
|
|
18
18
|
// realisticDisplay, from the ONE keyword table STOCK_LOOKS. Realistic-only, like the PLA
|
|
19
19
|
// default: the CAD view, 3MF colours and declaresMaterials read sp.display itself.
|
|
@@ -42,8 +42,9 @@ const IDENTITY = Object.freeze([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])
|
|
|
42
42
|
// takes a channel over wherever face_i < charcoal_i / charOfDark — walnut's green and blue —
|
|
43
43
|
// so a dark or strongly tinted face still chars darker in every channel. Mixing toward
|
|
44
44
|
// `charcoal` alone would RAISE walnut's near-black blue, since charcoal's is lighter. Char and
|
|
45
|
-
// scorched floors take `roughness`, a matte finish, and shed any clearcoat
|
|
46
|
-
//
|
|
45
|
+
// scorched floors take `roughness`, a matte finish, and shed any clearcoat. Plywood's plies
|
|
46
|
+
// are not the burn's: the ply pass (PLY below) draws them first, and the char keeps each
|
|
47
|
+
// texel's ratio to the face (`grain`), so they show through it.
|
|
47
48
|
// grain how much of the wood's grain shows through the char: each texel's ratio to
|
|
48
49
|
// the face average, blended in by `grain`, multiplies the average char (1, a
|
|
49
50
|
// pure multiply: every texel darkens by the same per-channel factor, so the
|
|
@@ -70,7 +71,6 @@ export const BURN = Object.freeze({
|
|
|
70
71
|
engraveLess: 0.1,
|
|
71
72
|
charOfDark: 0.37,
|
|
72
73
|
roughness: 0.68,
|
|
73
|
-
crossPly: 0.78,
|
|
74
74
|
grain: 1,
|
|
75
75
|
relief: 0.35,
|
|
76
76
|
});
|
|
@@ -83,30 +83,101 @@ export function srgbToLinear(hex) {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// Plies in a birch-plywood sheet t mm thick: 3 mm → 3, 6 → 5, 9 → 7, 12 → 9
|
|
87
|
-
// both outer plies run with the face
|
|
88
|
-
|
|
86
|
+
// Plies in a birch-plywood sheet t mm thick: 3 mm → 3, 6 → 5, 9 → 7, 12 → 9, then one ply
|
|
87
|
+
// per ~1.4 mm (15 → 11, 18 → 13, 24 → 17). Always odd, so both outer plies run with the face
|
|
88
|
+
// grain.
|
|
89
|
+
const PLY_STEPS = Object.freeze([[4.5, 3], [7.5, 5], [10.5, 7], [13.3, 9]]);
|
|
89
90
|
export function plyCount(t) {
|
|
90
91
|
for (const [below, plies] of PLY_STEPS) if (t < below) return plies;
|
|
91
|
-
return
|
|
92
|
+
return 2 * Math.round((t / PLY.thickPly - 1) / 2) + 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// How a laminated wood's cut wall looks (patterns.js applyPlies): what any `laminated`
|
|
96
|
+
// preset shows wherever a surface runs across its plies — a laser cut, a router pass, or
|
|
97
|
+
// the side of a plain solid in plywood. In a LAMINATE frame like the sheet's canonical one
|
|
98
|
+
// (plies stacked along z over [0, t]), each ply's grain alternates: the outer plies run
|
|
99
|
+
// with the face grain, every other one across it. On a wall, a ply whose grain runs along
|
|
100
|
+
// the wall shows SIDE grain — the face texture's long streaks, laid along the wall — and one
|
|
101
|
+
// whose grain runs into it shows END grain: darker and rougher, the pores open.
|
|
102
|
+
// wallNz the same wall band as the burn's (BURN.wallNz): below it a surface is a wall.
|
|
103
|
+
// endTone end grain's albedo, a multiple of the face average (birch end grain drinks
|
|
104
|
+
// light); `endWarm` tints it toward amber, per channel.
|
|
105
|
+
// endMottle how much of the texture's own variation end grain keeps (a fine mottle, not
|
|
106
|
+
// streaks: its sample runs across the grain, `endScale` times finer).
|
|
107
|
+
// endRough added to roughness on end grain.
|
|
108
|
+
// jitter each ply's own tone, ± this fraction (a hash of its index): veneers differ.
|
|
109
|
+
// glueMm the glue line between two plies, mm wide (none on the outer faces), drawn
|
|
110
|
+
// at `glue` of the ply colour, tinted toward `glueTint`.
|
|
111
|
+
// relief the share of the face's normal map a wall keeps: the face scan's relief laid
|
|
112
|
+
// across plies reads as noise, so walls are mostly geometric.
|
|
113
|
+
// thickPly above 13.3 mm, ply thickness in mm (plyCount).
|
|
114
|
+
// Like BURN, judged by eye on the contact sheet (materials.html, "Plywood edges").
|
|
115
|
+
export const PLY = Object.freeze({
|
|
116
|
+
wallNz: BURN.wallNz,
|
|
117
|
+
endTone: 0.74,
|
|
118
|
+
endWarm: Object.freeze([1.0, 0.93, 0.82]),
|
|
119
|
+
endMottle: 0.35,
|
|
120
|
+
endScale: 4,
|
|
121
|
+
endRough: 0.12,
|
|
122
|
+
jitter: 0.06,
|
|
123
|
+
glueMm: 0.12,
|
|
124
|
+
glue: 0.42,
|
|
125
|
+
glueTint: Object.freeze([1.0, 0.82, 0.62]),
|
|
126
|
+
relief: 0.25,
|
|
127
|
+
thickPly: 1.4,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Is this sub-part's realistic look a laminated wood (its walls draw plies)? Static — it
|
|
131
|
+
// reads no params — so it decides a PROGRAM, like burnsFor.
|
|
132
|
+
export function laminatedFor(sp) {
|
|
133
|
+
try {
|
|
134
|
+
const look = lookOf(sp);
|
|
135
|
+
return look.params.pattern === "wood" && look.preset.laminated === true;
|
|
136
|
+
} catch { return false; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The laminate frame of a sub-part with no sheet frame: its delivered mesh's thinnest
|
|
140
|
+
// object-space bounding-box axis is the stack, the box's low corner z = 0. `min`/`max` are
|
|
141
|
+
// the box corners ([x, y, z]); `grainAxis` is the wood pass's grain axis (patterns.js
|
|
142
|
+
// grainAxisFor). Returns { frame, t, plies, grain }: a column-major 4×4 that permutes the
|
|
143
|
+
// thin axis onto z (keeping the frame right-handed) and the face grain in that frame, a
|
|
144
|
+
// unit [x, y] (x when the grain axis IS the thin one, a cube's tie). null for an empty box.
|
|
145
|
+
export function boxPlyFrame(min, max, grainAxis = 0) {
|
|
146
|
+
const s = [0, 1, 2].map((i) => max[i] - min[i]);
|
|
147
|
+
if (!s.every((v) => Number.isFinite(v)) || Math.max(...s) <= 0) return null;
|
|
148
|
+
let thin = 2;
|
|
149
|
+
for (const i of [1, 0]) if (s[i] < s[thin] - Math.max(...s) * 1e-6) thin = i;
|
|
150
|
+
const a = (thin + 1) % 3, b = (thin + 2) % 3; // cyclic, so (a, b, thin) → (x, y, z) is a rotation
|
|
151
|
+
const frame = Array(16).fill(0);
|
|
152
|
+
frame[a * 4 + 0] = 1; frame[b * 4 + 1] = 1; frame[thin * 4 + 2] = 1; frame[15] = 1;
|
|
153
|
+
frame[12] = -min[a]; frame[13] = -min[b]; frame[14] = -min[thin];
|
|
154
|
+
const t = s[thin];
|
|
155
|
+
return { frame, t, plies: plyCount(t), grain: grainAxis === b ? [0, 1] : [1, 0] };
|
|
92
156
|
}
|
|
93
157
|
|
|
94
158
|
// The material realistic mode draws this sub-part in.
|
|
95
159
|
const lookOf = (sp) => resolveMaterial(realisticDisplay(sp));
|
|
96
160
|
|
|
97
|
-
// A laser
|
|
161
|
+
// A sheet part (laser or CNC-routed) with no usable material takes its look from its stock label: the first row
|
|
98
162
|
// with a stem that STARTS a word of the label (ignoring case; a hyphenated word is also read
|
|
99
163
|
// joined, so "poly-carbonate" is one word and "clear-acrylic" still has "acrylic"), else
|
|
100
164
|
// DEFAULT_STOCK_LOOK — plywood for birch, basswood, poplar, MDF, hardboard or anything
|
|
101
165
|
// unlabelled. Word starts, not substrings, so "Plexiglas" and "Acrylite" match while
|
|
102
166
|
// "perplexing" does not. A residual, named in the guide: stock that is neither wood nor
|
|
103
|
-
// acrylic (felt, leather, card, cork) takes the plywood look too, burns and all
|
|
167
|
+
// acrylic (felt, leather, card, cork) takes the plywood look too, burns and all; and a
|
|
168
|
+
// species-faced ply ("walnut plywood", "oak ply") takes that species' SOLID look, with no
|
|
169
|
+
// plies — name display.material to choose otherwise. Only a
|
|
104
170
|
// string label is read: a (p, d) function would make a sub-part's look (and its program)
|
|
105
171
|
// depend on the params it was first drawn at, so it counts as unlabelled.
|
|
106
172
|
export const STOCK_LOOKS = Object.freeze([
|
|
107
173
|
Object.freeze({ look: "clear-acrylic", words: Object.freeze([
|
|
108
174
|
"acryl", "perspex", "plexi", "pmma", "methacryl", "polycarb", "lexan", "makrolon", "lucite",
|
|
109
175
|
]) }),
|
|
176
|
+
// The woods the library has a look for, so a board labelled by its species draws in it.
|
|
177
|
+
// Pine first: "pine plywood" and Douglas-fir sheet are softwood ply, the pine-plywood look.
|
|
178
|
+
Object.freeze({ look: "pine-plywood", words: Object.freeze(["pine", "douglas"]) }),
|
|
179
|
+
Object.freeze({ look: "walnut", words: Object.freeze(["walnut"]) }),
|
|
180
|
+
Object.freeze({ look: "oak", words: Object.freeze(["oak"]) }),
|
|
110
181
|
]);
|
|
111
182
|
export const DEFAULT_STOCK_LOOK = "plywood";
|
|
112
183
|
|
|
@@ -121,12 +192,15 @@ export function stockLook(stock) {
|
|
|
121
192
|
return STOCK_LOOKS.find((row) => row.words.some((stem) => words.some((w) => w.startsWith(stem))))?.look ?? DEFAULT_STOCK_LOOK;
|
|
122
193
|
}
|
|
123
194
|
|
|
124
|
-
// The
|
|
195
|
+
// The processes whose parts are drawn as their stock: flat stock, cut by a laser or a router.
|
|
196
|
+
const STOCK_LOOK_PROCESSES = new Set(["laser", "cnc-router"]);
|
|
197
|
+
|
|
198
|
+
// The stock's look for a sheet part, else null (lint names it in its fallback hint).
|
|
125
199
|
export function sheetStockLook(sp) {
|
|
126
|
-
return isSheetPart(sp) && sp.sheet.process
|
|
200
|
+
return isSheetPart(sp) && STOCK_LOOK_PROCESSES.has(sp.sheet.process) ? stockLook(sp.sheet.material) : null;
|
|
127
201
|
}
|
|
128
202
|
|
|
129
|
-
// The display realistic mode draws: sp.display itself, except that a
|
|
203
|
+
// The display realistic mode draws: sp.display itself, except that a sheet part
|
|
130
204
|
// naming no KNOWN material — none, or one the library does not know, the same "no usable
|
|
131
205
|
// material" resolve.js treats as one case — gets its stock's look. A `color` rides along,
|
|
132
206
|
// so a bare colour tints that look (stained plywood, coloured acrylic).
|
|
@@ -138,26 +212,43 @@ export function realisticDisplay(sp) {
|
|
|
138
212
|
return { ...(display && typeof display === "object" ? display : {}), material: look };
|
|
139
213
|
}
|
|
140
214
|
|
|
215
|
+
// Is this a sheet part (any process) the viewer can trust with a canonical frame: one whose
|
|
216
|
+
// delivered mesh is its canonical solid moved by poseSteps alone? A custom build, an author
|
|
217
|
+
// place, a replaced place or a pose entry in a views map all move it off that pose.
|
|
218
|
+
function canonicalSheet(sp) {
|
|
219
|
+
if (!isSheetPart(sp)) return false;
|
|
220
|
+
if (sp.build !== sp.sheet.generatedBuild) return false; // a custom build is not the canonical sheet
|
|
221
|
+
if (sp.sheet.place != null) return false; // an author place moves it off its pose
|
|
222
|
+
if ((sp.place ?? null) !== (sp.sheet.generatedPlace ?? null)) return false; // place replaced after sheetPart
|
|
223
|
+
if (isViewsMap(sp.views) && Object.values(sp.views).some((v) => typeof v === "function")) return false; // a pose entry moves it off its pose
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
141
227
|
// Does this sub-part carry the burn pass at all? Static — it reads no params — so the
|
|
142
228
|
// viewer decides a sub-part's PROGRAM once, and every sub-part that answers false keeps
|
|
143
229
|
// exactly the program it had before the burn existed.
|
|
144
230
|
export function burnsFor(sp) {
|
|
145
231
|
if (!isSheetPart(sp) || sp.sheet.process !== "laser") return false;
|
|
146
|
-
if (sp
|
|
147
|
-
if (sp.sheet.place != null) return false; // an author place moves it off its pose
|
|
148
|
-
if ((sp.place ?? null) !== (sp.sheet.generatedPlace ?? null)) return false; // place replaced after sheetPart
|
|
149
|
-
if (isViewsMap(sp.views) && Object.values(sp.views).some((v) => typeof v === "function")) return false; // a pose entry moves it off its pose
|
|
232
|
+
if (!canonicalSheet(sp)) return false;
|
|
150
233
|
try { return lookOf(sp).params.pattern === "wood"; } catch { return false; }
|
|
151
234
|
}
|
|
152
235
|
|
|
236
|
+
// Does this sub-part want a canonical sheet frame from mount? A burning laser sheet does
|
|
237
|
+
// (its burn needs one), and so does any canonical sheet part — laser or routed — in a
|
|
238
|
+
// laminated wood, whose plies then stack exactly along its thickness. Everything else that
|
|
239
|
+
// is laminated stacks its plies by its bounding box (boxPlyFrame), in the viewer.
|
|
240
|
+
export function wantsSheetFrame(sp) {
|
|
241
|
+
return burnsFor(sp) || (canonicalSheet(sp) && laminatedFor(sp));
|
|
242
|
+
}
|
|
243
|
+
|
|
153
244
|
// At (p, d) — a delivery's params — the frame that maps the delivered display mesh's
|
|
154
245
|
// object space into the canonical sheet frame, the sheet's thickness, and how many plies
|
|
155
|
-
// its cut walls show (0 unless its
|
|
246
|
+
// its cut walls show (0 unless its look is laminated). null when wantsSheetFrame says no or
|
|
156
247
|
// the pose or thickness cannot be read (the build reports those).
|
|
157
248
|
// `frame` "canonical": the delivered mesh is the sheet's own canonical solid (place() not
|
|
158
249
|
// applied), so no frame maps it — identity, same t and plies.
|
|
159
250
|
export function sheetFrameFor(sp, { p, d, frame: delivered = "posed" } = {}) {
|
|
160
|
-
if (!
|
|
251
|
+
if (!wantsSheetFrame(sp)) return null;
|
|
161
252
|
const meta = sheetMeta(sp, p, d);
|
|
162
253
|
if (!meta) return null;
|
|
163
254
|
const t = meta.thickness;
|
|
@@ -165,7 +256,7 @@ export function sheetFrameFor(sp, { p, d, frame: delivered = "posed" } = {}) {
|
|
|
165
256
|
try { pose = typeof sp.sheet.pose === "function" ? sp.sheet.pose(p, d) : sp.sheet.pose; } catch { return null; }
|
|
166
257
|
if (pose != null && validatePose(pose)) return null;
|
|
167
258
|
const frame = delivered === "canonical" || pose == null ? [...IDENTITY] : invertRigid(composePose(poseSteps(pose, t)));
|
|
168
|
-
return { frame, t, plies:
|
|
259
|
+
return { frame, t, plies: laminatedFor(sp) ? plyCount(t) : 0 };
|
|
169
260
|
}
|
|
170
261
|
|
|
171
262
|
// The shader's surface classification, in JS: the frame tests hold real geometry to it.
|
package/src/framework/mount.js
CHANGED
|
@@ -14,7 +14,7 @@ import { attachSelectionPopover } from "./selection-popover.js";
|
|
|
14
14
|
import { ENVIRONMENTS } from "./materials/environments.js";
|
|
15
15
|
import { declaresMaterials, resolveMaterial } from "./materials/resolve.js";
|
|
16
16
|
import { printFrameMatrix } from "./materials/print-frame.js";
|
|
17
|
-
import {
|
|
17
|
+
import { realisticDisplay, sheetFrameFor, wantsSheetFrame } from "./materials/sheet-look.js";
|
|
18
18
|
import { pruneOverrides } from "./materials/overrides.js";
|
|
19
19
|
import { subPartRows } from "./sub-part-list.js";
|
|
20
20
|
import { buildControls } from "./controls.js";
|
|
@@ -1171,7 +1171,8 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, pick
|
|
|
1171
1171
|
const isLayerLined = (n) => {
|
|
1172
1172
|
try { return resolveMaterial(realisticDisplay(effectiveSubPart(n))).params.pattern === "layer-lines"; } catch { return false; }
|
|
1173
1173
|
};
|
|
1174
|
-
|
|
1174
|
+
// A burning laser sheet, or a canonical sheet part (laser or routed) in plywood.
|
|
1175
|
+
const isBurning = (n) => wantsSheetFrame(effectiveSubPart(n));
|
|
1175
1176
|
// LAZY: a delivery only records the view and params its job was DISPATCHED with
|
|
1176
1177
|
// (`dispatched`, set where the loop sends) — not the live ones, which a delivery shown
|
|
1177
1178
|
// during playback was never built at; the viewer pulls the frames (computeFrames)
|
package/src/framework/viewer.js
CHANGED
|
@@ -12,7 +12,7 @@ import { ensureBoxUVs } from "./materials/uv.js";
|
|
|
12
12
|
import { loadEnvironmentRig } from "./materials/environment.js";
|
|
13
13
|
import { assetUrl } from "./materials/assets.js";
|
|
14
14
|
import { resolveEnvironmentId } from "./materials/resolve.js";
|
|
15
|
-
import { burnsFor, realisticDisplay } from "./materials/sheet-look.js";
|
|
15
|
+
import { boxPlyFrame, burnsFor, realisticDisplay } from "./materials/sheet-look.js";
|
|
16
16
|
import { effectiveDisplay, mergeOverride, sanitizeDisplayPatch, subPartVisibility, APPEARANCE_OVERRIDE_KEYS, VIEWING_KEYS } from "./materials/overrides.js";
|
|
17
17
|
import { neutralToneMapToSrgb8 } from "./materials/tonemap-readback.js";
|
|
18
18
|
import { createMaterialThumbCache, thumbDisplay } from "./materials/thumb-cache.js";
|
|
@@ -844,14 +844,13 @@ export function createViewer(container, part) {
|
|
|
844
844
|
const sp = part.parts[name];
|
|
845
845
|
// `burn` is decided per sub-part material (sheet-look.js burnsFor): only a laser sheet in
|
|
846
846
|
// a wood compiles the burn pass, so every other program is exactly what it was.
|
|
847
|
-
// realisticDisplay: a
|
|
847
|
+
// realisticDisplay: a sheet part with no material draws as its stock (sheet-look.js).
|
|
848
848
|
// Both read sp.display, so hand them a sub-part whose display is the EFFECTIVE one
|
|
849
849
|
// (authored + the user's appearance override).
|
|
850
850
|
const eff = { ...sp, display: effectiveDisplayOf(name) };
|
|
851
851
|
m = cloneKeepsPattern(buildPhysicalMaterial(realisticDisplay(eff), { printFrame: printFrames[name], burn: burnsFor(eff), loadTexture }));
|
|
852
852
|
physicalMats.set(name, m);
|
|
853
853
|
syncGrain(name);
|
|
854
|
-
applySheetFrame(m, sheetFrames[name]);
|
|
855
854
|
}
|
|
856
855
|
return m;
|
|
857
856
|
}
|
|
@@ -863,6 +862,7 @@ export function createViewer(container, part) {
|
|
|
863
862
|
const m = physicalMats.get(name);
|
|
864
863
|
const geo = subCache[name];
|
|
865
864
|
if (m && geo) setGrainAxis(m, grainAxisFor(geo.boundingBox));
|
|
865
|
+
if (m) applySheetFrame(m, sheetFrames[name], name);
|
|
866
866
|
}
|
|
867
867
|
|
|
868
868
|
function publishMode(extra = {}) {
|
|
@@ -1159,16 +1159,34 @@ export function createViewer(container, part) {
|
|
|
1159
1159
|
|
|
1160
1160
|
// A burning sheet's canonical frame (sheet-look.js sheetFrameFor). No frame — not yet
|
|
1161
1161
|
// delivered, or not computable at these params — is pfSheetT = 0: plain wood.
|
|
1162
|
-
|
|
1162
|
+
// The same frame stacks a laminated sheet's plies (patterns.js applyPlies); a laminated
|
|
1163
|
+
// sub-part with no sheet frame stacks them by its own bounding box (boxPlyFrame).
|
|
1164
|
+
function applySheetFrame(m, f, name) {
|
|
1163
1165
|
const u = m.userData.patternUniforms;
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1166
|
+
if (u?.pfSheetFrame) {
|
|
1167
|
+
u.pfSheetFrame.value.fromArray(f?.frame ?? IDENTITY_MAT4);
|
|
1168
|
+
u.pfSheetT.value = f?.t ?? 0;
|
|
1169
|
+
}
|
|
1170
|
+
if (u?.pfPlyFrame) applyPlyFrame(u, m, f, name);
|
|
1171
|
+
}
|
|
1172
|
+
function applyPlyFrame(u, m, f, name) {
|
|
1173
|
+
const box = subCache[name]?.boundingBox;
|
|
1174
|
+
const axis = m.userData.pfGrainAxis ?? 0;
|
|
1175
|
+
let ply = f?.plies > 0 ? { frame: f.frame, t: f.t, plies: f.plies, grain: null } : null;
|
|
1176
|
+
if (!ply && box && !box.isEmpty()) ply = boxPlyFrame(box.min.toArray(), box.max.toArray(), axis);
|
|
1177
|
+
if (!ply) { u.pfPlyT.value = 0; return; }
|
|
1178
|
+
u.pfPlyFrame.value.fromArray(ply.frame);
|
|
1179
|
+
u.pfPlyT.value = ply.t;
|
|
1180
|
+
u.pfPlyCount.value = ply.plies;
|
|
1181
|
+
if (ply.grain) { u.pfPlyGrain.value.fromArray(ply.grain); return; }
|
|
1182
|
+
// A sheet frame: the face grain is the wood pass's grain axis carried into it.
|
|
1183
|
+
const g = new THREE.Vector3().setComponent(axis, 1).transformDirection(u.pfPlyFrame.value);
|
|
1184
|
+
if (Math.hypot(g.x, g.y) < 1e-6) u.pfPlyGrain.value.set(1, 0);
|
|
1185
|
+
else u.pfPlyGrain.value.set(g.x, g.y).normalize();
|
|
1168
1186
|
}
|
|
1169
1187
|
function setSheetFrames(frames) {
|
|
1170
1188
|
sheetFrames = frames ?? {};
|
|
1171
|
-
for (const [n, m] of physicalMats) applySheetFrame(m, sheetFrames[n]);
|
|
1189
|
+
for (const [n, m] of physicalMats) applySheetFrame(m, sheetFrames[n], n);
|
|
1172
1190
|
}
|
|
1173
1191
|
|
|
1174
1192
|
// The lazy form mount uses. Frames are PULLED only when something is about to draw the
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { PRESETS } from "../framework/materials/presets.js";
|
|
6
6
|
import { sheetPart } from "../framework/sheet/part.js";
|
|
7
7
|
import { sheetHole } from "../framework/sheet/joinery.js";
|
|
8
|
+
import { routedPart } from "../framework/sheet/routed.js";
|
|
8
9
|
|
|
9
10
|
const ids = Object.keys(PRESETS);
|
|
10
11
|
const COLS = 6, PITCH = 40;
|
|
@@ -71,10 +72,56 @@ function laserSwatch(s, unburnt) {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
// The "Plywood edges" view: the ply pass (materials/patterns.js applyPlies) on every kind of
|
|
76
|
+
// plywood wall — CNC-routed panels at 6, 12 and 18 mm (a rounded profile, a pocket and a
|
|
77
|
+
// through drill, so straight, curved and inner walls all show), one standing on edge, a
|
|
78
|
+
// routed disc that names no material (drawn as its stock; a wall that turns through every grain direction), an 18 mm pine-plywood panel, a 6 mm laser panel (plies
|
|
79
|
+
// under the char), and two plain solids in plywood that are not sheet parts at all (their
|
|
80
|
+
// plies stack along the thinnest axis).
|
|
81
|
+
const PW = 50;
|
|
82
|
+
const roundedRect = (k, x, y, w, h, r) => k.shape2d([[x, y], [x + w, y], [x + w, y + h], [x, y + h]]).fillet(r);
|
|
83
|
+
const ROUTED = [[6, 0], [12, 1], [18, 2]];
|
|
84
|
+
const plywoodParts = {
|
|
85
|
+
...Object.fromEntries(ROUTED.map(([t, col]) => [`ply_routed_${t}`, routedPart({
|
|
86
|
+
label: `Routed plywood, ${t} mm`, views: ["plywood"], material: "birch plywood", thickness: t, display: { material: "plywood" },
|
|
87
|
+
profile: (k) => roundedRect(k, col * (PW + 15), 0, PW, 36, 6),
|
|
88
|
+
pockets: (k) => [{ region: roundedRect(k, col * (PW + 15) + 6, 6, 18, 24, 3), depth: t / 2 }],
|
|
89
|
+
drills: () => [{ at: [col * (PW + 15) + 37, 18], d: 10, depth: t }],
|
|
90
|
+
})])),
|
|
91
|
+
ply_routed_standing: routedPart({
|
|
92
|
+
label: "Routed plywood, 12 mm, standing", views: ["plywood"], material: "birch plywood", thickness: 12, display: { material: "plywood" },
|
|
93
|
+
profile: (k) => roundedRect(k, 0, 0, PW, 36, 6),
|
|
94
|
+
drills: () => [{ at: [25, 18], d: 10, depth: 12 }],
|
|
95
|
+
pose: { face: "-Y", up: "+Z", at: [0, -30, 0] },
|
|
96
|
+
}),
|
|
97
|
+
ply_routed_disc: routedPart({
|
|
98
|
+
// names no material: drawn as its stock (sheet-look.js realisticDisplay)
|
|
99
|
+
label: "Routed plywood disc, 18 mm (no material)", views: ["plywood"], material: "birch plywood", thickness: 18,
|
|
100
|
+
profile: (k) => k.shape2d(Array.from({ length: 96 }, (_, i) => [90 + 22 * Math.cos((i / 96) * 2 * Math.PI), -45 + 22 * Math.sin((i / 96) * 2 * Math.PI)])),
|
|
101
|
+
}),
|
|
102
|
+
ply_routed_pine: routedPart({
|
|
103
|
+
label: "Routed pine plywood, 18 mm", views: ["plywood"], material: "pine plywood", thickness: 18, display: { material: "pine-plywood" },
|
|
104
|
+
profile: (k) => roundedRect(k, 130, -100, PW, 30, 6),
|
|
105
|
+
drills: () => [{ at: [165, -85], d: 10, depth: 18 }],
|
|
106
|
+
}),
|
|
107
|
+
ply_laser_6: sheetPart({
|
|
108
|
+
label: "Laser-cut plywood, 6 mm", views: ["plywood"], material: "birch plywood", thickness: 6, display: { material: "plywood" },
|
|
109
|
+
profile: (k) => k.shape2d([[130, -60], [130 + SW, -60], [130 + SW, -60 + SW], [130, -60 + SW]]).cut(sheetHole({ d: 8, at: [151, -39] })),
|
|
110
|
+
}),
|
|
111
|
+
ply_block: {
|
|
112
|
+
label: "Plywood block (not a sheet part)", views: ["plywood"], display: { material: "plywood" },
|
|
113
|
+
build: (k) => k.box({ min: [0, -100, 0], max: [40, -70, 18] }).cut(k.cylinder({ d: 10, h: 30 }).translate([28, -85, -5])),
|
|
114
|
+
},
|
|
115
|
+
ply_filleted: {
|
|
116
|
+
label: "Filleted plywood block (not a sheet part)", views: ["plywood"], display: { material: "plywood" },
|
|
117
|
+
build: (k) => k.box({ min: [60, -100, 0], max: [100, -70, 18] }).fillet({ r: 4 }),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
74
121
|
export default {
|
|
75
122
|
meta: { title: "Material swatches", units: "mm", environment: "studio" },
|
|
76
123
|
defaults: {},
|
|
77
|
-
views: { all: { label: "All presets", default: true }, colours: { label: "PLA colours" }, petg: { label: "PETG colours" }, print: { label: "Layer lines" }, laser: { label: "Laser-cut" }, unburnt: { label: "Laser-cut, unburnt" } },
|
|
124
|
+
views: { all: { label: "All presets", default: true }, colours: { label: "PLA colours" }, petg: { label: "PETG colours" }, print: { label: "Layer lines" }, laser: { label: "Laser-cut" }, unburnt: { label: "Laser-cut, unburnt" }, plywood: { label: "Plywood edges" } },
|
|
78
125
|
parts: {
|
|
79
126
|
...Object.fromEntries(ids.map((id, i) => [id.replaceAll("-", "_"), {
|
|
80
127
|
label: PRESETS[id].label,
|
|
@@ -121,5 +168,6 @@ export default {
|
|
|
121
168
|
[`laser_${s.key}`, laserSwatch(s, false)],
|
|
122
169
|
[`unburnt_${s.key}`, laserSwatch(s, true)],
|
|
123
170
|
])),
|
|
171
|
+
...plywoodParts,
|
|
124
172
|
},
|
|
125
173
|
};
|