109 lines
3.2 KiB
TypeScript

import type {
AnyNodeId,
EyebrowVentNode,
FloorplanGeometry,
FloorplanPoint,
GeometryContext,
RoofNode,
RoofSegmentNode,
} from '@pascal-app/core'
/**
* Floor-plan builder for an eyebrow vent — seen from above it reads as the
* lens (eye-shaped) footprint of the hood, with a faint centre seam. The
* coordinate frame mirrors the 3D transform stack (roof → roof-segment →
* vent), same as the box-vent / cupola builders.
*/
export function buildEyebrowVentFloorplan(
node: EyebrowVentNode,
ctx: GeometryContext,
): FloorplanGeometry | null {
const segment = ctx.parent as RoofSegmentNode | null
if (segment?.type !== 'roof-segment') return null
const roofId = segment.parentId as AnyNodeId | null
const roof = roofId ? (ctx.resolve(roofId) as RoofNode | undefined) : undefined
if (roof?.type !== 'roof') return null
const cosR = Math.cos(-roof.rotation)
const sinR = Math.sin(-roof.rotation)
const segCx = roof.position[0] + segment.position[0] * cosR - segment.position[2] * sinR
const segCz = roof.position[2] + segment.position[0] * sinR + segment.position[2] * cosR
const segRot = -(roof.rotation + segment.rotation)
const cosS = Math.cos(segRot)
const sinS = Math.sin(segRot)
const cx = segCx + node.position[0] * cosS - node.position[2] * sinS
const cz = segCz + node.position[0] * sinS + node.position[2] * cosS
const rot = -(roof.rotation + segment.rotation + node.rotation)
const cos = Math.cos(rot)
const sin = Math.sin(rot)
const toPlan = (lx: number, lz: number): FloorplanPoint => [
cx + lx * cos - lz * sin,
cz + lx * sin + lz * cos,
]
const view = ctx.viewState
const palette = view?.palette
const isSelected = view?.selected ?? false
const isHighlighted = view?.highlighted ?? false
const isHovered = view?.hovered ?? false
const showSelectedChrome = isSelected || isHighlighted
const baseInk = '#475569'
const stroke =
showSelectedChrome && palette
? palette.selectedStroke
: isHovered && palette
? palette.wallHoverStroke
: baseInk
const fill = showSelectedChrome ? '#fed7aa' : '#dbe1e8'
const fillOpacity = showSelectedChrome ? 0.55 : 0.7
const lineWidth = showSelectedChrome ? 0.03 : 0.02
const hw = Math.max(node.width, 0.1) / 2
const hd = Math.max(node.depth, 0.1) / 2
const rect = (halfX: number, halfZ: number): FloorplanPoint[] => [
toPlan(-halfX, -halfZ),
toPlan(halfX, -halfZ),
toPlan(halfX, halfZ),
toPlan(-halfX, halfZ),
]
const children: FloorplanGeometry[] = []
// Hood footprint outline + hit target (no flashing plate).
const hood = rect(hw, hd)
children.push({
kind: 'polygon',
points: hood,
fill: stroke,
fillOpacity: 0,
stroke: 'none',
strokeWidth: 0,
pointerEvents: 'all',
})
children.push({
kind: 'polygon',
points: hood,
fill,
fillOpacity,
stroke,
strokeWidth: lineWidth,
strokeLinejoin: 'round',
pointerEvents: 'none',
})
// Front edge (the louvered opening faces +Z).
children.push({
kind: 'polyline',
points: [toPlan(-hw, hd), toPlan(hw, hd)],
stroke,
strokeWidth: lineWidth,
strokeOpacity: 0.9,
pointerEvents: 'none',
})
return { kind: 'group', children }
}