107 lines
2.8 KiB
TypeScript
107 lines
2.8 KiB
TypeScript
'use client'
|
|
|
|
import {
|
|
type ChimneyNode,
|
|
type RoofSegmentNode,
|
|
RoofSegmentNode as RoofSegmentSchema,
|
|
} from '@pascal-app/core'
|
|
import { useEffect, useMemo } from 'react'
|
|
import * as THREE from 'three'
|
|
import { INVALID_GHOST_COLOR } from '../shared/ghost-materials'
|
|
import { buildChimneyGeometry } from './geometry'
|
|
|
|
const ghostMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0xff_ff_ff,
|
|
emissive: 0xff_ff_ff,
|
|
emissiveIntensity: 0.12,
|
|
roughness: 0.85,
|
|
transparent: true,
|
|
opacity: 0.55,
|
|
depthWrite: false,
|
|
})
|
|
|
|
const invalidGhostMaterial = new THREE.MeshStandardMaterial({
|
|
color: INVALID_GHOST_COLOR,
|
|
emissive: INVALID_GHOST_COLOR,
|
|
emissiveIntensity: 0.12,
|
|
roughness: 0.85,
|
|
transparent: true,
|
|
opacity: 0.4,
|
|
depthWrite: false,
|
|
})
|
|
|
|
/**
|
|
* The preview needs a segment fixture to build the body height. The
|
|
* placement tool passes the segment under the cursor; when floating
|
|
* (off-roof fallback), segment is absent — build against RoofSegmentNode
|
|
* defaults so the ghost renders flat at the grid position with yaw 0.
|
|
*/
|
|
const ChimneyPreview = ({
|
|
node,
|
|
segment,
|
|
invalid,
|
|
}: {
|
|
node: ChimneyNode
|
|
segment?: RoofSegmentNode
|
|
invalid?: boolean
|
|
}) => {
|
|
const material = invalid ? invalidGhostMaterial : ghostMaterial
|
|
const effectiveSegment = segment ?? RoofSegmentSchema.parse({})
|
|
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: deps deliberately list the build inputs; depending on the whole object would rebuild on unrelated field changes.
|
|
const geo = useMemo(
|
|
() => buildChimneyGeometry(node, effectiveSegment),
|
|
[
|
|
effectiveSegment.wallHeight,
|
|
effectiveSegment.pitch,
|
|
effectiveSegment.roofType,
|
|
effectiveSegment.width,
|
|
effectiveSegment.depth,
|
|
node.width,
|
|
node.depth,
|
|
node.heightAboveRidge,
|
|
node.bodyShape,
|
|
node.shoulderStyle,
|
|
node.shoulderHeight,
|
|
node.shoulderExtent,
|
|
node.cap,
|
|
node.capShape,
|
|
node.capOverhang,
|
|
node.capThickness,
|
|
node.flueCount,
|
|
node.flueShape,
|
|
node.flueHeight,
|
|
node.flueDiameter,
|
|
node.flueSpacing,
|
|
node.cricketStyle,
|
|
node.cricketSide,
|
|
node.cricketLength,
|
|
node.cricketHeight,
|
|
node.position[0],
|
|
node.position[2],
|
|
node.rotation,
|
|
],
|
|
)
|
|
|
|
useEffect(
|
|
() => () => {
|
|
geo.body.dispose()
|
|
geo.cap?.dispose()
|
|
geo.flues?.dispose()
|
|
geo.cricket?.dispose()
|
|
},
|
|
[geo],
|
|
)
|
|
|
|
return (
|
|
<group>
|
|
<mesh geometry={geo.body} material={material} raycast={() => {}} />
|
|
{geo.cap && <mesh geometry={geo.cap} material={material} raycast={() => {}} />}
|
|
{geo.flues && <mesh geometry={geo.flues} material={material} raycast={() => {}} />}
|
|
{geo.cricket && <mesh geometry={geo.cricket} material={material} raycast={() => {}} />}
|
|
</group>
|
|
)
|
|
}
|
|
|
|
export default ChimneyPreview
|