feat(paint-slots): round-2 feedback — fence 2 slots + UVs, roof defaults, stair body
- fence: split into two paint slots — panel (posts/base/infill, default charcoal) and rail (cap, default wood-finewood27) — as separate meshes with userData.slotId. Fix applyFenceUVs to continuous world-space 1 UV unit = 1 m (drop the per-part min origin that broke tiling across parts). New generateFenceSlotGeometries. - roof: real catalog defaults for the segment surfaces via getRoofMaterialArray (the actual default path): wall/trim concrete-plate (matches walls), deck + soffit soft-white, shingle terracotta; textures-off role escape hatch kept. Align nodes getRoofMaterials no-parent fallback to match. - stair: body slot default -> preset-lightgrey. - (biome formatting normalization of the round-1 merged renderer files rides along.) 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
ad227a04a2
commit
e92ee05702
@@ -5,30 +5,30 @@ import {
|
||||
createDefaultMaterial,
|
||||
createMaterial,
|
||||
createSurfaceRoleMaterial,
|
||||
generateFenceGeometry,
|
||||
generateFenceSlotGeometries,
|
||||
type RenderShading,
|
||||
resolveMaterialRef,
|
||||
resolveSlotDefaultMaterial,
|
||||
} from '@pascal-app/viewer'
|
||||
import { FrontSide, Group, type Material, Mesh, type Texture } from 'three'
|
||||
import type { FenceNode } from './schema'
|
||||
import { FENCE_SLOT_DEFAULT } from './slots'
|
||||
import { FENCE_PANEL_SLOT_DEFAULT, FENCE_RAIL_SLOT_DEFAULT, type FenceSlotId } from './slots'
|
||||
|
||||
/**
|
||||
* Stage B builder for fence. Reuses the legacy `generateFenceGeometry`
|
||||
* (pure function from viewer that returns a merged BufferGeometry of
|
||||
* posts + base + top rail + curve spans) and wraps it in a Mesh-in-Group
|
||||
* shape the generic `<GeometrySystem>` expects.
|
||||
* Stage B builder for fence. Splits the geometry into two paintable slots —
|
||||
* `panel` (posts / base / infill) and `rail` (the cap rail) — each its own Mesh
|
||||
* with a `userData.slotId` so the unified slot paint resolves and previews per
|
||||
* part.
|
||||
*
|
||||
* 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.
|
||||
* Per slot the material resolves: `node.slots[slotId]` (a shared scene material
|
||||
* or `library:` finish) → the legacy inline `node.material` / `materialPreset`
|
||||
* (pre-slot-model scenes, applied to both parts) → the declared slot default.
|
||||
* Textures-off collapses both parts to the themed joinery role.
|
||||
*
|
||||
* 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.
|
||||
* Phase 6 cleanup moves the 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 `generateFenceSlotGeometries` is publicly
|
||||
* re-exported from viewer.
|
||||
*/
|
||||
type FenceMaterial = Material & {
|
||||
alphaMap?: Texture | null
|
||||
@@ -37,10 +37,16 @@ type FenceMaterial = Material & {
|
||||
transparent: boolean
|
||||
}
|
||||
|
||||
const SLOT_DEFAULTS: Record<FenceSlotId, string> = {
|
||||
panel: FENCE_PANEL_SLOT_DEFAULT,
|
||||
rail: FENCE_RAIL_SLOT_DEFAULT,
|
||||
}
|
||||
|
||||
const fenceMaterialCache = new Map<string, Material>()
|
||||
|
||||
function getFenceMaterial(
|
||||
function getFenceSlotMaterial(
|
||||
node: FenceNode,
|
||||
slotId: FenceSlotId,
|
||||
shading: RenderShading,
|
||||
textures: boolean,
|
||||
colorPreset: ColorPreset,
|
||||
@@ -51,7 +57,7 @@ function getFenceMaterial(
|
||||
return createSurfaceRoleMaterial('joinery', colorPreset, FrontSide, sceneTheme)
|
||||
}
|
||||
|
||||
const slotRef = node.slots?.surface
|
||||
const slotRef = node.slots?.[slotId]
|
||||
if (slotRef) {
|
||||
const resolved = resolveMaterialRef(slotRef, sceneMaterials, shading)
|
||||
if (resolved) return resolved
|
||||
@@ -61,7 +67,7 @@ function getFenceMaterial(
|
||||
return getLegacyFenceMaterial(node, shading)
|
||||
}
|
||||
|
||||
return resolveSlotDefaultMaterial(FENCE_SLOT_DEFAULT, shading, 0.8)
|
||||
return resolveSlotDefaultMaterial(SLOT_DEFAULTS[slotId], shading, 0.8)
|
||||
}
|
||||
|
||||
function getLegacyFenceMaterial(node: FenceNode, shading: RenderShading): Material {
|
||||
@@ -105,13 +111,26 @@ export function buildFenceGeometry(
|
||||
sceneTheme?: string,
|
||||
): Group {
|
||||
const group = new Group()
|
||||
const geometry = generateFenceGeometry(node)
|
||||
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)
|
||||
const geometries = generateFenceSlotGeometries(node)
|
||||
|
||||
for (const slotId of ['panel', 'rail'] as const) {
|
||||
const geometry = geometries[slotId]
|
||||
if (geometry.getAttribute('position') === undefined) continue
|
||||
const material = getFenceSlotMaterial(
|
||||
node,
|
||||
slotId,
|
||||
shading,
|
||||
textures,
|
||||
colorPreset,
|
||||
sceneTheme,
|
||||
ctx?.materials,
|
||||
)
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.castShadow = true
|
||||
mesh.receiveShadow = true
|
||||
mesh.userData.slotId = slotId
|
||||
group.add(mesh)
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user