161 lines
4.1 KiB
TypeScript

import type { DoorNode, ItemNode, SlabNode, StairNode, WallNode, WindowNode } from '@pascal-app/core'
import {
doesPolygonIntersectSelectionBounds,
getDistanceToWallSegment,
isPointInsidePolygon,
isPointInsidePolygonWithHoles,
type FloorplanSelectionBounds,
type Point2D,
} from '@pascal-app/core'
type OpeningNode = WindowNode | DoorNode
type OpeningPolygonEntry = {
opening: OpeningNode
polygon: Point2D[]
}
type ItemEntry = {
item: ItemNode
polygon: Point2D[]
}
type StairEntry = {
stair: StairNode
segments: Array<{ polygon: Point2D[] }>
}
type WallEntry = {
wall: WallNode
polygon: Point2D[]
}
type SlabEntry = {
slab: SlabNode
polygon: Point2D[]
holes: Point2D[][]
}
type FloorplanSelectionToolContext = {
point: Point2D
phase: 'site' | 'structure' | 'furnish'
isItemContextActive: boolean
items: ItemEntry[]
openings: OpeningPolygonEntry[]
stairs: StairEntry[]
walls: WallEntry[]
slabs: SlabEntry[]
openingHitTolerance: number
wallHitTolerance: number
getOpeningCenterLine: (polygon: Point2D[]) => { start: Point2D; end: Point2D } | null
}
function getItemHitId(context: FloorplanSelectionToolContext) {
if (!context.isItemContextActive) {
return null
}
const itemHit = context.items.find(({ polygon }) => isPointInsidePolygon(context.point, polygon))
return itemHit?.item.id ?? null
}
export function getFloorplanHitNodeId(context: FloorplanSelectionToolContext) {
if (context.phase === 'structure') {
const openingHit = context.openings.find(({ polygon }) => {
if (isPointInsidePolygon(context.point, polygon)) {
return true
}
const centerLine = context.getOpeningCenterLine(polygon)
if (!centerLine) {
return false
}
return (
getDistanceToWallSegment(
context.point,
[centerLine.start.x, centerLine.start.y],
[centerLine.end.x, centerLine.end.y],
) <= context.openingHitTolerance
)
})
if (openingHit) {
return openingHit.opening.id
}
const stairHit = context.stairs.find(({ segments }) =>
segments.some(({ polygon }) => isPointInsidePolygon(context.point, polygon)),
)
if (stairHit) {
return stairHit.stair.id
}
const wallHit = context.walls.find(
({ wall, polygon }) =>
isPointInsidePolygon(context.point, polygon) ||
getDistanceToWallSegment(context.point, wall.start, wall.end) <= context.wallHitTolerance,
)
if (wallHit) {
return wallHit.wall.id
}
const slabHit = context.slabs.find(({ polygon, holes }) =>
isPointInsidePolygonWithHoles(context.point, polygon, holes),
)
if (slabHit) {
return slabHit.slab.id
}
}
return getItemHitId(context)
}
type FloorplanSelectionBoundsContext = {
bounds: FloorplanSelectionBounds
phase: 'site' | 'structure' | 'furnish'
isItemContextActive: boolean
items: ItemEntry[]
walls: WallEntry[]
openings: OpeningPolygonEntry[]
slabs: SlabEntry[]
stairs: StairEntry[]
}
export function getFloorplanSelectionIdsInBounds({
bounds,
phase,
isItemContextActive,
items,
walls,
openings,
slabs,
stairs,
}: FloorplanSelectionBoundsContext) {
const itemIds = isItemContextActive
? items
.filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
.map(({ item }) => item.id)
: []
if (phase !== 'structure') {
return itemIds
}
const wallIds = walls
.filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
.map(({ wall }) => wall.id)
const openingIds = openings
.filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
.map(({ opening }) => opening.id)
const slabIds = slabs
.filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
.map(({ slab }) => slab.id)
const stairIds = stairs
.filter(({ segments }) =>
segments.some(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds)),
)
.map(({ stair }) => stair.id)
return Array.from(new Set([...itemIds, ...wallIds, ...openingIds, ...slabIds, ...stairIds]))
}