feat(materials): dynamic library registry, picker source tabs, Other category (#525)

* feat(materials): dynamic library registry, picker source tabs, create-material entry point

Core gains a runtime material registry (registerLibraryMaterials/
unregisterLibraryMaterials/subscribeLibraryMaterials) so embedders can
feed user/community materials; library: refs to registered materials
resolve in the viewer unchanged. MaterialCatalogItem carries an optional
source (pascal|community|mine|workspace). MaterialPicker gets a source
filter row and an optional onCreateMaterialRequest '+ New material'
tile, threaded through MaterialPaintPanel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(materials): underline source tabs in picker, matching catalog browse surfaces

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(materials): add 'other' category for uncategorized library materials

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(viewer): rebuild node material when a texture slot is cleared

Reused cached WebGPU materials keep a compiled TextureNode per slot; nulling
the slot for a cold texture load (or a preset without the map) without
needsUpdate leaves the node's per-frame material reference pulling null,
crashing the render pass in TextureNode.update. Cold loads are the norm for
freshly generated library materials, whose maps aren't in the texture cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(viewer): standard materials always carry a displacement texture

three's WebGPU shadow pass copies each object's displacementMap onto one
shared per-light shadow material while caching per-mesh shadow node graphs
against that shared override. A mesh whose shadow graph was built with a
displacement TextureNode (painted with a generated material — the only
presets carrying height maps) crashes the render pass with "null (reading
'matrix')" the moment a material without a displacement texture is swapped
onto it, which is exactly what the paint hover preview does. Give every
MeshStandardNodeMaterial a shared 1x1 black displacement texel (zero offset)
when it has no real height map, so the copied slot is never null in either
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-07-21 10:00:48 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent bf89b5bcf2
commit 603f5d2242
6 changed files with 234 additions and 26 deletions
+6
View File
@@ -157,7 +157,9 @@ export {
} from './lib/zone-quantities'
export {
getCatalogMaterialById,
getDynamicLibraryMaterials,
getLibraryMaterialIdFromRef,
getLibraryMaterialsVersion,
getMaterialPresetByRef,
getMaterialsForCategory,
getSceneMaterialIdFromRef,
@@ -168,12 +170,16 @@ export {
type MaterialCatalogItem,
type MaterialCategory,
type MaterialRef,
type MaterialSource,
type MaterialSurface,
type ParsedMaterialRef,
parseMaterialRef,
registerLibraryMaterials,
SCENE_MATERIAL_REF_PREFIX,
subscribeLibraryMaterials,
toLibraryMaterialRef,
toSceneMaterialRef,
unregisterLibraryMaterials,
} from './material-library'
export type {
FloorPlacedFootprint,