The materials module wraps every applicable
model!procgen.paintMaterials | procgen/paintMaterials painter into a
lookup table the host can drive from a context menu or button bar.
Per-(SceneModel, painter) SceneMaterials
are created on first use and shared across every mesh that adopts
them. Applying the same painter to a thousand objects produces
one material, not a thousand.
Material swapping uses the SDK's supported mutation pattern:
snapshot the mesh's params (id, geometryId, matrix, opacity,
parentTransform), detach + destroy the old mesh, then create a
fresh mesh with the same id bound to the palette's
SceneMaterial and re-attach to the
source SceneObject. The object keeps
its identity; only the mesh's material binding changes.
Shape
The palette sits over a small cast of types: a MaterialsPalette
owns a list of PainterCatalogEntry entries, delegates the actual
texture work to the procgen painters in
procgen!materials.paintMaterials | paintMaterials, and reuses each
resulting SceneMaterial through an
internal per-SceneModel cache.
%%{init:{"theme":"dark"}}%%
classDiagram
direction TB
class MaterialsPalette {
+catalog : PainterCatalogEntry[]
+textureSize : number
+uvScale : number
+painterIds() string[]
+getEntry(id) PainterCatalogEntry
+paintMaterial(mesh, id) SDKResult
}
class PainterCatalogEntry {
+id : string
+label : string
+category : string
+paint(size) MaterialMaps
+material : MaterialOverrides
}
class MatCache {
<<internal>>
+get(model, id) SceneMaterial
}
class SceneMesh {
<<scene>>
}
class SceneMaterial {
<<scene>>
}
class paintMaterials {
<<procgen>>
}
MaterialsPalette "1" *-- "*" PainterCatalogEntry : catalog
MaterialsPalette "1" *-- "1" MatCache : reuses materials
MaterialsPalette ..> paintMaterials : invokes painters
MaterialsPalette ..> SceneMesh : repaints
MatCache ..> SceneMaterial : shared per SceneModel + painterId
%%{init:{"theme":"default"}}%%
classDiagram
direction TB
class MaterialsPalette {
+catalog : PainterCatalogEntry[]
+textureSize : number
+uvScale : number
+painterIds() string[]
+getEntry(id) PainterCatalogEntry
+paintMaterial(mesh, id) SDKResult
}
class PainterCatalogEntry {
+id : string
+label : string
+category : string
+paint(size) MaterialMaps
+material : MaterialOverrides
}
class MatCache {
<<internal>>
+get(model, id) SceneMaterial
}
class SceneMesh {
<<scene>>
}
class SceneMaterial {
<<scene>>
}
class paintMaterials {
<<procgen>>
}
MaterialsPalette "1" *-- "*" PainterCatalogEntry : catalog
MaterialsPalette "1" *-- "1" MatCache : reuses materials
MaterialsPalette ..> paintMaterials : invokes painters
MaterialsPalette ..> SceneMesh : repaints
MatCache ..> SceneMaterial : shared per SceneModel + painterId
classDiagram
direction TB
class MaterialsPalette {
+catalog : PainterCatalogEntry[]
+textureSize : number
+uvScale : number
+painterIds() string[]
+getEntry(id) PainterCatalogEntry
+paintMaterial(mesh, id) SDKResult
}
class PainterCatalogEntry {
+id : string
+label : string
+category : string
+paint(size) MaterialMaps
+material : MaterialOverrides
}
class MatCache {
<<internal>>
+get(model, id) SceneMaterial
}
class SceneMesh {
<<scene>>
}
class SceneMaterial {
<<scene>>
}
class paintMaterials {
<<procgen>>
}
MaterialsPalette "1" *-- "*" PainterCatalogEntry : catalog
MaterialsPalette "1" *-- "1" MatCache : reuses materials
MaterialsPalette ..> paintMaterials : invokes painters
MaterialsPalette ..> SceneMesh : repaints
MatCache ..> SceneMaterial : shared per SceneModel + painterId
Mesh repaint sequence
A repaint call routes through a catalog lookup, an optional first-time
material build (painter → textures → material, cached), and the SDK's
supported mutation pattern for swapping a mesh's material binding —
snapshot the mesh's params, destroy the old mesh, then recreate it
with the same id and re-attach it to its SceneObject.
%%{init:{"theme":"dark"}}%%
flowchart TD
A[paintMaterial mesh painterId] --> B[lookup entry by id]
B --> C{material cached for SceneModel?}
C -- no --> D[call painter → MaterialMaps]
D --> E[create SceneTextures]
E --> F[create SceneMaterial — cache as materialId]
C -- yes --> G[reuse cached materialId]
F --> H[snapshot mesh params]
G --> H
H --> I[detach + destroy old mesh]
I --> J[create new mesh — same id, new materialId]
J --> K[re-attach to SceneObject]
%%{init:{"theme":"default"}}%%
flowchart TD
A[paintMaterial mesh painterId] --> B[lookup entry by id]
B --> C{material cached for SceneModel?}
C -- no --> D[call painter → MaterialMaps]
D --> E[create SceneTextures]
E --> F[create SceneMaterial — cache as materialId]
C -- yes --> G[reuse cached materialId]
F --> H[snapshot mesh params]
G --> H
H --> I[detach + destroy old mesh]
I --> J[create new mesh — same id, new materialId]
J --> K[re-attach to SceneObject]
flowchart TD
A[paintMaterial mesh painterId] --> B[lookup entry by id]
B --> C{material cached for SceneModel?}
C -- no --> D[call painter → MaterialMaps]
D --> E[create SceneTextures]
E --> F[create SceneMaterial — cache as materialId]
C -- yes --> G[reuse cached materialId]
F --> H[snapshot mesh params]
G --> H
H --> I[detach + destroy old mesh]
I --> J[create new mesh — same id, new materialId]
J --> K[re-attach to SceneObject]
Behavior
Default catalog — every applicable
model!procgen.paintMaterials | procgen/paintMaterials painter is
pre-registered (brick, concrete, marble, oak, polished steel, copper,
gold, glass, etc.) and grouped into categories.
Per-(SceneModel, painter) sharing — repainting a thousand
meshes with the same painter creates one
SceneMaterial, not a thousand.
Materials are cached in a WeakMap keyed by SceneModel, so a
destroyed SceneModel drops its entries without explicit
cleanup.
Hatch passthrough — each catalog entry carries the painter's
engineering hatch pattern (ANSI 32 steel, ANSI 36 brick, ISO
concrete, etc.); the painted material binds the hatch into
hatchPattern, so section caps and Detailed-mode body shading
pick it up automatically.
Transparent dielectrics — glass and other transparent
painters carry opacity / alphaMode: "BLEND" in their entry,
so no per-call override is required.
Triplanar by default — painted materials forward
uvScale as triplanarScale for
UV-less geometry.
In-place repaint — paintMaterial keeps the source
SceneObject intact; only the mesh's
material binding changes, via the SDK's supported
detach + destroy + recreate + reattach pattern.
Custom catalogs — pass catalog to the constructor to replace
the default with host-defined painter entries.
With no params, the palette uses the default catalog.
constpalette = newMaterialsPalette();
Or override texture size and UV scale:
constpalette = newMaterialsPalette({ textureSize:512, // sharper close-up texturing uvScale:0.5// tile half as often (larger pattern) });
3) Repaint one mesh
Pass a SceneMesh and a painter id. The
returned SDKResult carries the new (replacement) SceneMesh on
success.
constresult = palette.paintMaterial(mesh, "brick"); if (!result.ok) console.warn(result.error);
4) Repaint every mesh under a SceneObject
paintMaterial returns a fresh SceneMesh each call; iterate the
SceneObject's meshes array once via a snapshot so the loop
isn't disturbed by the in-flight detach + reattach.
for (constmof [...sceneObject.meshes]) { constr = palette.paintMaterial(m, "polSteel"); if (!r.ok) console.warn(r.error); }
5) Drive from a UI menu
painterIds returns the
registered ids in catalog order; getEntry
looks up an entry for label / category metadata. Together they're
enough to build a menu grouped by category.
The palette is stateless across SceneModels — its material cache
is a WeakMap, so destroying a SceneModel automatically drops its
cached materials. The palette itself has no destroy method;
drop the reference when you're done.
Materials Palette
The
materialsmodule wraps every applicable model!procgen.paintMaterials | procgen/paintMaterials painter into a lookup table the host can drive from a context menu or button bar. Per-(SceneModel, painter) SceneMaterials are created on first use and shared across every mesh that adopts them. Applying the same painter to a thousand objects produces one material, not a thousand.Material swapping uses the SDK's supported mutation pattern: snapshot the mesh's params (id, geometryId, matrix, opacity, parentTransform), detach + destroy the old mesh, then create a fresh mesh with the same id bound to the palette's SceneMaterial and re-attach to the source SceneObject. The object keeps its identity; only the mesh's material binding changes.
Shape
The palette sits over a small cast of types: a MaterialsPalette owns a list of PainterCatalogEntry entries, delegates the actual texture work to the procgen painters in procgen!materials.paintMaterials | paintMaterials, and reuses each resulting SceneMaterial through an internal per-
SceneModelcache.Mesh repaint sequence
A repaint call routes through a catalog lookup, an optional first-time material build (painter → textures → material, cached), and the SDK's supported mutation pattern for swapping a mesh's material binding — snapshot the mesh's params, destroy the old mesh, then recreate it with the same id and re-attach it to its SceneObject.
Behavior
hatchPattern, so section caps and Detailed-mode body shading pick it up automatically.opacity/alphaMode: "BLEND"in their entry, so no per-call override is required.triplanarScalefor UV-less geometry.paintMaterialkeeps the source SceneObject intact; only the mesh's material binding changes, via the SDK's supported detach + destroy + recreate + reattach pattern.catalogto the constructor to replace the default with host-defined painter entries.Usage
1) Import the entry points
2) Construct the palette
With no params, the palette uses the default catalog.
Or override texture size and UV scale:
3) Repaint one mesh
Pass a SceneMesh and a painter id. The returned
SDKResultcarries the new (replacement) SceneMesh on success.4) Repaint every mesh under a SceneObject
paintMaterialreturns a fresh SceneMesh each call; iterate the SceneObject'smeshesarray once via a snapshot so the loop isn't disturbed by the in-flight detach + reattach.5) Drive from a UI menu
painterIds returns the registered ids in catalog order; getEntry looks up an entry for label / category metadata. Together they're enough to build a menu grouped by category.
6) Custom catalog
Pass a catalog to replace the default.
7) Lifetime
The palette is stateless across SceneModels — its material cache is a WeakMap, so destroying a SceneModel automatically drops its cached materials. The palette itself has no
destroymethod; drop the reference when you're done.