partforge 0.134.0 → 0.136.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 +45 -14
- package/package.json +1 -1
- package/src/app-materials.js +3 -1
- package/src/framework/fonts.js +8 -0
- package/src/framework/jobs.js +38 -11
- package/src/framework/lint/rules-materials.js +9 -2
- package/src/framework/materials/assets/SOURCES.md +10 -4
- package/src/framework/materials/assets/pattern-plywood-color.jpg +0 -0
- package/src/framework/materials/assets/pattern-plywood-normal.jpg +0 -0
- package/src/framework/materials/assets/pattern-plywood-rough.jpg +0 -0
- package/src/framework/materials/assets.js +3 -0
- package/src/framework/materials/patterns.js +103 -0
- package/src/framework/materials/physical.js +5 -2
- package/src/framework/materials/presets.js +11 -5
- package/src/framework/materials/resolve.js +4 -2
- package/src/framework/materials/sheet-look.js +204 -0
- package/src/framework/mount.js +61 -32
- package/src/framework/panel/font-picker.js +76 -5
- package/src/framework/panel/widgets/font.js +13 -1
- package/src/framework/sheet/part.js +32 -17
- package/src/framework/vectors.js +3 -2
- package/src/framework/viewer.js +53 -29
- package/src/parts/laser-box.js +1 -1
- package/src/parts/material-swatches.js +58 -2
- package/types/part.d.ts +4 -0
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -334,9 +334,10 @@ meta: { title: "…", units: "mm", environment: "studio" },
|
|
|
334
334
|
- `material` — a preset id from the table below. Without one (or with one the
|
|
335
335
|
library does not know) the sub-part keeps the CAD view's look — its `color`,
|
|
336
336
|
else the viewer's blue-grey — and **realistic mode shows it as a PLA print**
|
|
337
|
-
(`pla-print`'s finish and layer lines) in that same colour
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
(`pla-print`'s finish and layer lines) in that same colour — except a laser
|
|
338
|
+
`sheetPart`, which shows its stock instead (below). So an untouched part looks
|
|
339
|
+
printed in realistic mode; name a material when it is made some other way. The
|
|
340
|
+
PLA look is realistic-only: the CAD view, export colours and
|
|
340
341
|
`declaresMaterials` are unaffected, and a part that names no material is not
|
|
341
342
|
treated as declaring one.
|
|
342
343
|
- `color` — the base colour (`0xRRGGBB`). With a preset it is the TINT. Presets
|
|
@@ -349,6 +350,23 @@ meta: { title: "…", units: "mm", environment: "studio" },
|
|
|
349
350
|
- **One material per sub-part.** Something that needs two finishes (a knurled
|
|
350
351
|
grip in rubber on an aluminium body) is two sub-parts.
|
|
351
352
|
|
|
353
|
+
**Sheet parts default to their stock.** A laser `sheetPart` that names no material (or
|
|
354
|
+
one the library does not know) is not drawn as a PLA print: realistic mode shows it as the
|
|
355
|
+
sheet its stock label names — the first row with a stem that starts a word of the label,
|
|
356
|
+
ignoring case (so `Plexiglas`, `Acrylite` and `poly-carbonate` match; `perplexing` does not):
|
|
357
|
+
|
|
358
|
+
| A word of the stock label starts with | Realistic look |
|
|
359
|
+
| --- | --- |
|
|
360
|
+
| `acryl`, `perspex`, `plexi`, `pmma`, `methacryl`, `polycarb`, `lexan`, `makrolon`, `lucite` | `clear-acrylic` |
|
|
361
|
+
| anything else — plywood, birch, basswood, poplar, MDF, hardboard — or no readable label | `plywood` |
|
|
362
|
+
|
|
363
|
+
Stock that is neither wood nor acrylic — felt, leather, card, cork — takes the `plywood`
|
|
364
|
+
look too, charred edges and all. A `color` tints the look, as it tints any preset (a
|
|
365
|
+
stained plywood, a coloured acrylic); the CAD view still shows the `color` itself. Only a
|
|
366
|
+
string label is read: a stock written as a `(p, d)` function counts as unreadable. Name
|
|
367
|
+
`display.material` to choose the look yourself — for such stock, or any other. Like the
|
|
368
|
+
PLA look, this is realistic-only and never makes a part declare a material.
|
|
369
|
+
|
|
352
370
|
**Where it shows.** The CAD view (with feature lines) shows each material's
|
|
353
371
|
colour, flattened so dark materials stay readable. **Realistic** mode — the
|
|
354
372
|
viewer's toggle — shows the full material under environment lighting, with a
|
|
@@ -365,6 +383,15 @@ displayed. If the lines run the wrong way, fix the export pose with `place()`
|
|
|
365
383
|
fixed to the sub-part, so they never slide when the camera or an animation
|
|
366
384
|
moves.
|
|
367
385
|
|
|
386
|
+
**Laser-cut wood.** In realistic mode a laser `sheetPart` in a wood — `plywood`, `oak` or
|
|
387
|
+
`walnut`, named or its stock's default (so a plywood sheet that names no material burns too)
|
|
388
|
+
— shows what the laser did: its cut edges are charred, darker on thicker stock (on
|
|
389
|
+
`plywood` the plies show through), and its engraving and score lines are scorched, while its
|
|
390
|
+
faces stay wood. There is nothing to set, and it is still one material: the char follows
|
|
391
|
+
from the sheet and the wood it is drawn in. It needs the sheet's own frame, so a sheet part
|
|
392
|
+
with a custom `build` or its own `place` shows plain wood. The CAD view and every export are
|
|
393
|
+
unchanged.
|
|
394
|
+
|
|
368
395
|
**`textureScale` is millimetres, and what it measures depends on the preset's
|
|
369
396
|
pattern** — so a value copied from one preset family is wrong on another (0.2
|
|
370
397
|
on oak shrinks the grain to a 0.2 mm tile, i.e. invisible noise):
|
|
@@ -373,7 +400,7 @@ on oak shrinks the grain to a 0.2 mm tile, i.e. invisible noise):
|
|
|
373
400
|
| --- | --- | --- | --- |
|
|
374
401
|
| layer lines | `pla-print`, `petg-print` | layer height | 0.2 |
|
|
375
402
|
| SLS grain | `nylon-sls` | grain size | 0.15 |
|
|
376
|
-
| wood | `oak`, `walnut` | size of one texture tile (the grain repeats every this many mm) | 250 (`oak`), 400 (`walnut`) |
|
|
403
|
+
| wood | `oak`, `walnut`, `plywood` | size of one texture tile (the grain repeats every this many mm) | 250 (`oak`), 400 (`walnut`), 150 (`plywood`) |
|
|
377
404
|
| carbon weave | `carbon-fiber` | size of one texture tile | 48 |
|
|
378
405
|
|
|
379
406
|
Presets without a pattern ignore it.
|
|
@@ -409,6 +436,7 @@ the viewer's blue-grey.
|
|
|
409
436
|
| `rubber` | Rubber | yes | Matte elastomer: gaskets, feet, grips; tint with `color`. |
|
|
410
437
|
| `oak` | Oak | — | Light oak with open grain. |
|
|
411
438
|
| `walnut` | Walnut | — | Dark oiled walnut. |
|
|
439
|
+
| `plywood` | Birch plywood | — | Birch plywood sheet: a pale, fine-grained face; its plies show on laser-cut edges. |
|
|
412
440
|
| `carbon-fiber` | Carbon fibre | — | 2x2 twill carbon fibre under clear coat. |
|
|
413
441
|
|
|
414
442
|
**Environments** (`meta.environment`, default `studio`; viewers can switch):
|
|
@@ -1808,8 +1836,8 @@ needing the kernel is `(k, p, d) => …`; a plain value is a literal or `(p, d)
|
|
|
1808
1836
|
`material`, `thickness` and `profile` are required; `score`, `engrave`, `pose`,
|
|
1809
1837
|
`process` (default `"laser"`) are optional; `label`, `views`, `display`, `export`,
|
|
1810
1838
|
`enabled`, `exportable`, `reference` pass through; your own `place` runs after the
|
|
1811
|
-
pose. `build` is supplied — passing one throws, as do `kerf`, `outline`/`cut`
|
|
1812
|
-
`
|
|
1839
|
+
pose. `build` is supplied — passing one throws, as do `kerf`, `outline`/`cut` and
|
|
1840
|
+
`quantity`; each error names the fix.
|
|
1813
1841
|
|
|
1814
1842
|
### Cut, score and engrave
|
|
1815
1843
|
|
|
@@ -1898,7 +1926,7 @@ PRINT pose, `place` it for display, and cut the slot where the tongue lands:
|
|
|
1898
1926
|
| `verify-process-sheets-only` | warning | `verify.process` is set but every exportable part is a sheet part |
|
|
1899
1927
|
| `laser-thickness-range` | warning | a laser sheet is thinner than 0.5 mm or thicker than 12 mm |
|
|
1900
1928
|
|
|
1901
|
-
**Verify** runs the laser checks on every sheet part
|
|
1929
|
+
**Verify** runs the laser checks on every sheet part, as
|
|
1902
1930
|
*volunteered* warnings: none counts toward `declared`/`evaluated`, so none makes
|
|
1903
1931
|
`verify.ok` true on its own. Declare one in `expect` to make it count:
|
|
1904
1932
|
|
|
@@ -1928,12 +1956,13 @@ A host downloads sheet parts as one ZIP, the **cut & print kit** (`format: "bund
|
|
|
1928
1956
|
|
|
1929
1957
|
- One thickness per joint: fingers, tabs and T-slots join panels cut from the
|
|
1930
1958
|
same sheet.
|
|
1931
|
-
- Poses are rigid and axis-aligned: `face` and `up` are the six axis words.
|
|
1932
|
-
panel
|
|
1959
|
+
- Poses are rigid and axis-aligned: `face` and `up` are the six axis words. An
|
|
1960
|
+
angled panel is an author `place` after the pose (realistic mode then shows
|
|
1961
|
+
no laser burns), or a printed part.
|
|
1933
1962
|
- No bends, folds, living hinges or grain direction: `folds`, `bends` and
|
|
1934
1963
|
`grain` are reserved keys and throw.
|
|
1935
|
-
- Cut and score are vector lines; engraving is filled regions
|
|
1936
|
-
image
|
|
1964
|
+
- Cut and score are vector lines; engraving is filled regions, never a raster
|
|
1965
|
+
image.
|
|
1937
1966
|
- The laser checks are warnings. Sharp-corner shrinking can over-report at acute
|
|
1938
1967
|
tips, and a web within 0.05 mm of the floor can read as passing.
|
|
1939
1968
|
|
|
@@ -1986,7 +2015,7 @@ const hingeSlots = (k, p, d, pose, z) => d.hingeX.flatMap((hx) =>
|
|
|
1986
2015
|
[hx - HINGE.tongueX, hx + HINGE.tongueX].map((x) =>
|
|
1987
2016
|
k.shape2d(tab(p).slot).translate(worldToSheet(pose, [x, p.depth / 2, z]))));
|
|
1988
2017
|
|
|
1989
|
-
const PLY = { views: ["box"], display: { material: "
|
|
2018
|
+
const PLY = { views: ["box"], display: { material: "plywood" }, material: "birch plywood", thickness: (p) => p.t };
|
|
1990
2019
|
const panel = (name, label, extra = {}) => sheetPart({
|
|
1991
2020
|
...PLY, label,
|
|
1992
2021
|
profile: (k, p, d) => d.box[name].outline, // drawn as seen from outside
|
|
@@ -3177,7 +3206,8 @@ addition to) the generated view style button above:
|
|
|
3177
3206
|
revert too.
|
|
3178
3207
|
- `runtime.declaresMaterials` — `true` when any sub-part names a
|
|
3179
3208
|
`display.material`. A part with none still supports realistic mode (every
|
|
3180
|
-
sub-part renders as a PLA print in its CAD colour
|
|
3209
|
+
sub-part renders as a PLA print in its CAD colour, a laser sheet part as its
|
|
3210
|
+
stock — "Sheet parts default to their stock"), so use this to decide
|
|
3181
3211
|
whether to surface your own realistic control at all, not whether it works.
|
|
3182
3212
|
Feature lines are not a preference: they draw whenever `runtime.renderMode`
|
|
3183
3213
|
reads `"cad"` and never while it reads `"realistic"` — there is no switch
|
|
@@ -3665,7 +3695,8 @@ trust handling.
|
|
|
3665
3695
|
|
|
3666
3696
|
**Appearance** (all warnings) — `unknown-material` (a `display.material` the
|
|
3667
3697
|
library does not know; the viewer draws it as if it named none — a PLA print
|
|
3668
|
-
in realistic mode
|
|
3698
|
+
in realistic mode, or for a laser sheet part its stock's look, per "Sheet parts
|
|
3699
|
+
default to their stock"), `unknown-environment`
|
|
3669
3700
|
(`meta.environment` not known; realistic mode uses `studio`),
|
|
3670
3701
|
`material-key-unknown` (a `display` key that is not colour, opacity, material
|
|
3671
3702
|
or one of the six overrides; ignored), `material-override-clamped` (an override
|
package/package.json
CHANGED
package/src/app-materials.js
CHANGED
|
@@ -12,7 +12,9 @@ import { mount } from "./framework/index.js";
|
|
|
12
12
|
// open /materials.html. Not part of the production build — check materials.html
|
|
13
13
|
// by eye after any preset or shader change (see AGENTS.md "Architecture").
|
|
14
14
|
// Dev-only: the handle is stashed on window so scripts/check-app.mjs can drive
|
|
15
|
-
// the embedding contract (runtime.captureCurrent) the way an embedder would
|
|
15
|
+
// the embedding contract (runtime.captureCurrent) the way an embedder would, and
|
|
16
|
+
// scripts/capture-contact-sheet.mjs can switch views and environments and call
|
|
17
|
+
// runtime.renderViews — removing it breaks both.
|
|
16
18
|
window.__pfRuntime = mount(materialSwatchesPart, {
|
|
17
19
|
createWorker: (name) =>
|
|
18
20
|
new Worker(new URL("./materials-worker.js", import.meta.url), { type: "module", name }),
|
package/src/framework/fonts.js
CHANGED
|
@@ -31,3 +31,11 @@ export async function resolveFonts(fontsDecl) {
|
|
|
31
31
|
}
|
|
32
32
|
return resolveDecl(fontsDecl, resolveOne);
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// Drop one source's memo entry. jobs.js calls this once a font is PARSED: the
|
|
36
|
+
// parsed Font is what the kernel keeps (kernel._fontsBySource, bounded), so
|
|
37
|
+
// holding the raw ArrayBuffer here as well doubled the cost of every font a
|
|
38
|
+
// session ever auditioned and released none of it (spec 2026-10-01 §3).
|
|
39
|
+
export function forgetFontSource(source) {
|
|
40
|
+
cache.delete(source);
|
|
41
|
+
}
|
package/src/framework/jobs.js
CHANGED
|
@@ -6,7 +6,7 @@ import { meshTo3MF } from "./geometry/threemf.js";
|
|
|
6
6
|
import { printColor } from "./materials/resolve.js";
|
|
7
7
|
import { exportablePartNames } from "./export-select.js";
|
|
8
8
|
import { fontControlAllows, fontSourceAllowed, isNoFontSource } from "./font-source.js";
|
|
9
|
-
import { fontsFor, resolveFonts } from "./fonts.js";
|
|
9
|
+
import { fontsFor, resolveFonts, forgetFontSource } from "./fonts.js";
|
|
10
10
|
import { normalizeOpentype, parseFont } from "./geometry/opentype-interop.js";
|
|
11
11
|
import { imageControlAllows, imageSourceAllowed, isNoImageSource } from "./image-source.js";
|
|
12
12
|
import { imagesFor, ensureImages } from "./images.js";
|
|
@@ -16,6 +16,8 @@ import { vectorControlAllows, vectorSourceAllowed, isNoVectorSource } from "./ve
|
|
|
16
16
|
import { vectorsFor, ensureVectors } from "./vectors.js";
|
|
17
17
|
import { exportSubParts, resolveParams, buildPosed } from "./part-model.js";
|
|
18
18
|
|
|
19
|
+
export const FONT_SOURCE_CACHE_MAX = 4;
|
|
20
|
+
|
|
19
21
|
// The oracle loads LAZILY, per job family, never at worker boot. It is the largest
|
|
20
22
|
// JS payload in the worker's graph (measure/verify/build and silhouette/match), and
|
|
21
23
|
// only the `inspect` job runs any of it — the generate/export hot path touches none.
|
|
@@ -264,26 +266,51 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
264
266
|
if (part.fonts && kernel._fonts) {
|
|
265
267
|
const declared = fontsToResolve ?? {};
|
|
266
268
|
if (Object.keys(declared).length) {
|
|
267
|
-
onProgress("resolving fonts");
|
|
268
|
-
const opentype = normalizeOpentype(await import("opentype.js"));
|
|
269
|
-
const bufs = await resolveFonts(declared);
|
|
270
269
|
// Keyed on the SOURCE, not the name. A name is not a font identity: one
|
|
271
270
|
// worker outlives many parts (worker-rebind) and, once a font can come
|
|
272
271
|
// from a param, many picks — all of which reuse the same declared name.
|
|
273
272
|
// The old `if (!_fonts.has(name))` made the first bytes ever seen under a
|
|
274
273
|
// name permanent for the life of the worker.
|
|
275
274
|
//
|
|
276
|
-
// The
|
|
277
|
-
// resolver's
|
|
278
|
-
// every time. Key on that and this memo silently degrades to per-fetch
|
|
279
|
-
// identity — a re-parse per build — the day eviction is added there.
|
|
275
|
+
// The parse cache keys on the source and is the ONLY holder of a font:
|
|
276
|
+
// the resolver's memo is transient (forgotten once a font is parsed).
|
|
280
277
|
kernel._fontsBySource ??= new Map();
|
|
281
|
-
|
|
282
|
-
|
|
278
|
+
// Resolve only what this kernel has not parsed yet: a source already in
|
|
279
|
+
// the parse cache needs no bytes at all, which is what lets the resolver
|
|
280
|
+
// forget a buffer the moment its font is parsed (below).
|
|
281
|
+
const unparsed = Object.fromEntries(
|
|
282
|
+
Object.entries(declared).filter(([, src]) => !kernel._fontsBySource.has(src)),
|
|
283
|
+
);
|
|
284
|
+
let bufs = new Map();
|
|
285
|
+
if (Object.keys(unparsed).length) {
|
|
286
|
+
onProgress("resolving fonts");
|
|
287
|
+
bufs = await resolveFonts(unparsed);
|
|
288
|
+
}
|
|
289
|
+
const opentype = bufs.size ? normalizeOpentype(await import("opentype.js")) : null;
|
|
290
|
+
for (const [name, source] of Object.entries(declared)) {
|
|
283
291
|
let font = kernel._fontsBySource.get(source);
|
|
284
|
-
if (
|
|
292
|
+
if (font) {
|
|
293
|
+
kernel._fontsBySource.delete(source); // re-insert = most recent
|
|
294
|
+
} else {
|
|
295
|
+
font = parseFont(opentype, bufs.get(name), name);
|
|
296
|
+
forgetFontSource(source); // the parsed Font is what we keep
|
|
297
|
+
}
|
|
298
|
+
kernel._fontsBySource.set(source, font);
|
|
285
299
|
kernel._fonts.set(name, font);
|
|
286
300
|
}
|
|
301
|
+
// Bounded (spec 2026-10-01 §3): auditioning fonts in the picker
|
|
302
|
+
// otherwise kept every face ever tried, per kernel, for the worker's
|
|
303
|
+
// life. Map order is recency (re-inserted on use above), so eviction
|
|
304
|
+
// takes the oldest. A source THIS build declared is never evicted — a
|
|
305
|
+
// part declaring more than the bound would otherwise evict its own
|
|
306
|
+
// fonts and refetch/reparse them on every build — so the bound is
|
|
307
|
+
// max(FONT_SOURCE_CACHE_MAX, this build's distinct sources).
|
|
308
|
+
const keep = new Set(Object.values(declared));
|
|
309
|
+
const limit = Math.max(FONT_SOURCE_CACHE_MAX, keep.size);
|
|
310
|
+
for (const src of kernel._fontsBySource.keys()) {
|
|
311
|
+
if (kernel._fontsBySource.size <= limit) break;
|
|
312
|
+
if (!keep.has(src)) kernel._fontsBySource.delete(src);
|
|
313
|
+
}
|
|
287
314
|
}
|
|
288
315
|
// Drop every name this build's declaration does not supply. `_fonts` is
|
|
289
316
|
// the kernel's, and the kernel outlives the job: without this, a face the
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// src/framework/lint/rules-materials.js
|
|
2
2
|
// Appearance findings. All WARNINGS by design: a misspelled material must never
|
|
3
3
|
// block geometry, and the viewer already falls back (resolve.js). Pure — imports
|
|
4
|
-
// only
|
|
4
|
+
// only three-free materials and sheet data (test/lint-purity.test.js).
|
|
5
5
|
import { warn } from "./finding.js";
|
|
6
6
|
import { PRESETS } from "../materials/presets.js";
|
|
7
7
|
import { ENVIRONMENTS } from "../materials/environments.js";
|
|
8
8
|
import { resolveMaterial, resolveEnvironmentId, OVERRIDE_RANGES } from "../materials/resolve.js";
|
|
9
9
|
import { suggest } from "../geometry/op-options.js";
|
|
10
|
+
import { sheetStockLook } from "../materials/sheet-look.js";
|
|
10
11
|
|
|
11
12
|
const listedPresets = () => Object.keys(PRESETS);
|
|
12
13
|
const subParts = (part) => Object.entries(part?.parts ?? {}).filter(([, sp]) => sp && typeof sp === "object");
|
|
@@ -18,9 +19,15 @@ export const MATERIAL_RULES = [
|
|
|
18
19
|
id: "unknown-material",
|
|
19
20
|
run: ({ part }) => issuesOf(part, "unknown-material").map(({ name, value }) => {
|
|
20
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) —
|
|
23
|
+
// read from a string label only: a label written as a function takes the default look.
|
|
24
|
+
const sp = part.parts[name];
|
|
25
|
+
const stock = sheetStockLook(sp);
|
|
26
|
+
const why = typeof sp.sheet?.material === "string" ? ", from its stock label" : ": its stock label is a function";
|
|
27
|
+
const fallback = stock ? `in realistic mode \`${stock}\`${why}` : "a PLA print in realistic mode";
|
|
21
28
|
return warn("unknown-material",
|
|
22
29
|
`sub-part "${name}" names material ${JSON.stringify(value)}, which is not in the library`,
|
|
23
|
-
`${near ? `Did you mean "${near}"? ` : ""}The viewer draws it as if it named no material instead (
|
|
30
|
+
`${near ? `Did you mean "${near}"? ` : ""}The viewer draws it as if it named no material instead (${fallback}). Known materials: ${listedPresets().join(", ")}.`,
|
|
24
31
|
`parts.${name}.display.material`);
|
|
25
32
|
}),
|
|
26
33
|
},
|
|
@@ -66,6 +66,9 @@ the sliced primary and gain-map buffers now start with SOI (`FF D8`) and end wit
|
|
|
66
66
|
| `pattern-walnut-color.jpg` | Poly Haven "Dark Wood" (colour; API key `Diffuse`) | https://polyhaven.com/a/dark_wood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dark_wood/dark_wood_diff_1k.jpg | 2026-09-24 | 1K JPG, sRGB colour -> `node scripts/bake-environments.mjs --texture dark_wood_diff_1k.jpg pattern-walnut-color.jpg` (q82). Scanned tile 2000 mm (`api.polyhaven.com/info/dark_wood` dimensions). |
|
|
67
67
|
| `pattern-walnut-normal.jpg` | Poly Haven "Dark Wood" (OpenGL normal map; API key `nor_gl`) | https://polyhaven.com/a/dark_wood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dark_wood/dark_wood_nor_gl_1k.jpg | 2026-09-24 | 1K JPG, OpenGL (+Y up) tangent normals, kept as data -> `node scripts/bake-environments.mjs --texture dark_wood_nor_gl_1k.jpg pattern-walnut-normal.jpg --data --quality 95` (4:4:4 chroma, no grayscale/tint; q95 because the map is nearly flat and the viewer amplifies it, so q90's 8x8 blocks showed). |
|
|
68
68
|
| `pattern-walnut-rough.jpg` | Poly Haven "Dark Wood" (roughness; API key `Rough`) | https://polyhaven.com/a/dark_wood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dark_wood/dark_wood_rough_1k.jpg | 2026-09-24 | 1K JPG, grayscale data -> `node scripts/bake-environments.mjs --texture dark_wood_rough_1k.jpg pattern-walnut-rough.jpg --gray` (source is neutral grey; channel means unchanged). |
|
|
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
|
+
| `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
|
+
| `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`). |
|
|
69
72
|
| `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). |
|
|
70
73
|
| `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. |
|
|
71
74
|
| `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.) |
|
|
@@ -76,9 +79,11 @@ the sliced primary and gain-map buffers now start with SOI (`FF D8`) and end wit
|
|
|
76
79
|
| `pattern-carbon.jpg` | None — procedural | n/a | n/a | ambientCG has no CC0 carbon-fiber weave (searched "carbon"; only unrelated fabric hits). Generated in-baker: a 256x256 2x2 twill tile (32 threads/tile, alternating dark/light grey diagonal bands) -> `node scripts/bake-environments.mjs --carbon pattern-carbon.jpg`. No external source/licence to record. |
|
|
77
80
|
|
|
78
81
|
ambientCG publishes every asset under CC0 (https://ambientcg.com/, "CC0 Public Domain
|
|
79
|
-
license", checked 2026-09-24
|
|
80
|
-
|
|
81
|
-
(
|
|
82
|
+
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
|
|
84
|
+
(`pattern-oak-*`, `pattern-walnut-*`, `pattern-plywood-*`, `ground-maple-*`) are Poly Haven
|
|
85
|
+
textures, CC0 like its HDRIs above (https://polyhaven.com/license, checked 2026-09-24, and
|
|
86
|
+
again 2026-09-30 for `plywood`); the exact 1K JPG URLs came
|
|
82
87
|
from `https://api.polyhaven.com/files/<slug>` (keys `Diffuse`, `nor_gl`, `Rough`).
|
|
83
88
|
The presets tile them smaller than the scanned size — see the comment on `oak` in
|
|
84
89
|
`presets.js` and on `workshop` in `environments.js`.
|
|
@@ -86,5 +91,6 @@ The presets tile them smaller than the scanned size — see the comment on `oak`
|
|
|
86
91
|
## Budgets
|
|
87
92
|
|
|
88
93
|
Environments stay under 1.5 MB (largest: `env-workshop.jpg` at ~398 KB); ground/pattern
|
|
89
|
-
textures stay under 400 KB (largest: `
|
|
94
|
+
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
|
|
90
96
|
outputs are well inside both ceilings — see `test/framework/materials-assets.test.js`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -25,6 +25,9 @@ const URLS = {
|
|
|
25
25
|
"pattern-walnut-color.jpg": new URL("./assets/pattern-walnut-color.jpg", import.meta.url).href,
|
|
26
26
|
"pattern-walnut-normal.jpg": new URL("./assets/pattern-walnut-normal.jpg", import.meta.url).href,
|
|
27
27
|
"pattern-walnut-rough.jpg": new URL("./assets/pattern-walnut-rough.jpg", import.meta.url).href,
|
|
28
|
+
"pattern-plywood-color.jpg": new URL("./assets/pattern-plywood-color.jpg", import.meta.url).href,
|
|
29
|
+
"pattern-plywood-normal.jpg": new URL("./assets/pattern-plywood-normal.jpg", import.meta.url).href,
|
|
30
|
+
"pattern-plywood-rough.jpg": new URL("./assets/pattern-plywood-rough.jpg", import.meta.url).href,
|
|
28
31
|
"pattern-carbon.jpg": new URL("./assets/pattern-carbon.jpg", import.meta.url).href,
|
|
29
32
|
};
|
|
30
33
|
|
|
@@ -5,7 +5,10 @@
|
|
|
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 laser-cut wood sheet's burn (applyBurn) composes over the wood pass in the sheet's
|
|
9
|
+
// canonical frame (sheet-look.js).
|
|
8
10
|
import * as THREE from "three";
|
|
11
|
+
import { BURN, srgbToLinear } from "./sheet-look.js";
|
|
9
12
|
|
|
10
13
|
const VERT_DECL = "varying vec3 vPfObjPos;\nvarying vec3 vPfObjNormal;\nvarying vec3 vPfPrintUpView;\nuniform mat4 pfPrintFrame;\n";
|
|
11
14
|
// Wood's triplanar normal map builds its normal in OBJECT space; the fragment
|
|
@@ -354,3 +357,103 @@ export function applyBrushFrame(material) {
|
|
|
354
357
|
material.needsUpdate = true;
|
|
355
358
|
return material;
|
|
356
359
|
}
|
|
360
|
+
|
|
361
|
+
// --- laser burns ------------------------------------------------------------------
|
|
362
|
+
// A laser-cut sheet in a wood shows what the laser did to it (sheet-look.js decides which
|
|
363
|
+
// sub-parts, and mount hands the viewer each one's canonical frame): cut walls charred,
|
|
364
|
+
// engrave and score floors scorched, faces untouched. Composes over the wood pass the way
|
|
365
|
+
// applyBrushFrame does, injecting at three points in three's meshphysical order:
|
|
366
|
+
// (a) colour + roughness, before <normal_fragment_begin>: after wood's body (injected
|
|
367
|
+
// after <roughnessmap_fragment>) and <metalnessmap_fragment>. A second replace of
|
|
368
|
+
// <roughnessmap_fragment> would land IN FRONT of wood's body, so it is not reused.
|
|
369
|
+
// pfBurnWall / pfBurnFloor are declared at main() scope, like pfLayer, for (b), (c).
|
|
370
|
+
// (b) the normal, before <emissivemap_fragment>: after wood's normal map and the
|
|
371
|
+
// clearcoat normals. Char flattens the wood's relief: the wood normal fades toward
|
|
372
|
+
// the geometric one (nonPerturbedNormal, set by <normal_fragment_begin>), keeping
|
|
373
|
+
// BURN.relief of it.
|
|
374
|
+
// (c) clearcoat, after <lights_physical_fragment>, which assigns material.clearcoat:
|
|
375
|
+
// walnut's lacquer comes off the char.
|
|
376
|
+
// Every snippet sits between `// pf-burn {` and `// } pf-burn` lines: that is how the tests
|
|
377
|
+
// prove a burning program is the plain wood program plus these and nothing else. The
|
|
378
|
+
// vertex stage is untouched — the pass reads the varyings every pattern already has. With
|
|
379
|
+
// pfSheetT = 0 (no frame delivered yet) it draws plain wood. No noise term: an unfiltered
|
|
380
|
+
// striation would shimmer at part scale (the layer-lines lesson above).
|
|
381
|
+
const glf = (x) => (Number.isInteger(x) ? x.toFixed(1) : String(x));
|
|
382
|
+
const CHARCOAL = srgbToLinear(BURN.charcoal);
|
|
383
|
+
const burnBlock = (glsl) => `// pf-burn {\n${glsl}\n// } pf-burn`;
|
|
384
|
+
|
|
385
|
+
const BURN_FRAG_DECL = burnBlock(`uniform mat4 pfSheetFrame;
|
|
386
|
+
uniform float pfSheetT;
|
|
387
|
+
uniform float pfPlies;
|
|
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
|
+
}`);
|
|
397
|
+
|
|
398
|
+
const BURN_FRAG_BODY = burnBlock(`float pfBurnWall = 0.0;
|
|
399
|
+
float pfBurnFloor = 0.0;
|
|
400
|
+
if (pfSheetT > 0.0) {
|
|
401
|
+
vec3 pfC = (pfSheetFrame * vec4(vPfObjPos, 1.0)).xyz;
|
|
402
|
+
vec3 pfCn = normalize(mat3(pfSheetFrame) * vPfObjNormal);
|
|
403
|
+
pfBurnWall = 1.0 - smoothstep(${glf(BURN.wallNz[0])}, ${glf(BURN.wallNz[1])}, abs(pfCn.z));
|
|
404
|
+
pfBurnFloor = step(0.5, pfCn.z) * step(pfC.z, pfSheetT - ${glf(BURN.floorDepth)});
|
|
405
|
+
float pfK = mix(${glf(BURN.kThin)}, ${glf(BURN.kThick)}, clamp((pfSheetT - ${glf(BURN.tThin)}) / ${glf(BURN.tThick - BURN.tThin)}, 0.0, 1.0));
|
|
406
|
+
float pfZ = clamp(pfC.z / pfSheetT, 0.0, 1.0);
|
|
407
|
+
vec3 pfFace = diffuse * pfFaceAvg;
|
|
408
|
+
// Per channel, not the vector scaled by one luminance ratio (sheet-look.js's burnAlbedo,
|
|
409
|
+
// this shader's twin, explains why): a channel where charOfDark·face is already below raw
|
|
410
|
+
// charcoal chars toward ITS OWN darker value, so no channel can come out lighter than it
|
|
411
|
+
// went in.
|
|
412
|
+
vec3 pfChar = min(vec3(${CHARCOAL.map((c) => c.toFixed(6)).join(", ")}), ${glf(BURN.charOfDark)} * pfFace);
|
|
413
|
+
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
|
+
vec3 pfMark = mix(pfFace, pfChar, pfK - ${glf(BURN.engraveLess)});
|
|
416
|
+
// The grain survives the char (sheet-look.js's burnTexel is the twin): pfEdge and pfMark
|
|
417
|
+
// 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
|
|
419
|
+
// no texel comes out lighter than it went in.
|
|
420
|
+
vec3 pfGrain = mix(vec3(1.0), diffuseColor.rgb / max(pfFace, vec3(1e-4)), ${glf(BURN.grain)});
|
|
421
|
+
pfEdge *= pfGrain;
|
|
422
|
+
pfMark *= pfGrain;
|
|
423
|
+
diffuseColor.rgb = mix(diffuseColor.rgb, pfEdge, pfBurnWall);
|
|
424
|
+
diffuseColor.rgb = mix(diffuseColor.rgb, pfMark, pfBurnFloor);
|
|
425
|
+
roughnessFactor = mix(roughnessFactor, ${glf(BURN.roughness)}, max(pfBurnWall, pfBurnFloor));
|
|
426
|
+
}`);
|
|
427
|
+
|
|
428
|
+
const BURN_FRAG_NORMAL = burnBlock(`normal = normalize(mix(normal, nonPerturbedNormal, max(pfBurnWall, pfBurnFloor) * ${glf(1 - BURN.relief)}));`);
|
|
429
|
+
|
|
430
|
+
const BURN_FRAG_CLEARCOAT = burnBlock(`#ifdef USE_CLEARCOAT
|
|
431
|
+
material.clearcoat *= 1.0 - max(pfBurnWall, pfBurnFloor);
|
|
432
|
+
#endif`);
|
|
433
|
+
|
|
434
|
+
// `faceAvg` is the preset's own colour (0xRRGGBB) — its texture's average, never the tint:
|
|
435
|
+
// the tint arrives as `diffuse`, so the shader's face colour is diffuse · pfFaceAvg.
|
|
436
|
+
export function applyBurn(material, { faceAvg }) {
|
|
437
|
+
const uniforms = material.userData.patternUniforms;
|
|
438
|
+
if (!uniforms?.pfGrainSwap) return material; // wood only: the burn draws over the wood pass
|
|
439
|
+
Object.assign(uniforms, {
|
|
440
|
+
pfSheetFrame: { value: new THREE.Matrix4() },
|
|
441
|
+
pfSheetT: { value: 0 },
|
|
442
|
+
pfPlies: { value: 0 },
|
|
443
|
+
pfFaceAvg: { value: new THREE.Color(faceAvg) },
|
|
444
|
+
});
|
|
445
|
+
const prev = material.onBeforeCompile;
|
|
446
|
+
const prevKey = material.customProgramCacheKey;
|
|
447
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
448
|
+
prev?.call(material, shader, renderer); // applyPattern's hook copies `uniforms` — burn uniforms included
|
|
449
|
+
shader.fragmentShader = shader.fragmentShader
|
|
450
|
+
.replace("#include <common>", `#include <common>\n${BURN_FRAG_DECL}`)
|
|
451
|
+
.replace("#include <normal_fragment_begin>", `${BURN_FRAG_BODY}\n#include <normal_fragment_begin>`)
|
|
452
|
+
.replace("#include <emissivemap_fragment>", `${BURN_FRAG_NORMAL}\n#include <emissivemap_fragment>`)
|
|
453
|
+
.replace("#include <lights_physical_fragment>", `#include <lights_physical_fragment>\n${BURN_FRAG_CLEARCOAT}`);
|
|
454
|
+
};
|
|
455
|
+
const base = prevKey && prevKey !== THREE.Material.prototype.customProgramCacheKey ? prevKey.call(material) : "";
|
|
456
|
+
material.customProgramCacheKey = () => `${base}|pf-burn`;
|
|
457
|
+
material.needsUpdate = true;
|
|
458
|
+
return material;
|
|
459
|
+
}
|
|
@@ -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 } from "./resolve.js";
|
|
8
|
-
import { applyBrushFrame, applyPattern } from "./patterns.js";
|
|
8
|
+
import { applyBrushFrame, applyBurn, applyPattern } from "./patterns.js";
|
|
9
9
|
import { PATTERN_TEXTURES } from "./assets.js";
|
|
10
10
|
|
|
11
11
|
const APPEARANCE_KEYS = ["color", "opacity", "material", "roughness", "metalness", "clearcoat", "clearcoatRoughness", "anisotropy", "textureScale"];
|
|
@@ -24,8 +24,11 @@ export function buildCadMaterial(display, base) {
|
|
|
24
24
|
|
|
25
25
|
// Realistic-mode material. `loadTexture(fileName)` is injected (the viewer owns
|
|
26
26
|
// a caching TextureLoader), so this stays unit-testable without a network.
|
|
27
|
+
// `burn` is the viewer's per-sub-part decision (sheet-look.js burnsFor): it adds the laser
|
|
28
|
+
// burn pass to a wood and changes no other program.
|
|
27
29
|
export function buildPhysicalMaterial(display, opts = {}) {
|
|
28
|
-
|
|
30
|
+
let m = buildPhysical(display, opts);
|
|
31
|
+
if (opts.burn) m = applyBurn(m, { faceAvg: resolveMaterial(display).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
|
}
|
|
@@ -13,7 +13,8 @@ const P = (id, label, category, use, fields) => ({ id, label, category, use, tin
|
|
|
13
13
|
export const PRESETS = {
|
|
14
14
|
// Every record here is a documented, nameable material. A sub-part that names
|
|
15
15
|
// none is drawn by resolve.js (the blue-grey CAD look; a PLA print in that
|
|
16
|
-
// colour in realistic mode
|
|
16
|
+
// colour in realistic mode, or a laser sheet part's stock look — sheet-look.js
|
|
17
|
+
// realisticDisplay) — there is no hidden "default" record any more.
|
|
17
18
|
|
|
18
19
|
"machined-aluminum": P("machined-aluminum", "Machined aluminium", "aluminium", "Bare CNC-milled aluminium, fine tool marks, satin sheen.",
|
|
19
20
|
{ color: 0xf5f6f6, metalness: 1, roughness: 0.32 }),
|
|
@@ -73,17 +74,22 @@ export const PRESETS = {
|
|
|
73
74
|
// mode the texture carries the colour itself; an explicit `color` tints it.
|
|
74
75
|
// `roughnessMean` is the roughness map's mean, so the preset `roughness` (or an
|
|
75
76
|
// override) sets the AVERAGE roughness and the map only varies it. `normalScale`
|
|
76
|
-
// strengthens the normal map:
|
|
77
|
-
// ~1% tilt), which reads as flat under an environment map
|
|
77
|
+
// strengthens the normal map: the oak and walnut scans' tangent normals barely leave +Z (a
|
|
78
|
+
// ~1% tilt), which reads as flat under an environment map; plywood's carries about three
|
|
79
|
+
// times that, a sanded face's relief, so it keeps its own. `grain` is the
|
|
78
80
|
// texture axis the scan's figure runs along — oak's runs up the image ("v"),
|
|
79
|
-
// walnut's across it ("u") — so patterns.js can lay it along each sub-part's
|
|
80
|
-
// long axis.
|
|
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) asks the burn pass (patterns.js applyBurn) for ply
|
|
83
|
+
// bands on laser-cut walls, at sheet-look.js's plyCount(t).
|
|
81
84
|
oak: P("oak", "Oak", "natural", "Light oak with open grain.",
|
|
82
85
|
{ color: 0xa17e57, metalness: 0, roughness: 0.65, pattern: "wood", textureScale: 250,
|
|
83
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" } }),
|
|
84
87
|
walnut: P("walnut", "Walnut", "natural", "Dark oiled walnut.",
|
|
85
88
|
{ color: 0x5d2a18, metalness: 0, roughness: 0.5, clearcoat: 0.3, clearcoatRoughness: 0.4, pattern: "wood", textureScale: 400,
|
|
86
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 laser-cut edges.",
|
|
91
|
+
{ color: 0xe1c1a1, metalness: 0, roughness: 0.6, pattern: "wood", textureScale: 150, laminated: true,
|
|
92
|
+
textures: { color: "pattern-plywood-color.jpg", normal: "pattern-plywood-normal.jpg", roughness: "pattern-plywood-rough.jpg", roughnessMean: 0.749, normalScale: 1, grain: "u" } }),
|
|
87
93
|
"carbon-fiber": P("carbon-fiber", "Carbon fibre", "natural", "2x2 twill carbon fibre under clear coat.",
|
|
88
94
|
{ color: 0x1b1c1e, metalness: 0.2, roughness: 0.35, clearcoat: 1, clearcoatRoughness: 0.05, pattern: "carbon", textureScale: 48 }),
|
|
89
95
|
};
|
|
@@ -27,8 +27,10 @@ const isColor = (v) => Number.isInteger(v) && v >= 0 && v <= 0xffffff;
|
|
|
27
27
|
// material and threemf.js's uncoloured-object colour are the same literal).
|
|
28
28
|
// Realistic mode draws it as a PLA print — the pla-print finish, layer lines
|
|
29
29
|
// and all — in that same colour, or the part's own `color`, so a part is the
|
|
30
|
-
// same colour in both views, just printed
|
|
31
|
-
//
|
|
30
|
+
// same colour in both views, just printed — except a laser sheet part, which
|
|
31
|
+
// realistic mode draws as its stock instead (sheet-look.js realisticDisplay,
|
|
32
|
+
// applied before this resolves). `declaresMaterials` still counts only a NAMED
|
|
33
|
+
// material: this default never makes a part "declare" one.
|
|
32
34
|
export const NO_MATERIAL_COLOR = 0x9fb4cc;
|
|
33
35
|
export const NO_MATERIAL_PRESET_ID = "pla-print";
|
|
34
36
|
const NO_MATERIAL_CAD = { color: NO_MATERIAL_COLOR, metalness: 0.25, roughness: 0.55 };
|