ai-chatbot/packages/editor/src/lib/zone-content.ts
Wassim SAMAD 50304d7d10
Support template scans and room preset workflows (#390)
* Allow template forks to preserve scans

* Add room preset save trigger

* Show room preset action for selected zones

* Use standard inspector for selected zones

* Support room preset editor workflows

* Preview room clear-underneath items
2026-06-09 16:36:31 -04:00

131 lines
4.6 KiB
TypeScript

import {
type AnyNode,
type AnyNodeId,
type CeilingNode,
type ItemNode,
pointInPolygon2D,
pointOnSegment,
type SlabNode,
type WallNode,
type ZoneNode,
} from '@pascal-app/core'
type Point2D = [number, number]
const POINT_TOLERANCE = 0.5
const COLLINEAR_TOLERANCE = 1e-6
const SURFACE_POLYGON_TOLERANCE = 0.15
function getPointToSegmentDistance(point: Point2D, start: Point2D, end: Point2D): number {
const dx = end[0] - start[0]
const dz = end[1] - start[1]
const lengthSq = dx * dx + dz * dz
if (lengthSq === 0) return Math.hypot(point[0] - start[0], point[1] - start[1])
const rawT = ((point[0] - start[0]) * dx + (point[1] - start[1]) * dz) / lengthSq
const t = Math.max(0, Math.min(1, rawT))
const projected: Point2D = [start[0] + t * dx, start[1] + t * dz]
return Math.hypot(point[0] - projected[0], point[1] - projected[1])
}
function pointInPolygonWithTolerance(point: Point2D, polygon: Point2D[]): boolean {
if (pointInPolygon2D(point, polygon, { includeBoundary: true })) return true
return polygon.some((start, index) => {
const end = polygon[(index + 1) % polygon.length]
return end ? getPointToSegmentDistance(point, start, end) <= POINT_TOLERANCE : false
})
}
function polygonContainsWithTolerance(
outer: Point2D[],
inner: Point2D[],
tolerance: number,
): boolean {
return inner.every((point) => {
if (pointInPolygon2D(point, outer, { includeBoundary: true })) return true
return outer.some((start, index) => {
const end = outer[(index + 1) % outer.length]
return end ? getPointToSegmentDistance(point, start, end) <= tolerance : false
})
})
}
function polygonMatchesZoneFootprint(surfacePolygon: Point2D[], footprint: Point2D[]): boolean {
if (surfacePolygon.length < 3) return false
return (
polygonContainsWithTolerance(footprint, surfacePolygon, SURFACE_POLYGON_TOLERANCE) &&
polygonContainsWithTolerance(surfacePolygon, footprint, SURFACE_POLYGON_TOLERANCE)
)
}
function areSegmentsCollinear(a: Point2D, b: Point2D, c: Point2D, d: Point2D): boolean {
const abx = b[0] - a[0]
const abz = b[1] - a[1]
const acx = c[0] - a[0]
const acz = c[1] - a[1]
const adx = d[0] - a[0]
const adz = d[1] - a[1]
const crossC = abx * acz - abz * acx
const crossD = abx * adz - abz * adx
return Math.abs(crossC) <= COLLINEAR_TOLERANCE && Math.abs(crossD) <= COLLINEAR_TOLERANCE
}
function segmentsOverlap(a: Point2D, b: Point2D, c: Point2D, d: Point2D): boolean {
const useX = Math.abs(b[0] - a[0]) >= Math.abs(b[1] - a[1])
const a0 = useX ? a[0] : a[1]
const a1 = useX ? b[0] : b[1]
const c0 = useX ? c[0] : c[1]
const c1 = useX ? d[0] : d[1]
const minA = Math.min(a0, a1)
const maxA = Math.max(a0, a1)
const minC = Math.min(c0, c1)
const maxC = Math.max(c0, c1)
return Math.max(minA, minC) <= Math.min(maxA, maxC) + COLLINEAR_TOLERANCE
}
function wallLiesOnZoneBoundary(wall: WallNode, polygon: Point2D[]): boolean {
return polygon.some((start, index) => {
const end = polygon[(index + 1) % polygon.length]
if (!end) return false
return (
areSegmentsCollinear(wall.start, wall.end, start, end) &&
segmentsOverlap(wall.start, wall.end, start, end) &&
pointOnSegment(wall.start, start, end, POINT_TOLERANCE) &&
pointOnSegment(wall.end, start, end, POINT_TOLERANCE)
)
})
}
export function collectZoneContentIds(
nodes: Readonly<Record<AnyNodeId, AnyNode>>,
zone: ZoneNode,
): AnyNodeId[] {
const levelId = zone.parentId
if (!levelId) return []
const footprint = zone.polygon.map((point) => [point[0], point[1]] as Point2D)
const boundaryWalls = Object.values(nodes)
.filter((node): node is WallNode => node.type === 'wall' && node.parentId === levelId)
.filter((wall) => wallLiesOnZoneBoundary(wall, footprint))
const surfaces = Object.values(nodes)
.filter(
(node): node is SlabNode | CeilingNode =>
(node.type === 'slab' || node.type === 'ceiling') && node.parentId === levelId,
)
.filter((surface) => {
const polygon = surface.polygon.map((point) => [point[0], point[1]] as Point2D)
return polygonMatchesZoneFootprint(polygon, footprint)
})
const floorItems = Object.values(nodes)
.filter((node): node is ItemNode => node.type === 'item' && node.parentId === levelId)
.filter((item) => pointInPolygonWithTolerance([item.position[0], item.position[2]], footprint))
return Array.from(
new Set<AnyNodeId>([
...boundaryWalls.map((wall) => wall.id as AnyNodeId),
...surfaces.map((surface) => surface.id as AnyNodeId),
...floorItems.map((item) => item.id as AnyNodeId),
]),
)
}