Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions wiki/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Canonical rules for code that touches `packages/core`, `packages/viewer`, `packa
| [renderers](renderers.md) | Node renderer pattern in `packages/viewer` |
| [node-definitions](node-definitions.md) | Three-checkbox composition model for registry-driven kinds (`geometry` / `renderer` / `system`) |
| [materials-and-themes](materials-and-themes.md) | Surface colour: surface roles, colour presets, the textures axis, and scene themes (appearance / ground / clay tints) |
| [item-authoring](item-authoring.md) | Content-author contract for catalog item GLBs: `slot_` material naming, authored defaults + `pascal_material` extras, the `cutout` reserved mesh, UV world scale, and the validated Blender/export recipe |
| [plugin-authoring](plugin-authoring.md) | Public contract for external plugins — `Plugin` shape, `setPluginDiscovery`, lifecycle, what's in and out of v1 |
| [tools](tools.md) | Editor tools structure, 2D↔3D behavioral parity, manipulation constraints, and Shift bypass defaults |
| [viewer-isolation](viewer-isolation.md) | Keeping `@pascal-app/viewer` editor-agnostic |
Expand Down
87 changes: 87 additions & 0 deletions wiki/architecture/item-authoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Item authoring (paintable GLBs)

How to author a catalog item GLB so the editor keeps its materials as the default
appearance **and** exposes named parts as paintable **slots**. This is the content-author
contract for the file format; the runtime side (how a slot's colour is resolved and
re-applied) lives in [materials-and-themes](materials-and-themes.md), and procedural
slot declarations live in [node-definitions](node-definitions.md).

The whole convention is opt-in by material name — an item with no slot materials renders
its authored/baked look untouched and exposes nothing to paint.

## The slot contract

A glTF **material** whose name starts with `slot_` (case-insensitive) marks a paintable
part. The canonical rules live in `packages/core/src/lib/slots.ts` and are shared by both
the upload scan and the renderer, so authored names, stored slot metadata, and runtime
meshes can never drift:

- **One material per paintable part, named `slot_<part>`.** e.g. `slot_frame`,
`slot_seat`, `slot_bed_frame`.
- **The slot id is derived** by `deriveSlotId`: strip the `slot_` prefix, drop a Blender
numeric-dedupe suffix (`.001`), lowercase the rest. So `slot_Bed_Frame` and
`slot_bed_frame.001` both resolve to the slot `bed_frame`. Splitting one logical part
across several Blender materials that dedupe to the same id **merges** them into a
single slot — intentional, so a part split for modelling reasons still paints as one.
- **Unmarked materials stay authored, forever.** A material without the `slot_` prefix
renders exactly as authored and exposes no slot. This is the right choice for fixed
labels, decals, signage, and baked detail (e.g. a fire-alarm sign, an AC front panel).
- **Paintable by naming alone.** A solid-colour slot samples no UVs, so a flat-colour
item becomes fully paintable just by renaming its materials — no re-unwrap needed.

## Default appearance

The default look of a slot is **the authored material's own data** (its
`baseColorFactor` / maps), never encoded in the slot name. Painting a slot overrides that
default; resetting returns to it.

Curated cross-item defaults (e.g. "this seat defaults to the catalog linen") travel
**with the asset** as `pascal_material` glTF material extras, read at runtime from
`material.userData.pascal_material`. They're optional — with none present, the authored
material is the default.

## Reserved names

- **`cutout`** — a **mesh** (not a material) named `cutout` is treated as a boolean-cut
helper: it is hidden at runtime and never becomes a slot or a visible surface. Use it
for the negative volume a host opening subtracts, not for geometry you want shown.

## UV world scale (~1 unit per metre)

Tileable finishes assume the same world-scale UV contract as procedural surfaces: **1 UV
unit = 1 m** (see [materials-and-themes](materials-and-themes.md) → *Texture world
scale*). This is an **authoring requirement, not a render-time correction** — the slot
validator's UV-presence check flags slots that need UVs and don't have them. Flat-colour
slots need no UVs at all.

For fixed multi-colour detail *within* one slot, bake it into **vertex colours**: painting
swaps the slot's material but vertex colours ride along, so a two-tone part stays two-tone
under any finish.

## Blender recipe (validated)

1. **Apply scale** — `Ctrl+A` → Scale, so 1 Blender unit exports as 1 m and the UV
world-scale promise holds.
2. **Rename materials** to `slot_<part>` for each paintable part.
3. **Hard-surface slots** — UV → **Cube Projection** with **Cube Size = 1.0**. This gives
exactly 1 UV unit/m by construction; overlapping islands are fine because finishes tile
and nothing is baked.
4. **Curved / soft slots** — use the **Texel Density** addon at **10.24 px/cm @ 1024 px**.
5. **Check** with a UV grid texture before exporting.

## Export (critical)

- **Custom Properties must be enabled.** In the Blender glTF exporter, turn on
*Include → Custom Properties*, or `pascal_material` extras never reach the GLB and
curated defaults are silently lost.
- **Preserve extras through optimisation.** Run an extras-preserving optimiser — gltfpack
with `-ke` (keep extras). Author-side extras are read once at upload, so any stripping
between export and upload loses them; the validator warns when extras look stripped.

## How the editor reads the result

On load, the renderer keeps the GLB's authored materials and, for every material whose
name derives a slot id, captures that slot on the instance so the paint tool can target
`(nodeId, slotId)`. Items authored with no `slot_` materials simply render their authored
look and expose no slots — there is no separate "mode" to set; the behaviour follows from
the material names in the file.
2 changes: 1 addition & 1 deletion wiki/architecture/materials-and-themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Every procedural surface generates UVs in metres: 1 UV unit = 1 m.

This contract is shared by wall `systems/wall/wall-system.tsx` (`ExtrudeGeometry`), slab `systems/slab/slab-system.tsx` (`generatePositiveSlabGeometry`, and `generatePoolGeometry`), ceiling `systems/ceiling/ceiling-system.tsx`, roof `systems/roof/roof-system.tsx`, and chimney/dormer `nodes/src/chimney/geometry.ts`.

GLB item slots follow the same ~1 UV unit/m authoring convention, enforced by the slot validator's UV-presence check and the phase-6 Blender recipe. This is an authoring requirement, not a render-time correction.
GLB item slots follow the same ~1 UV unit/m authoring convention, enforced by the slot validator's UV-presence check and the Blender recipe in [item-authoring](item-authoring.md). This is an authoring requirement, not a render-time correction.

A catalog material's `repeat` (`mapProperties.repeatX/repeatY` in `packages/core/src/material-library.ts`) is therefore a per-material world-scale setting: tiles per metre.

Expand Down
Loading