120 lines
3.8 KiB
TypeScript

import type {
ChimneyMaterialRole,
ChimneyNode,
MaterialSchema,
PaintCapability,
} from '@pascal-app/core'
import { createMaterial, createMaterialFromPresetRef } from '@pascal-app/viewer'
import type { Material, Mesh } from 'three'
/**
* Resolve a chimney face click to its logical surface role.
*
* `holes.ts:partitionTopFaceGroups` partitions the chimney body
* mesh's material slots so:
* 0 = body
* 1 = top (the cap face)
* Faces outside any group (cricket mesh, which renders with a single
* material) fall back to 'body'.
*/
export function resolveChimneyRole(materialIndex: number | null): ChimneyMaterialRole {
return materialIndex === 1 ? 'top' : 'body'
}
export function buildChimneyMaterialPatch(
role: ChimneyMaterialRole,
material: MaterialSchema | undefined,
materialPreset: string | undefined,
): Partial<ChimneyNode> {
if (role === 'top') {
return { topMaterial: material, topMaterialPreset: materialPreset }
}
return { material, materialPreset }
}
export function getEffectiveChimneyMaterial(
node: ChimneyNode,
role: ChimneyMaterialRole,
): { material: MaterialSchema | undefined; materialPreset: string | undefined } {
if (role === 'top') {
const hasTop = node.topMaterial !== undefined || node.topMaterialPreset !== undefined
if (hasTop) {
return { material: node.topMaterial, materialPreset: node.topMaterialPreset }
}
}
return { material: node.material, materialPreset: node.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 chimney's mesh subtree for the
* given role. The body mesh uses a 2-slot material array (body =
* slot 0, top = slot 1) so paint-target → slot index is a single
* lookup. The cricket mesh uses a single material, which only the
* 'body' role paints.
*/
function applyChimneyPreview(
role: ChimneyMaterialRole,
previewMaterial: Material,
root: import('three').Object3D,
): (() => void) | null {
const restores: Array<() => void> = []
root.traverse((object) => {
const mesh = object as Mesh
if (!mesh.isMesh) return
const current = mesh.material as Material | Material[]
if (Array.isArray(current)) {
const idx = role === 'top' ? 1 : 0
const previousAtIdx = current[idx]
if (!previousAtIdx) return
const previousArray = [...current]
const nextArray = [...current]
nextArray[idx] = previewMaterial
mesh.material = nextArray
restores.push(() => {
mesh.material = previousArray
})
} else if (role === 'body') {
const previous = mesh.material
mesh.material = previewMaterial
restores.push(() => {
mesh.material = previous
})
}
})
if (restores.length === 0) return null
return () => {
for (let i = restores.length - 1; i >= 0; i -= 1) restores[i]?.()
}
}
/**
* Capability binding for the chimney kind. The editor's
* selection-manager invokes these in place of the legacy
* `if (node.type === 'chimney') { ... }` arm.
*/
export const chimneyPaint: PaintCapability = {
resolveRole: ({ materialIndex }) => resolveChimneyRole(materialIndex),
buildPatch: ({ role, material, materialPreset }) =>
buildChimneyMaterialPatch(role as ChimneyMaterialRole, material, materialPreset),
applyPreview: ({ role, material, materialPreset, root }) => {
const previewMaterial = buildPreviewMaterial(material, materialPreset)
if (!previewMaterial) return null
return applyChimneyPreview(role as ChimneyMaterialRole, previewMaterial, root)
},
getEffectiveMaterial: ({ node, role }) =>
getEffectiveChimneyMaterial(node as ChimneyNode, role as ChimneyMaterialRole),
}