feat(paint-slots): migrate fence onto the unified slot model
Fence joins the node.slots paint model with one paintable 'surface' slot (merged post/base/rail geometry is a single mesh). Resolution order matches slab: textures-off joinery role -> node.slots.surface ref -> legacy inline material/preset -> declared default (library:wood-finewood27). Tags the mesh userData.slotId='surface' and wires capabilities.slots + paint. Stops borrowing DEFAULT_STAIR_MATERIAL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a8f32dd47a
commit
5555ad1fe0
@@ -1,10 +1,18 @@
|
||||
import { type GeometryContext, getMaterialPresetByRef } from '@pascal-app/core'
|
||||
import {
|
||||
DEFAULT_STAIR_MATERIAL,
|
||||
applyMaterialPresetToMaterials,
|
||||
type ColorPreset,
|
||||
createDefaultMaterial,
|
||||
createMaterial,
|
||||
createSurfaceRoleMaterial,
|
||||
generateFenceGeometry,
|
||||
type RenderShading,
|
||||
resolveMaterialRef,
|
||||
resolveSlotDefaultMaterial,
|
||||
} from '@pascal-app/viewer'
|
||||
import { Group, Mesh } from 'three'
|
||||
import { FrontSide, Group, type Material, Mesh, type Texture } from 'three'
|
||||
import type { FenceNode } from './schema'
|
||||
import { FENCE_SLOT_DEFAULT } from './slots'
|
||||
|
||||
/**
|
||||
* Stage B builder for fence. Reuses the legacy `generateFenceGeometry`
|
||||
@@ -12,26 +20,98 @@ import type { FenceNode } from './schema'
|
||||
* posts + base + top rail + curve spans) and wraps it in a Mesh-in-Group
|
||||
* shape the generic `<GeometrySystem>` expects.
|
||||
*
|
||||
* Material is a single shared reference — fences look the same regardless
|
||||
* of instance, so we don't clone per node. If per-fence material
|
||||
* customization lands later (color picker on the panel maps to a real
|
||||
* material), this becomes a per-node lookup.
|
||||
* Materials follow the unified slot model: the single `surface` slot resolves
|
||||
* `node.slots.surface` (a shared scene material or `library:` finish) → the
|
||||
* legacy inline `node.material` / `materialPreset` (pre-slot-model scenes) →
|
||||
* the declared slot default. Textures-off collapses to themed joinery.
|
||||
*
|
||||
* Phase 6 cleanup moves the 280 lines of geometry math out of the
|
||||
* legacy `viewer/src/systems/fence/fence-system.tsx` into this folder
|
||||
* once the legacy system file is deleted. Until then `generateFenceGeometry`
|
||||
* is publicly re-exported from viewer.
|
||||
*/
|
||||
type FenceMaterial = Material & {
|
||||
alphaMap?: Texture | null
|
||||
depthWrite: boolean
|
||||
opacity: number
|
||||
transparent: boolean
|
||||
}
|
||||
|
||||
const fenceMaterialCache = new Map<string, Material>()
|
||||
|
||||
function getFenceMaterial(
|
||||
node: FenceNode,
|
||||
shading: RenderShading,
|
||||
textures: boolean,
|
||||
colorPreset: ColorPreset,
|
||||
sceneTheme: string | undefined,
|
||||
sceneMaterials: GeometryContext['materials'],
|
||||
): Material {
|
||||
if (!textures) {
|
||||
return createSurfaceRoleMaterial('joinery', colorPreset, FrontSide, sceneTheme)
|
||||
}
|
||||
|
||||
const slotRef = node.slots?.surface
|
||||
if (slotRef) {
|
||||
const resolved = resolveMaterialRef(slotRef, sceneMaterials, shading)
|
||||
if (resolved) return resolved
|
||||
}
|
||||
|
||||
if (node.materialPreset || node.material) {
|
||||
return getLegacyFenceMaterial(node, shading)
|
||||
}
|
||||
|
||||
return resolveSlotDefaultMaterial(FENCE_SLOT_DEFAULT, shading, 0.8)
|
||||
}
|
||||
|
||||
function getLegacyFenceMaterial(node: FenceNode, shading: RenderShading): Material {
|
||||
const cacheKey = JSON.stringify({
|
||||
shading,
|
||||
material: node.material ?? null,
|
||||
materialPreset: node.materialPreset ?? null,
|
||||
})
|
||||
const cached = fenceMaterialCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
const preset = getMaterialPresetByRef(node.materialPreset)
|
||||
const material = preset
|
||||
? createDefaultMaterial('#ffffff', 0.5, shading)
|
||||
: node.material
|
||||
? createMaterial(node.material, shading).clone()
|
||||
: createDefaultMaterial('#ffffff', 0.9, shading)
|
||||
|
||||
if (preset) {
|
||||
applyMaterialPresetToMaterials(material, preset)
|
||||
}
|
||||
|
||||
const fenceMaterial = material as FenceMaterial
|
||||
fenceMaterial.transparent = false
|
||||
fenceMaterial.opacity = 1
|
||||
fenceMaterial.alphaMap = null
|
||||
fenceMaterial.side = FrontSide
|
||||
fenceMaterial.depthWrite = true
|
||||
fenceMaterial.needsUpdate = true
|
||||
|
||||
fenceMaterialCache.set(cacheKey, material)
|
||||
return material
|
||||
}
|
||||
|
||||
export function buildFenceGeometry(
|
||||
node: FenceNode,
|
||||
_ctx?: unknown,
|
||||
ctx?: GeometryContext,
|
||||
shading: RenderShading = 'rendered',
|
||||
textures = true,
|
||||
colorPreset: ColorPreset = 'clay',
|
||||
sceneTheme?: string,
|
||||
): Group {
|
||||
const group = new Group()
|
||||
const geometry = generateFenceGeometry(node)
|
||||
const mesh = new Mesh(geometry, DEFAULT_STAIR_MATERIAL(shading))
|
||||
const material = getFenceMaterial(node, shading, textures, colorPreset, sceneTheme, ctx?.materials)
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.castShadow = true
|
||||
mesh.receiveShadow = true
|
||||
mesh.userData.slotId = 'surface'
|
||||
mesh.userData.surfaceRole = 'joinery'
|
||||
group.add(mesh)
|
||||
return group
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user