121 lines
3.8 KiB
TypeScript

import type {
DormerNode,
DormerSurfaceMaterialRole,
MaterialSchema,
PaintCapability,
} from '@pascal-app/core'
import { getEffectiveDormerSurfaceMaterial } from '@pascal-app/core'
import { createMaterial, createMaterialFromPresetRef } from '@pascal-app/viewer'
import type { Material, Mesh } from 'three'
/**
* Resolve a dormer face click to its logical surface role.
*
* The dormer body mesh has 5 material slots (see
* `csg-geometry.ts:generateDormerGeometry`):
* 0 = wall (rectangular wall)
* 1 = side (deck along the slope)
* 2 = interior — paint as wall
* 3 = top (roof shingle)
* 4 = gable triangle — paint as wall
*
* Window-frame meshes are not in the body mesh; they're routed to
* 'side' separately by the editor when the click lands on them.
*/
export function resolveDormerRole(materialIndex: number | null): DormerSurfaceMaterialRole {
if (materialIndex === 3) return 'top'
if (materialIndex === 1) return 'side'
return 'wall'
}
export function buildDormerMaterialPatch(
role: DormerSurfaceMaterialRole,
material: MaterialSchema | undefined,
materialPreset: string | undefined,
): Partial<DormerNode> {
if (role === 'top') {
return { topMaterial: material, topMaterialPreset: materialPreset }
}
if (role === 'side') {
return { sideMaterial: material, sideMaterialPreset: materialPreset }
}
return { wallMaterial: material, wallMaterialPreset: materialPreset }
}
function buildPreviewMaterial(
material: MaterialSchema | undefined,
materialPreset: string | undefined,
): Material | null {
if (materialPreset) {
return createMaterialFromPresetRef(materialPreset)
}
if (material) {
return createMaterial(material)
}
return null
}
/**
* Apply a preview material to the dormer's `dormer-body` mesh at the
* slots that map to `role`:
* role='wall' → slots [0, 2, 4]
* role='side' → slot [1]
* role='top' → slot [3]
*/
function applyDormerPreview(
role: DormerSurfaceMaterialRole,
previewMaterial: Material,
root: import('three').Object3D,
): (() => void) | null {
const slotsToPaint = (() => {
if (role === 'top') return [3]
if (role === 'side') return [1]
return [0, 2, 4]
})()
const restores: Array<() => void> = []
root.traverse((object) => {
const mesh = object as Mesh
if (!mesh.isMesh) return
if (mesh.name !== 'dormer-body') return
const current = mesh.material as Material | Material[]
if (!Array.isArray(current)) return
const previousArray = [...current]
const nextArray = [...current]
for (const idx of slotsToPaint) {
if (current[idx]) nextArray[idx] = previewMaterial
}
mesh.material = nextArray
restores.push(() => {
mesh.material = previousArray
})
})
if (restores.length === 0) return null
return () => {
for (let i = restores.length - 1; i >= 0; i -= 1) restores[i]?.()
}
}
/**
* Capability binding for the dormer kind. The editor's
* selection-manager invokes these in place of the legacy
* `if (node.type === 'dormer') { ... }` arm.
*/
export const dormerPaint: PaintCapability = {
resolveRole: ({ materialIndex }) => resolveDormerRole(materialIndex),
buildPatch: ({ role, material, materialPreset }) =>
buildDormerMaterialPatch(role as DormerSurfaceMaterialRole, material, materialPreset),
applyPreview: ({ role, material, materialPreset, root }) => {
const previewMaterial = buildPreviewMaterial(material, materialPreset)
if (!previewMaterial) return null
return applyDormerPreview(role as DormerSurfaceMaterialRole, previewMaterial, root)
},
getEffectiveMaterial: ({ node, role }) => {
const spec = getEffectiveDormerSurfaceMaterial(
node as DormerNode,
role as DormerSurfaceMaterialRole,
)
return { material: spec.material, materialPreset: spec.materialPreset }
},
}