diff --git a/packages/editor/src/components/editor-2d/renderers/floorplan-draft-layer.tsx b/packages/editor/src/components/editor-2d/renderers/floorplan-draft-layer.tsx index bdd0debe..c2a4df91 100644 --- a/packages/editor/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +++ b/packages/editor/src/components/editor-2d/renderers/floorplan-draft-layer.tsx @@ -11,6 +11,7 @@ type SvgLine = { type FloorplanDraftLayerProps = { draftPolygonPoints: string | null + linearDraftSegment: SvgLine | null polygonDraftPolygonPoints: string | null polygonDraftPolylinePoints: string | null polygonDraftClosingSegment: SvgLine | null @@ -22,6 +23,7 @@ type FloorplanDraftLayerProps = { export const FloorplanDraftLayer = memo(function FloorplanDraftLayer({ draftPolygonPoints, + linearDraftSegment, polygonDraftPolygonPoints, polygonDraftPolylinePoints, polygonDraftClosingSegment, @@ -44,6 +46,21 @@ export const FloorplanDraftLayer = memo(function FloorplanDraftLayer({ /> )} + {linearDraftSegment && ( + + )} + {polygonDraftPolygonPoints && ( )} diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index 6e1ef5a8..7d51d30c 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -32,6 +32,7 @@ import { type RoofNode, type RoofSegmentNode, rotatePlanVector as rotateSharedPlanVector, + sceneRegistry, type SiteNode, SlabNode, type StairNode, @@ -81,6 +82,7 @@ import { DEFAULT_STAIR_THICKNESS, DEFAULT_STAIR_WIDTH, } from '../tools/stair/stair-defaults' +import { snapFenceDraftPoint } from '../tools/fence/fence-drafting' import { createWallOnCurrentLevel, isWallLongEnough, @@ -156,6 +158,9 @@ const FLOORPLAN_WALL_OUTER_MEASUREMENT_EXTENSION = 'rgba(96, 165, 250, 0.9)' const FLOORPLAN_WALL_INNER_MEASUREMENT_STROKE = 'rgba(96, 165, 250, 0.95)' const FLOORPLAN_WALL_INNER_MEASUREMENT_TEXT = 'rgba(59, 130, 246, 0.98)' const FLOORPLAN_WALL_INNER_MEASUREMENT_EXTENSION = 'rgba(147, 197, 253, 0.9)' +const FLOORPLAN_OPENING_MEASUREMENT_STROKE = 'rgba(249, 115, 22, 0.98)' +const FLOORPLAN_OPENING_MEASUREMENT_TEXT = 'rgba(234, 88, 12, 0.98)' +const FLOORPLAN_OPENING_MEASUREMENT_EXTENSION = 'rgba(251, 146, 60, 0.9)' const FLOORPLAN_ITEM_CLEARANCE_MAX_DISTANCE = 12 const FLOORPLAN_ITEM_CLEARANCE_MIN_DISTANCE = 0.05 const FLOORPLAN_ITEM_CLEARANCE_EDGE_PARALLEL_THRESHOLD = 0.65 @@ -410,6 +415,13 @@ type SlabPolygonEntry = { path: string } +type CeilingPolygonEntry = { + ceiling: CeilingNode + polygon: Point2D[] + holes: Point2D[][] + path: string +} + type SitePolygonEntry = { site: SiteNode polygon: Point2D[] @@ -489,6 +501,10 @@ type FloorplanPalette = { slabStroke: string selectedSlabFill: string selectedSlabStroke: string + ceilingFill: string + ceilingStroke: string + selectedCeilingFill: string + selectedCeilingStroke: string wallFill: string wallStroke: string wallInnerStroke: string @@ -2808,6 +2824,13 @@ type WallFaceLine = { end: Point2D } +type WallMeasurementFaceContext = { + outerFace: WallFaceLine + innerFace: WallFaceLine + outwardNormal: Point2D + inwardNormal: Point2D +} + function getWallFaceLines(polygon: Point2D[], wall: WallNode): { left: WallFaceLine; right: WallFaceLine } | null { if (polygon.length < 4 || isCurvedWall(wall)) { return null @@ -2844,11 +2867,10 @@ function getLineMidpoint(line: WallFaceLine): Point2D { } } -function getSelectedWallMeasurementOverlays( +function getWallMeasurementFaceContext( selectedWallEntry: WallPolygonEntry, wallPolygons: WallPolygonEntry[], - unit: 'metric' | 'imperial', -): LinearMeasurementOverlay[] { +): WallMeasurementFaceContext | null { let minX = Number.POSITIVE_INFINITY let maxX = Number.NEGATIVE_INFINITY let minY = Number.POSITIVE_INFINITY @@ -2864,16 +2886,10 @@ function getSelectedWallMeasurementOverlays( const centerX = minX === Number.POSITIVE_INFINITY ? 0 : (minX + maxX) / 2 const centerY = minY === Number.POSITIVE_INFINITY ? 0 : (minY + maxY) / 2 const { wall, polygon } = selectedWallEntry - - if (isCurvedWall(wall)) { - const overlay = getWallMeasurementOverlay(wall, centerX, centerY, unit) - return overlay ? [overlay] : [] - } - const faceLines = getWallFaceLines(polygon, wall) + if (!faceLines) { - const overlay = getWallMeasurementOverlay(wall, centerX, centerY, unit) - return overlay ? [overlay] : [] + return null } const dx = wall.end[0] - wall.start[0] @@ -2881,7 +2897,7 @@ function getSelectedWallMeasurementOverlays( const length = Math.hypot(dx, dy) if (length < 1e-6) { - return [] + return null } const wallMidpoint = { @@ -2907,7 +2923,100 @@ function getSelectedWallMeasurementOverlays( (leftMidpoint.y - wallMidpoint.y) * outwardNormal.y const outerFace = rightScore >= leftScore ? faceLines.right : faceLines.left const innerFace = outerFace === faceLines.right ? faceLines.left : faceLines.right - const inwardNormal = { x: -outwardNormal.x, y: -outwardNormal.y } + + return { + outerFace, + innerFace, + outwardNormal, + inwardNormal: { x: -outwardNormal.x, y: -outwardNormal.y }, + } +} + +function getAdjacentOpeningBounds( + current: { + id: OpeningNode['id'] + wallId: WallNode['id'] + startDistance: number + endDistance: number + }, + openings: OpeningPolygonEntry[], +) { + let leftBoundary: number | null = null + let rightBoundary: number | null = null + + for (const { opening } of openings) { + if (opening.parentId !== current.wallId || opening.id === current.id) { + continue + } + + const startDistance = opening.position[0] - opening.width / 2 + const endDistance = opening.position[0] + opening.width / 2 + + if (endDistance <= current.startDistance && (leftBoundary === null || endDistance > leftBoundary)) { + leftBoundary = endDistance + } + + if ( + startDistance >= current.endDistance && + (rightBoundary === null || startDistance < rightBoundary) + ) { + rightBoundary = startDistance + } + } + + return { + leftBoundary, + rightBoundary, + } +} + +function getSelectedWallMeasurementOverlays( + selectedWallEntry: WallPolygonEntry, + wallPolygons: WallPolygonEntry[], + unit: 'metric' | 'imperial', +): LinearMeasurementOverlay[] { + const { wall } = selectedWallEntry + + if (isCurvedWall(wall)) { + let minX = Number.POSITIVE_INFINITY + let maxX = Number.NEGATIVE_INFINITY + let minY = Number.POSITIVE_INFINITY + let maxY = Number.NEGATIVE_INFINITY + + for (const { wall: candidateWall } of wallPolygons) { + minX = Math.min(minX, candidateWall.start[0], candidateWall.end[0]) + maxX = Math.max(maxX, candidateWall.start[0], candidateWall.end[0]) + minY = Math.min(minY, candidateWall.start[1], candidateWall.end[1]) + maxY = Math.max(maxY, candidateWall.start[1], candidateWall.end[1]) + } + + const centerX = minX === Number.POSITIVE_INFINITY ? 0 : (minX + maxX) / 2 + const centerY = minY === Number.POSITIVE_INFINITY ? 0 : (minY + maxY) / 2 + const overlay = getWallMeasurementOverlay(wall, centerX, centerY, unit) + return overlay ? [overlay] : [] + } + + const faceContext = getWallMeasurementFaceContext(selectedWallEntry, wallPolygons) + if (!faceContext) { + let minX = Number.POSITIVE_INFINITY + let maxX = Number.NEGATIVE_INFINITY + let minY = Number.POSITIVE_INFINITY + let maxY = Number.NEGATIVE_INFINITY + + for (const { wall: candidateWall } of wallPolygons) { + minX = Math.min(minX, candidateWall.start[0], candidateWall.end[0]) + maxX = Math.max(maxX, candidateWall.start[0], candidateWall.end[0]) + minY = Math.min(minY, candidateWall.start[1], candidateWall.end[1]) + maxY = Math.max(maxY, candidateWall.start[1], candidateWall.end[1]) + } + + const centerX = minX === Number.POSITIVE_INFINITY ? 0 : (minX + maxX) / 2 + const centerY = minY === Number.POSITIVE_INFINITY ? 0 : (minY + maxY) / 2 + const overlay = getWallMeasurementOverlay(wall, centerX, centerY, unit) + return overlay ? [overlay] : [] + } + + const { outerFace, innerFace, outwardNormal, inwardNormal } = faceContext const outerLength = Math.hypot(outerFace.end.x - outerFace.start.x, outerFace.end.y - outerFace.start.y) const innerLength = Math.hypot(innerFace.end.x - innerFace.start.x, innerFace.end.y - innerFace.start.y) const overlays: LinearMeasurementOverlay[] = [] @@ -3676,11 +3785,17 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({ canFocusGeometry, canSelectGeometry, canSelectSlabs, + canSelectCeilings, + ceilingPolygons, highlightedIdSet, + hoveredCeilingId, hoveredSlabId, hoveredOpeningId, hoveredWallId, isDeleteMode, + onCeilingDoubleClick, + onCeilingHoverChange, + onCeilingSelect, onSlabDoubleClick, onSlabHoverChange, onSlabSelect, @@ -3701,11 +3816,17 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({ }: { canFocusGeometry: boolean canSelectSlabs: boolean + canSelectCeilings: boolean canSelectGeometry: boolean + ceilingPolygons: CeilingPolygonEntry[] highlightedIdSet: ReadonlySet + hoveredCeilingId: CeilingNode['id'] | null hoveredSlabId: SlabNode['id'] | null hoveredOpeningId: OpeningNode['id'] | null isDeleteMode: boolean + onCeilingDoubleClick: (ceiling: CeilingNode) => void + onCeilingHoverChange: (ceilingId: CeilingNode['id'] | null) => void + onCeilingSelect: (ceilingId: CeilingNode['id'], event: ReactMouseEvent) => void onSlabDoubleClick: (slab: SlabNode) => void onSlabHoverChange: (slabId: SlabNode['id'] | null) => void onSlabSelect: (slabId: SlabNode['id'], event: ReactMouseEvent) => void @@ -3825,6 +3946,70 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({ ) })} + {ceilingPolygons.map(({ ceiling, path }) => { + const isSelected = selectedIdSet.has(ceiling.id) + const isHighlighted = highlightedIdSet.has(ceiling.id) + const isDeleteHovered = isDeleteMode && hoveredCeilingId === ceiling.id + const showSelectedCeilingStyle = isSelected || isHighlighted + const ceilingBorderStroke = isDeleteHovered + ? palette.deleteStroke + : showSelectedCeilingStyle + ? palette.selectedCeilingStroke + : palette.ceilingStroke + const ceilingBorderWidth = showSelectedCeilingStyle ? '0.065' : '0.05' + + return ( + + { + event.stopPropagation() + onCeilingSelect(ceiling.id, event) + } + : undefined + } + onDoubleClick={ + canFocusGeometry + ? (event) => { + event.stopPropagation() + onCeilingDoubleClick(ceiling) + } + : undefined + } + onPointerEnter={canSelectCeilings ? () => onCeilingHoverChange(ceiling.id) : undefined} + onPointerLeave={canSelectCeilings ? () => onCeilingHoverChange(null) : undefined} + pointerEvents={canSelectCeilings ? undefined : 'none'} + style={canSelectCeilings ? { cursor: EDITOR_CURSOR } : undefined} + stroke="none" + /> + + + ) + })} + {wallPolygons.map(({ wall, polygon, points }) => { const isSelected = selectedIdSet.has(wall.id) const isHighlighted = highlightedIdSet.has(wall.id) @@ -6426,6 +6611,7 @@ const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndicatorOv export function FloorplanPanel() { const viewportHostRef = useRef(null) const svgRef = useRef(null) + const floorplanSceneRef = useRef(null) const panStateRef = useRef(null) const guideInteractionRef = useRef(null) const guideTransformDraftRef = useRef(null) @@ -6676,6 +6862,11 @@ export function FloorplanPanel() { const [draftStart, setDraftStart] = useState(null) const [draftEnd, setDraftEnd] = useState(null) + const [fenceDraftStart, setFenceDraftStart] = useState(null) + const [fenceDraftEnd, setFenceDraftEnd] = useState(null) + const [roofDraftStart, setRoofDraftStart] = useState(null) + const [roofDraftEnd, setRoofDraftEnd] = useState(null) + const [ceilingDraftPoints, setCeilingDraftPoints] = useState([]) const [slabDraftPoints, setSlabDraftPoints] = useState([]) const [zoneDraftPoints, setZoneDraftPoints] = useState([]) const [siteBoundaryDraft, setSiteBoundaryDraft] = useState(null) @@ -6693,6 +6884,7 @@ export function FloorplanPanel() { const [hoveredWallId, setHoveredWallId] = useState(null) const [hoveredFenceId, setHoveredFenceId] = useState(null) const [hoveredSlabId, setHoveredSlabId] = useState(null) + const [hoveredCeilingId, setHoveredCeilingId] = useState(null) const [hoveredItemId, setHoveredItemId] = useState(null) const [hoveredStairId, setHoveredStairId] = useState(null) const [hoveredZoneId, setHoveredZoneId] = useState(null) @@ -7437,6 +7629,126 @@ export function FloorplanPanel() { return overlay ? [overlay] : [] }) }, [displayWallPolygons, selectedItemEntry, unit]) + const movingOpeningPlacementMeasurements = useMemo(() => { + if (!(movingNode?.type === 'door' || movingNode?.type === 'window')) { + return [] as LinearMeasurementOverlay[] + } + + const openingEntry = openingsPolygons.find(({ opening }) => opening.id === movingNode.id) + if (!openingEntry) { + return [] as LinearMeasurementOverlay[] + } + + const wallEntry = displayWallPolygons.find(({ wall }) => wall.id === openingEntry.opening.parentId) + if (!wallEntry || isCurvedWall(wallEntry.wall)) { + return [] as LinearMeasurementOverlay[] + } + + const faceContext = getWallMeasurementFaceContext(wallEntry, displayWallPolygons) + if (!faceContext) { + return [] as LinearMeasurementOverlay[] + } + + const wall = wallEntry.wall + const wallLength = Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]) + if (wallLength < 1e-6) { + return [] as LinearMeasurementOverlay[] + } + + const tangent = normalizePlanVector({ + x: wall.end[0] - wall.start[0], + y: wall.end[1] - wall.start[1], + }) + if (!tangent) { + return [] as LinearMeasurementOverlay[] + } + + const opening = openingEntry.opening + const startDistance = opening.position[0] - opening.width / 2 + const endDistance = opening.position[0] + opening.width / 2 + const { leftBoundary, rightBoundary } = getAdjacentOpeningBounds( + { + id: opening.id, + wallId: wall.id, + startDistance, + endDistance, + }, + openingsPolygons, + ) + const faceOffsetDistance = (wall.thickness ?? 0.1) / 2 + const openingFaceStart = { + x: wall.start[0] + tangent.x * startDistance + faceContext.outwardNormal.x * faceOffsetDistance, + y: wall.start[1] + tangent.y * startDistance + faceContext.outwardNormal.y * faceOffsetDistance, + } + const openingFaceEnd = { + x: wall.start[0] + tangent.x * endDistance + faceContext.outwardNormal.x * faceOffsetDistance, + y: wall.start[1] + tangent.y * endDistance + faceContext.outwardNormal.y * faceOffsetDistance, + } + const leftBoundaryPoint = + leftBoundary === null + ? faceContext.outerFace.start + : { + x: wall.start[0] + tangent.x * leftBoundary + faceContext.outwardNormal.x * faceOffsetDistance, + y: wall.start[1] + tangent.y * leftBoundary + faceContext.outwardNormal.y * faceOffsetDistance, + } + const rightBoundaryPoint = + rightBoundary === null + ? faceContext.outerFace.end + : { + x: wall.start[0] + tangent.x * rightBoundary + faceContext.outwardNormal.x * faceOffsetDistance, + y: wall.start[1] + tangent.y * rightBoundary + faceContext.outwardNormal.y * faceOffsetDistance, + } + const overlays: LinearMeasurementOverlay[] = [] + const leftDistance = getPlanPointDistance(leftBoundaryPoint, openingFaceStart) + + if (leftDistance >= 0.01) { + const overlay = getLinearMeasurementOverlay( + `${opening.id}:placement-left`, + leftBoundaryPoint, + openingFaceStart, + formatMeasurement(leftDistance, unit), + { + offsetDistance: FLOORPLAN_WALL_OUTER_MEASUREMENT_OFFSET, + offsetVector: faceContext.outwardNormal, + }, + ) + + if (overlay) { + overlays.push({ + ...overlay, + extensionStroke: FLOORPLAN_OPENING_MEASUREMENT_EXTENSION, + labelFill: FLOORPLAN_OPENING_MEASUREMENT_TEXT, + stroke: FLOORPLAN_OPENING_MEASUREMENT_STROKE, + }) + } + } + + const rightDistance = getPlanPointDistance(openingFaceEnd, rightBoundaryPoint) + + if (rightDistance >= 0.01) { + const overlay = getLinearMeasurementOverlay( + `${opening.id}:placement-right`, + openingFaceEnd, + rightBoundaryPoint, + formatMeasurement(rightDistance, unit), + { + offsetDistance: FLOORPLAN_WALL_OUTER_MEASUREMENT_OFFSET, + offsetVector: faceContext.outwardNormal, + }, + ) + + if (overlay) { + overlays.push({ + ...overlay, + extensionStroke: FLOORPLAN_OPENING_MEASUREMENT_EXTENSION, + labelFill: FLOORPLAN_OPENING_MEASUREMENT_TEXT, + stroke: FLOORPLAN_OPENING_MEASUREMENT_STROKE, + }) + } + } + + return overlays + }, [displayWallPolygons, movingNode, openingsPolygons, unit]) const selectedWallEntry = useMemo(() => { if (selectedIds.length !== 1) { return null @@ -7492,13 +7804,17 @@ export function FloorplanPanel() { const isSiteEditActive = phase === 'site' const isWallBuildActive = phase === 'structure' && mode === 'build' && tool === 'wall' const isSlabBuildActive = phase === 'structure' && mode === 'build' && tool === 'slab' + const isCeilingBuildActive = phase === 'structure' && mode === 'build' && tool === 'ceiling' const isZoneBuildActive = phase === 'structure' && mode === 'build' && tool === 'zone' const isDoorBuildActive = phase === 'structure' && mode === 'build' && tool === 'door' const isWindowBuildActive = phase === 'structure' && mode === 'build' && tool === 'window' const isPolygonBuildActive = isSlabBuildActive || isZoneBuildActive + const isPolygonDraftBuildActive = isPolygonBuildActive || isCeilingBuildActive const isOpeningBuildActive = isDoorBuildActive || isWindowBuildActive const isOpeningMoveActive = movingOpeningType !== null const isOpeningPlacementActive = isOpeningBuildActive || isOpeningMoveActive + const isFenceBuildActive = phase === 'structure' && mode === 'build' && tool === 'fence' + const isRoofBuildActive = phase === 'structure' && mode === 'build' && tool === 'roof' const isStairBuildActive = phase === 'structure' && mode === 'build' && tool === 'stair' const isStairMoveActive = movingNode?.type === 'stair' const isRoofMoveActive = movingNode?.type === 'roof' || movingNode?.type === 'roof-segment' @@ -7514,6 +7830,9 @@ export function FloorplanPanel() { const isFloorItemBuildActive = mode === 'build' && tool === 'item' && !selectedItem?.attachTo const isFloorItemMoveActive = movingNode?.type === 'item' && !movingNode.asset.attachTo const isFloorplanGridInteractionActive = + isFenceBuildActive || + isRoofBuildActive || + isCeilingBuildActive || isStairBuildActive || isStairMoveActive || isRoofMoveActive || @@ -7920,10 +8239,48 @@ export function FloorplanPanel() { return getWallPlanFootprint(draftWall, EMPTY_WALL_MITER_DATA) }, [draftEnd, draftStart, levelId]) const draftPolygonPoints = useMemo( - () => (draftPolygon ? formatPolygonPoints(draftPolygon) : null), - [draftPolygon], + () => { + if (isRoofBuildActive && roofDraftStart && roofDraftEnd) { + const minX = Math.min(roofDraftStart[0], roofDraftEnd[0]) + const maxX = Math.max(roofDraftStart[0], roofDraftEnd[0]) + const minY = Math.min(roofDraftStart[1], roofDraftEnd[1]) + const maxY = Math.max(roofDraftStart[1], roofDraftEnd[1]) + + if (Math.abs(maxX - minX) >= 1e-6 || Math.abs(maxY - minY) >= 1e-6) { + return formatPolygonPoints([ + { x: minX, y: minY }, + { x: maxX, y: minY }, + { x: maxX, y: maxY }, + { x: minX, y: maxY }, + ]) + } + } + + return draftPolygon ? formatPolygonPoints(draftPolygon) : null + }, + [draftPolygon, isRoofBuildActive, roofDraftEnd, roofDraftStart], ) + const fenceDraftSegment = useMemo(() => { + if (!(isFenceBuildActive && fenceDraftStart && fenceDraftEnd)) { + return null + } + + if (getPlanPointDistance(toPoint2D(fenceDraftStart), toPoint2D(fenceDraftEnd)) < 1e-6) { + return null + } + + return { + x1: toSvgX(fenceDraftStart[0]), + y1: toSvgY(fenceDraftStart[1]), + x2: toSvgX(fenceDraftEnd[0]), + y2: toSvgY(fenceDraftEnd[1]), + } + }, [fenceDraftEnd, fenceDraftStart, isFenceBuildActive]) const activePolygonDraftPoints = useMemo(() => { + if (isCeilingBuildActive) { + return ceilingDraftPoints + } + if (isZoneBuildActive) { return zoneDraftPoints } @@ -7933,23 +8290,30 @@ export function FloorplanPanel() { } return [] as WallPlanPoint[] - }, [isSlabBuildActive, isZoneBuildActive, slabDraftPoints, zoneDraftPoints]) + }, [ + ceilingDraftPoints, + isCeilingBuildActive, + isSlabBuildActive, + isZoneBuildActive, + slabDraftPoints, + zoneDraftPoints, + ]) const polygonDraftPolylinePoints = useMemo(() => { - if (!(isPolygonBuildActive && cursorPoint && activePolygonDraftPoints.length > 0)) { + if (!(isPolygonDraftBuildActive && cursorPoint && activePolygonDraftPoints.length > 0)) { return null } return formatPolygonPoints([...activePolygonDraftPoints.map(toPoint2D), toPoint2D(cursorPoint)]) - }, [activePolygonDraftPoints, cursorPoint, isPolygonBuildActive]) + }, [activePolygonDraftPoints, cursorPoint, isPolygonDraftBuildActive]) const polygonDraftPolygonPoints = useMemo(() => { - if (!(isPolygonBuildActive && cursorPoint && activePolygonDraftPoints.length >= 2)) { + if (!(isPolygonDraftBuildActive && cursorPoint && activePolygonDraftPoints.length >= 2)) { return null } return formatPolygonPoints([...activePolygonDraftPoints.map(toPoint2D), toPoint2D(cursorPoint)]) - }, [activePolygonDraftPoints, cursorPoint, isPolygonBuildActive]) + }, [activePolygonDraftPoints, cursorPoint, isPolygonDraftBuildActive]) const polygonDraftClosingSegment = useMemo(() => { - if (!(isPolygonBuildActive && cursorPoint && activePolygonDraftPoints.length >= 2)) { + if (!(isPolygonDraftBuildActive && cursorPoint && activePolygonDraftPoints.length >= 2)) { return null } @@ -7964,13 +8328,14 @@ export function FloorplanPanel() { x2: toSvgX(firstPoint[0]), y2: toSvgY(firstPoint[1]), } - }, [activePolygonDraftPoints, cursorPoint, isPolygonBuildActive]) + }, [activePolygonDraftPoints, cursorPoint, isPolygonDraftBuildActive]) const svgAspectRatio = surfaceSize.width / surfaceSize.height || 1 const fittedViewport = useMemo(() => { const allPoints = [ ...(visibleSitePolygon ? visibleSitePolygon.polygon : []), + ...ceilingPolygons.flatMap((entry) => entry.polygon), ...displaySlabPolygons.flatMap((entry) => entry.polygon), ...floorplanFenceEntries.flatMap((entry) => entry.centerline), ...floorplanItemEntries.flatMap((entry) => entry.polygon), @@ -8017,6 +8382,7 @@ export function FloorplanPanel() { width, } }, [ + ceilingPolygons, displaySlabPolygons, floorplanFenceEntries, floorplanItemEntries, @@ -8313,6 +8679,10 @@ export function FloorplanPanel() { slabStroke: 'rgba(148, 163, 184, 0.45)', selectedSlabFill: 'rgba(167, 139, 250, 0.14)', selectedSlabStroke: '#a78bfa', + ceilingFill: 'rgba(56, 189, 248, 0.12)', + ceilingStroke: '#38bdf8', + selectedCeilingFill: 'rgba(59, 130, 246, 0.16)', + selectedCeilingStroke: '#2563eb', wallFill: '#ffffff', wallStroke: 'rgba(31, 41, 55, 0.9)', wallInnerStroke: 'rgba(51, 65, 85, 0.72)', @@ -8351,6 +8721,10 @@ export function FloorplanPanel() { slabStroke: 'rgba(100, 116, 139, 0.55)', selectedSlabFill: 'rgba(167, 139, 250, 0.14)', selectedSlabStroke: '#a78bfa', + ceilingFill: 'rgba(14, 165, 233, 0.12)', + ceilingStroke: '#0ea5e9', + selectedCeilingFill: 'rgba(59, 130, 246, 0.16)', + selectedCeilingStroke: '#2563eb', wallFill: '#ffffff', wallStroke: 'rgba(31, 41, 55, 0.9)', wallInnerStroke: 'rgba(71, 85, 105, 0.58)', @@ -8416,7 +8790,8 @@ export function FloorplanPanel() { const getSvgPointFromClientPoint = useCallback( (clientX: number, clientY: number): SvgPoint | null => { const svg = svgRef.current - const ctm = svg?.getScreenCTM() + const target = floorplanSceneRef.current ?? svg + const ctm = target?.getScreenCTM() if (!(svg && ctm)) { return null } @@ -8438,8 +8813,8 @@ export function FloorplanPanel() { return null } - if (buildingRotationY !== 0) { - const [unrotX, unrotY] = rotatePlanVector(svgPoint.x, svgPoint.y, buildingRotationY) + if (!floorplanSceneRef.current && buildingRotationY !== 0) { + const [unrotX, unrotY] = rotatePlanVector(svgPoint.x, svgPoint.y, -buildingRotationY) return toPlanPointFromSvgPoint({ x: unrotX, y: unrotY }) } @@ -8655,6 +9030,17 @@ export function FloorplanPanel() { setDraftStart(null) setDraftEnd(null) }, []) + const clearFencePlacementDraft = useCallback(() => { + setFenceDraftStart(null) + setFenceDraftEnd(null) + }, []) + const clearRoofPlacementDraft = useCallback(() => { + setRoofDraftStart(null) + setRoofDraftEnd(null) + }, []) + const clearCeilingPlacementDraft = useCallback(() => { + setCeilingDraftPoints([]) + }, []) const clearSlabPlacementDraft = useCallback(() => { setSlabDraftPoints([]) }, []) @@ -8690,6 +9076,9 @@ export function FloorplanPanel() { const clearDraft = useCallback(() => { clearWallPlacementDraft() + clearFencePlacementDraft() + clearRoofPlacementDraft() + clearCeilingPlacementDraft() clearSlabPlacementDraft() clearZonePlacementDraft() clearWallEndpointDrag() @@ -8699,6 +9088,9 @@ export function FloorplanPanel() { clearZoneBoundaryInteraction() setCursorPoint(null) }, [ + clearFencePlacementDraft, + clearCeilingPlacementDraft, + clearRoofPlacementDraft, clearWallCurveDrag, clearSiteBoundaryInteraction, clearSlabBoundaryInteraction, @@ -8710,12 +9102,23 @@ export function FloorplanPanel() { ]) useEffect(() => { - if (isWallBuildActive || isPolygonBuildActive) { + if ( + isWallBuildActive || + isFenceBuildActive || + isRoofBuildActive || + isPolygonDraftBuildActive + ) { return } clearDraft() - }, [clearDraft, isPolygonBuildActive, isWallBuildActive]) + }, [ + clearDraft, + isFenceBuildActive, + isPolygonDraftBuildActive, + isRoofBuildActive, + isWallBuildActive, + ]) useEffect(() => { const handleCancel = () => { @@ -8778,7 +9181,9 @@ export function FloorplanPanel() { } const handleGridMove = (event: GridEvent) => { - setStairBuildPreviewPoint(getSnappedFloorplanPoint([event.position[0], event.position[2]])) + setStairBuildPreviewPoint( + getSnappedFloorplanPoint([event.localPosition[0], event.localPosition[2]]), + ) } emitter.on('grid:move', handleGridMove) @@ -9654,6 +10059,18 @@ export function FloorplanPanel() { }, []) const hoveredWallIdRef = useRef(null) + const floorplanGridLocalY = useMemo(() => { + if (movingNode?.type === 'item') { + return movingNode.position[1] + } + + if (levelId) { + return sceneRegistry.nodes.get(levelId as AnyNodeId)?.position.y ?? 0 + } + + return 0 + }, [levelId, movingNode]) + const floorplanGridWorldY = buildingPosition[1] + floorplanGridLocalY const emitFloorplanWallLeave = useCallback((wallId: string | null) => { if (!wallId) { return @@ -9673,23 +10090,25 @@ export function FloorplanPanel() { }, []) const emitFloorplanGridEvent = useCallback( ( - eventType: 'move' | 'click', + eventType: 'move' | 'click' | 'double-click', planPoint: WallPlanPoint, nativeEvent: ReactMouseEvent | ReactPointerEvent, ) => { const snappedPoint = getSnappedFloorplanPoint(planPoint) - const worldY = - movingNode?.type === 'stair' || movingNode?.type === 'item' ? movingNode.position[1] : 0 + const cos = Math.cos(buildingRotationY) + const sin = Math.sin(buildingRotationY) + const worldX = buildingPosition[0] + snappedPoint[0] * cos - snappedPoint[1] * sin + const worldZ = buildingPosition[2] + snappedPoint[0] * sin + snappedPoint[1] * cos emitter.emit(`grid:${eventType}` as any, { nativeEvent: nativeEvent.nativeEvent as any, - position: [snappedPoint[0], worldY, snappedPoint[1]], - localPosition: [snappedPoint[0], worldY, snappedPoint[1]], + position: [worldX, floorplanGridWorldY, worldZ], + localPosition: [snappedPoint[0], floorplanGridLocalY, snappedPoint[1]], }) return snappedPoint }, - [movingNode], + [buildingPosition, buildingRotationY, floorplanGridLocalY, floorplanGridWorldY], ) const handlePointerMove = useCallback( @@ -9740,6 +10159,59 @@ export function FloorplanPanel() { return } + if (isCeilingBuildActive) { + emitFloorplanGridEvent('move', planPoint, event) + + const snappedPoint = snapPolygonDraftPoint({ + point: planPoint, + start: ceilingDraftPoints[ceilingDraftPoints.length - 1], + angleSnap: ceilingDraftPoints.length > 0 && !shiftPressed, + }) + + setCursorPoint((previousPoint) => + previousPoint && pointsEqual(previousPoint, snappedPoint) ? previousPoint : snappedPoint, + ) + return + } + + if (isRoofBuildActive) { + const snappedPoint = getSnappedFloorplanPoint(planPoint) + emitFloorplanGridEvent('move', snappedPoint, event) + setCursorPoint((previousPoint) => + previousPoint && pointsEqual(previousPoint, snappedPoint) ? previousPoint : snappedPoint, + ) + + if (roofDraftStart) { + setRoofDraftEnd((previousPoint) => + previousPoint && pointsEqual(previousPoint, snappedPoint) ? previousPoint : snappedPoint, + ) + } + return + } + + if (isFenceBuildActive) { + emitFloorplanGridEvent('move', planPoint, event) + + const snappedPoint = snapFenceDraftPoint({ + point: planPoint, + walls, + fences, + start: fenceDraftStart ?? undefined, + angleSnap: Boolean(fenceDraftStart) && !shiftPressed, + }) + + setCursorPoint((previousPoint) => + previousPoint && pointsEqual(previousPoint, snappedPoint) ? previousPoint : snappedPoint, + ) + + if (fenceDraftStart) { + setFenceDraftEnd((previousEnd) => + previousEnd && pointsEqual(previousEnd, snappedPoint) ? previousEnd : snappedPoint, + ) + } + return + } + if (isFloorplanGridInteractionActive) { const snappedPoint = emitFloorplanGridEvent('move', planPoint, event) setCursorPoint((previousPoint) => @@ -9841,17 +10313,24 @@ export function FloorplanPanel() { }, [ draftStart, + ceilingDraftPoints, emitFloorplanWallLeave, emitFloorplanGridEvent, + fences, + fenceDraftStart, floorplanOpeningLocalY, fittedViewport, getPlanPointFromClientPoint, activePolygonDraftPoints, + isCeilingBuildActive, + isFenceBuildActive, isFloorplanGridInteractionActive, isMarqueeSelectionToolActive, isOpeningPlacementActive, isPolygonBuildActive, + isRoofBuildActive, isWallBuildActive, + roofDraftStart, siteVertexDragState, slabVertexDragState, shiftPressed, @@ -9911,6 +10390,53 @@ export function FloorplanPanel() { }, [clearDraft, createSlabOnCurrentLevel, slabDraftPoints], ) + const handleCeilingPlacementPoint = useCallback( + (point: WallPlanPoint) => { + const lastPoint = ceilingDraftPoints[ceilingDraftPoints.length - 1] + if (lastPoint && pointsEqual(lastPoint, point)) { + return + } + + const firstPoint = ceilingDraftPoints[0] + if ( + firstPoint && + ceilingDraftPoints.length >= 3 && + isPointNearPlanPoint(point, firstPoint) + ) { + clearCeilingPlacementDraft() + return + } + + setCeilingDraftPoints((currentPoints) => [...currentPoints, point]) + setCursorPoint(point) + }, + [ceilingDraftPoints, clearCeilingPlacementDraft], + ) + const handleCeilingPlacementConfirm = useCallback( + (point?: WallPlanPoint) => { + const firstPoint = ceilingDraftPoints[0] + const lastPoint = ceilingDraftPoints[ceilingDraftPoints.length - 1] + + let nextPoints = ceilingDraftPoints + if (point) { + const isClosingExistingPolygon = Boolean( + firstPoint && ceilingDraftPoints.length >= 3 && isPointNearPlanPoint(point, firstPoint), + ) + const isDuplicatePoint = Boolean(lastPoint && pointsEqual(lastPoint, point)) + + if (!(isClosingExistingPolygon || isDuplicatePoint)) { + nextPoints = [...ceilingDraftPoints, point] + } + } + + if (nextPoints.length < 3) { + return + } + + clearCeilingPlacementDraft() + }, + [ceilingDraftPoints, clearCeilingPlacementDraft], + ) const handleZonePlacementPoint = useCallback( (point: WallPlanPoint) => { const lastPoint = zoneDraftPoints[zoneDraftPoints.length - 1] @@ -10008,6 +10534,57 @@ export function FloorplanPanel() { return } + if (isCeilingBuildActive) { + emitFloorplanGridEvent('click', planPoint, event) + + const snappedPoint = snapPolygonDraftPoint({ + point: planPoint, + start: ceilingDraftPoints[ceilingDraftPoints.length - 1], + angleSnap: ceilingDraftPoints.length > 0 && !shiftPressed, + }) + + handleCeilingPlacementPoint(snappedPoint) + return + } + + if (isRoofBuildActive) { + const snappedPoint = getSnappedFloorplanPoint(planPoint) + emitFloorplanGridEvent('click', snappedPoint, event) + setCursorPoint(snappedPoint) + + if (!roofDraftStart) { + setRoofDraftStart(snappedPoint) + setRoofDraftEnd(snappedPoint) + } else { + clearRoofPlacementDraft() + } + return + } + + if (isFenceBuildActive) { + emitFloorplanGridEvent('click', planPoint, event) + + const snappedPoint = snapFenceDraftPoint({ + point: planPoint, + walls, + fences, + start: fenceDraftStart ?? undefined, + angleSnap: Boolean(fenceDraftStart) && !shiftPressed, + }) + + setCursorPoint(snappedPoint) + + if (!fenceDraftStart) { + setFenceDraftStart(snappedPoint) + setFenceDraftEnd(snappedPoint) + } else if (getPlanPointDistance(toPoint2D(fenceDraftStart), toPoint2D(snappedPoint)) >= 0.01) { + clearFencePlacementDraft() + } else { + setFenceDraftEnd(snappedPoint) + } + return + } + if (isFloorplanGridInteractionActive) { const snappedPoint = emitFloorplanGridEvent('click', planPoint, event) setCursorPoint(snappedPoint) @@ -10045,6 +10622,7 @@ export function FloorplanPanel() { if (canSelectElementFloorplanGeometry) { const hitId = getFloorplanHitNodeId({ point: toPoint2D(planPoint), + ceilings: ceilingPolygons, phase, isItemContextActive: isFloorplanItemContextActive, items: floorplanItemEntries, @@ -10119,12 +10697,16 @@ export function FloorplanPanel() { }, [ draftStart, + ceilingDraftPoints, emitFloorplanGridEvent, + fences, + fenceDraftStart, floorplanOpeningLocalY, getPlanPointFromClientPoint, activePolygonDraftPoints, canSelectElementFloorplanGeometry, canSelectFloorplanZones, + ceilingPolygons, displaySlabPolygons, displayWallPolygons, floorplanItemEntries, @@ -10133,13 +10715,19 @@ export function FloorplanPanel() { floorplanStairEntries, floorplanWallHitTolerance, getOpeningCenterLine, + handleCeilingPlacementPoint, handleSlabPlacementPoint, handleZonePlacementPoint, handleWallPlacementPoint, + clearFencePlacementDraft, + clearRoofPlacementDraft, + isCeilingBuildActive, + isFenceBuildActive, isFloorplanGridInteractionActive, isFloorplanItemContextActive, isOpeningPlacementActive, isPolygonBuildActive, + isRoofBuildActive, isWallBuildActive, isWindowBuildActive, isZoneBuildActive, @@ -10148,6 +10736,7 @@ export function FloorplanPanel() { movingOpeningType, openingsPolygons, phase, + roofDraftStart, setSelectedReferenceId, setSelection, shiftPressed, @@ -10158,7 +10747,7 @@ export function FloorplanPanel() { ) const handleBackgroundDoubleClick = useCallback( (event: ReactMouseEvent) => { - if (!isPolygonBuildActive) { + if (!(isPolygonDraftBuildActive && !isRoofBuildActive)) { return } @@ -10173,6 +10762,12 @@ export function FloorplanPanel() { angleSnap: activePolygonDraftPoints.length > 0 && !shiftPressed, }) + if (isCeilingBuildActive) { + emitFloorplanGridEvent('double-click', planPoint, event) + handleCeilingPlacementConfirm(snappedPoint) + return + } + if (isZoneBuildActive) { handleZonePlacementConfirm(snappedPoint) } else { @@ -10181,10 +10776,14 @@ export function FloorplanPanel() { }, [ activePolygonDraftPoints, + emitFloorplanGridEvent, + handleCeilingPlacementConfirm, getPlanPointFromClientPoint, handleSlabPlacementConfirm, handleZonePlacementConfirm, - isPolygonBuildActive, + isCeilingBuildActive, + isPolygonDraftBuildActive, + isRoofBuildActive, isZoneBuildActive, shiftPressed, ], @@ -10262,6 +10861,7 @@ export function FloorplanPanel() { const point = toPoint2D(planPoint) return getFloorplanHitNodeId({ point, + ceilings: ceilingPolygons, phase, isItemContextActive: isFloorplanItemContextActive, items: floorplanItemEntries, @@ -10276,6 +10876,7 @@ export function FloorplanPanel() { }) }, [ + ceilingPolygons, displaySlabPolygons, displayWallPolygons, floorplanItemEntries, @@ -10294,6 +10895,7 @@ export function FloorplanPanel() { (bounds: FloorplanSelectionBounds) => getSelectionIdsInBoundsFromTool({ bounds, + ceilings: ceilingPolygons, phase, isItemContextActive: isFloorplanItemContextActive, items: floorplanItemEntries, @@ -10304,6 +10906,7 @@ export function FloorplanPanel() { stairs: floorplanStairEntries, }), [ + ceilingPolygons, displaySlabPolygons, displayWallPolygons, floorplanItemEntries, @@ -10369,6 +10972,14 @@ export function FloorplanPanel() { [syncDeleteHoveredId], ) + const handleCeilingHoverChange = useCallback( + (ceilingId: CeilingNode['id'] | null) => { + setHoveredCeilingId(ceilingId) + syncDeleteHoveredId(ceilingId) + }, + [syncDeleteHoveredId], + ) + const handleItemHoverChange = useCallback( (itemId: ItemNode['id'] | null) => { setHoveredItemId(itemId) @@ -10398,6 +11009,7 @@ export function FloorplanPanel() { handleOpeningHoverChange(null) handleWallHoverChange(null) handleSlabHoverChange(null) + handleCeilingHoverChange(null) handleStairHoverChange(null) handleZoneHoverChange(null) handleItemHoverChange(itemId) @@ -10406,6 +11018,7 @@ export function FloorplanPanel() { handleFenceHoverChange, handleItemHoverChange, handleOpeningHoverChange, + handleCeilingHoverChange, handleSlabHoverChange, handleStairHoverChange, handleWallHoverChange, @@ -10418,6 +11031,7 @@ export function FloorplanPanel() { handleOpeningHoverChange(null) handleWallHoverChange(null) handleSlabHoverChange(null) + handleCeilingHoverChange(null) handleStairHoverChange(null) handleZoneHoverChange(null) handleFenceHoverChange(fenceId) @@ -10426,6 +11040,7 @@ export function FloorplanPanel() { handleFenceHoverChange, handleItemHoverChange, handleOpeningHoverChange, + handleCeilingHoverChange, handleSlabHoverChange, handleStairHoverChange, handleWallHoverChange, @@ -10438,6 +11053,7 @@ export function FloorplanPanel() { handleFenceHoverChange(null) handleOpeningHoverChange(null) handleSlabHoverChange(null) + handleCeilingHoverChange(null) handleWallHoverChange(null) handleZoneHoverChange(null) handleStairHoverChange(stairId) @@ -10446,6 +11062,7 @@ export function FloorplanPanel() { handleFenceHoverChange, handleItemHoverChange, handleOpeningHoverChange, + handleCeilingHoverChange, handleSlabHoverChange, handleStairHoverChange, handleWallHoverChange, @@ -10535,6 +11152,7 @@ export function FloorplanPanel() { | ItemNode['id'] | OpeningNode['id'] | SlabNode['id'] + | CeilingNode['id'] | StairNode['id'] | ZoneNodeType['id'], eventType: 'click' | 'double-click', @@ -10545,6 +11163,7 @@ export function FloorplanPanel() { !( node && (node.type === 'slab' || + node.type === 'ceiling' || node.type === 'door' || node.type === 'window' || node.type === 'item' || @@ -10734,6 +11353,12 @@ export function FloorplanPanel() { }, [emitFloorplanNodeClick], ) + const handleCeilingSelect = useCallback( + (ceilingId: CeilingNode['id'], event: ReactMouseEvent) => { + emitFloorplanNodeClick(ceilingId, 'click', event) + }, + [emitFloorplanNodeClick], + ) const handleZoneSelect = useCallback( (zoneId: ZoneNodeType['id'], event: ReactMouseEvent) => { emitFloorplanNodeClick(zoneId, 'click', event) @@ -10771,6 +11396,9 @@ export function FloorplanPanel() { const handleSlabDoubleClick = useCallback((slab: SlabNode) => { emitter.emit('camera-controls:focus', { nodeId: slab.id }) }, []) + const handleCeilingDoubleClick = useCallback((ceiling: CeilingNode) => { + emitter.emit('camera-controls:focus', { nodeId: ceiling.id }) + }, []) const handleOpeningDoubleClick = useCallback((opening: OpeningNode) => { emitter.emit('camera-controls:focus', { nodeId: opening.id }) }, []) @@ -11677,6 +12305,7 @@ export function FloorplanPanel() { handleItemHoverChange(null) handleWallHoverChange(null) handleSlabHoverChange(null) + handleCeilingHoverChange(null) handleStairHoverChange(null) handleZoneHoverChange(null) setHoveredEndpointId(null) @@ -11689,6 +12318,7 @@ export function FloorplanPanel() { } }, [ emitFloorplanWallLeave, + handleCeilingHoverChange, handleItemHoverChange, handleOpeningHoverChange, handleSlabHoverChange, @@ -12136,7 +12766,8 @@ export function FloorplanPanel() { selectedOpeningEntry, selectedStairEntry, ]) - const activeDraftAnchorPoint = draftStart ?? activePolygonDraftPoints[0] ?? null + const activeDraftAnchorPoint = + draftStart ?? fenceDraftStart ?? roofDraftStart ?? activePolygonDraftPoints[0] ?? null const floorplanCursorColor = mode === 'delete' ? palette.deleteStroke @@ -12272,7 +12903,10 @@ export function FloorplanPanel() { y={viewBox.minY} /> - + + {movingOpeningPlacementMeasurements.map((measurement) => ( + + + + + + + + + {measurement.label} + + + ))} + {selectedItemClearanceMeasurements.map((measurement) => ( { const onGridMove = (event: GridEvent) => { if (!(cursorRef.current && gridCursorRef.current)) return - const gridX = Math.round(event.position[0] * 2) / 2 - const gridZ = Math.round(event.position[2] * 2) / 2 + const gridX = Math.round(event.localPosition[0] * 2) / 2 + const gridZ = Math.round(event.localPosition[2] * 2) / 2 const gridPosition: [number, number] = [gridX, gridZ] setCursorPosition(gridPosition) - setLevelY(event.position[1]) + setLevelY(event.localPosition[1]) - const ceilingY = event.position[1] + CEILING_HEIGHT - const gridY = event.position[1] + GRID_OFFSET + const ceilingY = event.localPosition[1] + CEILING_HEIGHT + const gridY = event.localPosition[1] + GRID_OFFSET // Calculate snapped display position (bypass snap when Shift is held) const lastPoint = points[points.length - 1] diff --git a/packages/editor/src/components/tools/floorplan/selection-tool.ts b/packages/editor/src/components/tools/floorplan/selection-tool.ts index a8999b47..65203584 100644 --- a/packages/editor/src/components/tools/floorplan/selection-tool.ts +++ b/packages/editor/src/components/tools/floorplan/selection-tool.ts @@ -1,4 +1,5 @@ import type { + CeilingNode, DoorNode, ItemNode, RoofNode, @@ -46,6 +47,12 @@ type SlabEntry = { holes: Point2D[][] } +type CeilingEntry = { + ceiling: CeilingNode + polygon: Point2D[] + holes: Point2D[][] +} + type RoofEntry = { roof: RoofNode segments: Array<{ @@ -63,6 +70,7 @@ type FloorplanSelectionToolContext = { stairs: StairEntry[] walls: WallEntry[] slabs: SlabEntry[] + ceilings: CeilingEntry[] roofs: RoofEntry[] openingHitTolerance: number wallHitTolerance: number @@ -131,6 +139,13 @@ export function getFloorplanHitNodeId(context: FloorplanSelectionToolContext) { 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), ) @@ -150,6 +165,7 @@ type FloorplanSelectionBoundsContext = { walls: WallEntry[] openings: OpeningPolygonEntry[] slabs: SlabEntry[] + ceilings: CeilingEntry[] stairs: StairEntry[] roofs: RoofEntry[] } @@ -162,6 +178,7 @@ export function getFloorplanSelectionIdsInBounds({ walls, openings, slabs, + ceilings, stairs, roofs, }: FloorplanSelectionBoundsContext) { @@ -184,6 +201,9 @@ export function getFloorplanSelectionIdsInBounds({ 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) => @@ -198,6 +218,14 @@ export function getFloorplanSelectionIdsInBounds({ .map(({ roof }) => roof.id) return Array.from( - new Set([...itemIds, ...wallIds, ...openingIds, ...slabIds, ...stairIds, ...roofIds]), + new Set([ + ...itemIds, + ...wallIds, + ...openingIds, + ...slabIds, + ...ceilingIds, + ...stairIds, + ...roofIds, + ]), ) }