232 lines
5.5 KiB
TypeScript
232 lines
5.5 KiB
TypeScript
import type {
|
|
CeilingNode,
|
|
DoorNode,
|
|
ItemNode,
|
|
Point2D,
|
|
RoofNode,
|
|
RoofSegmentNode,
|
|
SlabNode,
|
|
StairNode,
|
|
WallNode,
|
|
WindowNode,
|
|
} from '@pascal-app/core'
|
|
import {
|
|
doesPolygonIntersectSelectionBounds,
|
|
getDistanceToWallSegment,
|
|
isPointInsidePolygon,
|
|
isPointInsidePolygonWithHoles,
|
|
} from './geometry'
|
|
import type { FloorplanSelectionBounds } from './types'
|
|
|
|
type OpeningNode = WindowNode | DoorNode
|
|
|
|
type OpeningPolygonEntry = {
|
|
opening: OpeningNode
|
|
polygon: Point2D[]
|
|
}
|
|
|
|
type ItemEntry = {
|
|
item: ItemNode
|
|
polygon: Point2D[]
|
|
}
|
|
|
|
type StairEntry = {
|
|
hitPolygons: Point2D[][]
|
|
stair: StairNode
|
|
segments: Array<{ polygon: Point2D[] }>
|
|
}
|
|
|
|
type WallEntry = {
|
|
wall: WallNode
|
|
polygon: Point2D[]
|
|
}
|
|
|
|
type SlabEntry = {
|
|
slab: SlabNode
|
|
polygon: Point2D[]
|
|
holes: Point2D[][]
|
|
}
|
|
|
|
type CeilingEntry = {
|
|
ceiling: CeilingNode
|
|
polygon: Point2D[]
|
|
holes: Point2D[][]
|
|
}
|
|
|
|
type RoofEntry = {
|
|
roof: RoofNode
|
|
segments: Array<{
|
|
polygon: Point2D[]
|
|
segment: RoofSegmentNode
|
|
}>
|
|
}
|
|
|
|
type FloorplanSelectionToolContext = {
|
|
point: Point2D
|
|
phase: 'site' | 'structure' | 'furnish'
|
|
isItemContextActive: boolean
|
|
items: ItemEntry[]
|
|
openings: OpeningPolygonEntry[]
|
|
stairs: StairEntry[]
|
|
walls: WallEntry[]
|
|
slabs: SlabEntry[]
|
|
ceilings: CeilingEntry[]
|
|
roofs: RoofEntry[]
|
|
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
|
|
}
|
|
|
|
function getStairHitPolygons(stair: StairEntry) {
|
|
return stair.hitPolygons.length > 0
|
|
? stair.hitPolygons
|
|
: stair.segments.map(({ polygon }) => polygon)
|
|
}
|
|
|
|
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((stair) =>
|
|
getStairHitPolygons(stair).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 roofHit = context.roofs.find(({ segments }) =>
|
|
segments.some(({ polygon }) => isPointInsidePolygon(context.point, polygon)),
|
|
)
|
|
if (roofHit) {
|
|
return roofHit.roof.id
|
|
}
|
|
|
|
const ceilingHit = context.ceilings.find(({ polygon, holes }) =>
|
|
isPointInsidePolygonWithHoles(context.point, polygon, holes),
|
|
)
|
|
if (ceilingHit) {
|
|
return ceilingHit.ceiling.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[]
|
|
ceilings: CeilingEntry[]
|
|
stairs: StairEntry[]
|
|
roofs: RoofEntry[]
|
|
}
|
|
|
|
export function getFloorplanSelectionIdsInBounds({
|
|
bounds,
|
|
phase,
|
|
isItemContextActive,
|
|
items,
|
|
walls,
|
|
openings,
|
|
slabs,
|
|
ceilings,
|
|
stairs,
|
|
roofs,
|
|
}: 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 ceilingIds = ceilings
|
|
.filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
|
|
.map(({ ceiling }) => ceiling.id)
|
|
const stairIds = stairs
|
|
.filter((stair) =>
|
|
getStairHitPolygons(stair).some((polygon) =>
|
|
doesPolygonIntersectSelectionBounds(polygon, bounds),
|
|
),
|
|
)
|
|
.map(({ stair }) => stair.id)
|
|
const roofIds = roofs
|
|
.filter(({ segments }) =>
|
|
segments.some(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds)),
|
|
)
|
|
.map(({ roof }) => roof.id)
|
|
|
|
return Array.from(
|
|
new Set([
|
|
...itemIds,
|
|
...wallIds,
|
|
...openingIds,
|
|
...slabIds,
|
|
...ceilingIds,
|
|
...stairIds,
|
|
...roofIds,
|
|
]),
|
|
)
|
|
}
|