From ff147626bb71c1342690e0ea8e5bfe2ede58c92d Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Tue, 19 May 2026 19:07:59 -0400 Subject: [PATCH] floorplan-panel: remove dead action menu, handle layers, selected* state and handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continuing the post-migration cleanup. After registry-driven kinds absorbed selection chrome, action menus, and boundary editing through `def.floorplanAffordances`, the legacy plumbing in `floorplan-panel.tsx` was left mounted with empty inputs — another ~4k LoC of dead code. Removed: - `Editor2dFloorplanActionMenuLayer` mount + its 10 `selectedX ActionMenuPosition` useMemos. All ten action menus computed positions from empty entry arrays / null `selectedXEntry`, so the layer never rendered anything. `FloorplanRegistryActionMenu` is the only mount left. - Legacy handle layers: `FloorplanWallEndpointLayer`, `FloorplanFenceEndpointLayer`, `FloorplanWallCurveHandleLayer`, and the four `FloorplanPolygonHandleLayer` mounts for slab / slab-hole / ceiling / ceiling-hole — all rendered from `wallEndpointHandles` / `slabVertexHandles` / etc. which were empty after the registry took over endpoint, curve, and polygon affordances. - `FloorplanZoneLabelLayer` mount + component. Zone labels are now emitted as `kind: 'text'` from `def.floorplan` on the zone kind. - Dead handlers: every `handle*Select`, `handle*PointerDown`, `handle*DoubleClick`, `handle*HoverChange`, `handleFloorplan*Hover Enter`, `handleSelected*` (Move/Delete/Duplicate/AddHole/HoleMove/ HoleDelete/Curve for all 10 legacy kinds), `duplicateSelected*`, `handleSelectedWallCurve`, `handleSlabVertex*`/`Midpoint*`/`Edge*`/ `Hole*` (same for ceiling and zone), `handleWallEndpointPointerDown`, `handleFenceEndpointPointerDown`, `handleWallCurvePointerDown`, `emitFloorplanNodeClick`, `syncDeleteHoveredId`, `handleZoneLabelClick`, `hasDuplicatableFloorplanSelection`, `handleDuplicateFloorplanSelection`, `FloorplanDuplicateHotkey` mount. - Dead handle data: `wallEndpointHandles`, `fenceEndpointHandles`, `wallCurveHandles`, `canCurveSelectedWall`, and all the slab/slab-hole/ceiling/ceiling-hole/zone vertex+midpoint+edge handle useMemos. - Dead selection / measurement useMemos: `selectedItemEntry`, `selectedOpeningEntry`, `selectedSpawnEntry`, `selectedFenceEntry`, `selectedStairEntry`, `selectedRoofEntry`, `selectedElevatorEntry`, `selectedWallEntry`, plus `selectedItemClearanceMeasurements` and `movingOpeningPlacementMeasurements` (both 100-200 lines of math that consumed the empty entries). - Two dead `` mounts (clearance / opening placement) — both fed by useMemos that returned `[]`. - `` mount — `visibleZonePolygons` is always empty. Kept: - `siteVertexHandles` / `siteMidpointHandles` + `` mount for site — site is the only kind not registry-driven. - `FloorplanStairLayer` mount (preview-only, hover/click props swapped for noop helpers since the preview isn't interactive). - Stub `selectedSlabEntry` / `selectedCeilingEntry` / `selectedZoneEntry` as `null` typed values so the remaining hole-editing fallback code compiles. Those fallbacks themselves run as no-ops now and can be torn down in a follow-up. Floor-plan panel: 14,395 → 10,466 lines (-3,929). Cumulative reduction vs `main`: 17,913 → 10,466 (-7,447 lines, ~42%). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/editor/floorplan-panel.tsx | 4001 +---------------- 1 file changed, 36 insertions(+), 3965 deletions(-) diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index 3715327d..f25af51d 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -8,7 +8,7 @@ import { type CeilingNode, type ColumnNode, calculateLevelMiters, - DoorNode, + type DoorNode, type ElevatorNode, emitter, type FenceNode, @@ -17,10 +17,8 @@ import { getRenderableSlabPolygon, getWallChordFrame, getWallCurveLength, - getWallMidpointHandlePoint, getWallPlanFootprint, type ItemNode, - ItemNode as ItemNodeSchema, isCurvedWall, type LevelNode, loadAssetUrl, @@ -43,8 +41,7 @@ import { useLiveTransforms, useScene, type WallNode, - WallNode as WallNodeSchema, - WindowNode, + type WindowNode, ZoneNode as ZoneNodeSchema, type ZoneNode as ZoneNodeType, } from '@pascal-app/core' @@ -71,18 +68,12 @@ import { type FloorplanNodeTransform as SharedFloorplanNodeTransform, } from '../../lib/floorplan' import { guideEmitter } from '../../lib/guide-events' -import { duplicateRoofSubtree } from '../../lib/roof-duplication' import { sfxEmitter } from '../../lib/sfx-bus' -import { duplicateStairSubtree } from '../../lib/stair-duplication' import { cn } from '../../lib/utils' import type { GuideUiState } from '../../store/use-editor' import useEditor from '../../store/use-editor' -import { FloorplanActionMenuLayer as Editor2dFloorplanActionMenuLayer } from '../editor-2d/floorplan-action-menu-layer' import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay' -import { - FloorplanDuplicateHotkey, - FloorplanSiteKeyHandler, -} from '../editor-2d/floorplan-hotkey-handlers' +import { FloorplanSiteKeyHandler } from '../editor-2d/floorplan-hotkey-handlers' import { FloorplanRegistryActionMenu } from '../editor-2d/floorplan-registry-action-menu' import { FloorplanRegistryMoveOverlay } from '../editor-2d/floorplan-registry-move-overlay' import { @@ -91,10 +82,7 @@ import { } from '../editor-2d/floorplan-render-context' import { FloorplanDraftLayer } from '../editor-2d/renderers/floorplan-draft-layer' import { FloorplanMarqueeLayer } from '../editor-2d/renderers/floorplan-marquee-layer' -import { - FloorplanMeasurementsLayer, - type LinearMeasurementOverlay, -} from '../editor-2d/renderers/floorplan-measurements-layer' +import type { LinearMeasurementOverlay } from '../editor-2d/renderers/floorplan-measurements-layer' import { FloorplanRegistryLayer } from '../editor-2d/renderers/floorplan-registry-layer' import { FloorplanStairLayer } from '../editor-2d/renderers/floorplan-stair-layer' import { buildSvgPolylinePath, formatPolygonPath, getArcPlanPoint } from '../editor-2d/svg-paths' @@ -177,6 +165,7 @@ const FLOORPLAN_ITEM_HOVER_RING_STROKE_WIDTH = 2 const FLOORPLAN_WALL_STROKE_WIDTH = '1' const FLOORPLAN_SELECTED_WALL_STROKE_WIDTH = '1.5' const FLOORPLAN_OPENING_HIT_STROKE_WIDTH = 16 +const noopFloorplanStairHandler = () => {} const FLOORPLAN_OPENING_STROKE_WIDTH = 0.05 const FLOORPLAN_OPENING_DETAIL_STROKE_WIDTH = 0.02 const FLOORPLAN_OPENING_DASHED_STROKE_WIDTH = 0.02 @@ -4375,421 +4364,6 @@ function FloorplanZoneLabel({ ) } -const FloorplanZoneLabelLayer = memo(function FloorplanZoneLabelLayer({ - onLabelHoverChange, - onZoneLabelClick, - selectedZoneId, - svgRef, - viewBox, - zonePolygons, -}: { - onLabelHoverChange: (zoneId: ZoneNodeType['id'] | null) => void - onZoneLabelClick: (zoneId: ZoneNodeType['id'], event: ReactMouseEvent) => void - selectedZoneId: ZoneNodeType['id'] | null - svgRef: React.RefObject - viewBox: { minX: number; minY: number; width: number; height: number } - zonePolygons: ZonePolygonEntry[] -}) { - const [editingZoneId, setEditingZoneId] = useState(null) - - // Listen for edit-label events (from 2D label click or external triggers) - useEffect(() => { - const handler = (event: { zoneId: string }) => { - setEditingZoneId(event.zoneId as ZoneNodeType['id']) - } - emitter.on('zone:edit-label' as any, handler as any) - return () => { - emitter.off('zone:edit-label' as any, handler as any) - } - }, []) - - // Clear editing when selection changes away - useEffect(() => { - if (editingZoneId && selectedZoneId !== editingZoneId) { - setEditingZoneId(null) - } - }, [selectedZoneId, editingZoneId]) - - return ( - <> - {zonePolygons.map(({ zone, polygon }) => { - if (polygon.length < 3) return null - const rawCentroid = polygonCentroid(polygon) - const centroid = toSvgPoint(rawCentroid) - const isEditing = editingZoneId === zone.id - - if (isEditing) { - return ( - setEditingZoneId(null)} - svgRef={svgRef} - viewBox={viewBox} - zone={zone} - /> - ) - } - - return ( - - ) - })} - - ) -}) - -const FloorplanWallEndpointLayer = memo(function FloorplanWallEndpointLayer({ - endpointHandles, - hoveredEndpointId, - onWallEndpointPointerDown, - onEndpointHoverChange, - palette, - unitsPerPixel, -}: { - endpointHandles: Array<{ - wall: WallNode - endpoint: WallEndpoint - point: WallPlanPoint - isSelected: boolean - isActive: boolean - }> - onWallEndpointPointerDown: ( - wall: WallNode, - endpoint: WallEndpoint, - event: ReactPointerEvent, - ) => void - hoveredEndpointId: string | null - onEndpointHoverChange: (endpointId: string | null) => void - palette: FloorplanPalette - unitsPerPixel: number -}) { - return ( - <> - {endpointHandles.map(({ wall, endpoint, point, isSelected, isActive }) => { - const endpointId = `${wall.id}:${endpoint}` - const isHovered = hoveredEndpointId === endpointId - const stroke = - isSelected || isActive ? palette.endpointHandleActiveStroke : palette.endpointHandleStroke - const hoverStroke = - isSelected || isActive - ? palette.endpointHandleActiveStroke - : palette.endpointHandleHoverStroke - const outerRadius = - (isActive - ? FLOORPLAN_ENDPOINT_HANDLE_ACTIVE_RADIUS_PX - : isSelected - ? FLOORPLAN_ENDPOINT_HANDLE_SELECTED_RADIUS_PX - : FLOORPLAN_ENDPOINT_HANDLE_RADIUS_PX) * unitsPerPixel - const dotRadius = - (isActive - ? FLOORPLAN_ENDPOINT_HANDLE_ACTIVE_DOT_RADIUS_PX - : FLOORPLAN_ENDPOINT_HANDLE_DOT_RADIUS_PX) * unitsPerPixel - const svgPoint = toSvgPlanPoint(point) - - return ( - { - event.stopPropagation() - }} - onPointerEnter={() => onEndpointHoverChange(endpointId)} - onPointerLeave={() => onEndpointHoverChange(null)} - > - - - - - onWallEndpointPointerDown(wall, endpoint, event)} - pointerEvents="all" - r={outerRadius} - stroke="transparent" - strokeWidth={FLOORPLAN_ENDPOINT_HIT_STROKE_WIDTH} - style={{ cursor: EDITOR_CURSOR }} - vectorEffect="non-scaling-stroke" - /> - - ) - })} - - ) -}) - -const FloorplanFenceEndpointLayer = memo(function FloorplanFenceEndpointLayer({ - endpointHandles, - hoveredEndpointId, - onEndpointHoverChange, - onFenceEndpointPointerDown, - palette, - unitsPerPixel, -}: { - endpointHandles: Array<{ - fence: FenceNode - endpoint: WallEndpoint - point: WallPlanPoint - isActive: boolean - isSelected: boolean - }> - hoveredEndpointId: string | null - onEndpointHoverChange: (endpointId: string | null) => void - onFenceEndpointPointerDown: ( - fence: FenceNode, - endpoint: WallEndpoint, - event: ReactPointerEvent, - ) => void - palette: FloorplanPalette - unitsPerPixel: number -}) { - return ( - <> - {endpointHandles.map(({ fence, endpoint, point, isSelected, isActive }) => { - const endpointId = `${fence.id}:${endpoint}` - const isHovered = hoveredEndpointId === endpointId - const stroke = - isSelected || isActive ? palette.endpointHandleActiveStroke : palette.endpointHandleStroke - const hoverStroke = - isSelected || isActive - ? palette.endpointHandleActiveStroke - : palette.endpointHandleHoverStroke - const outerRadius = - (isActive - ? FLOORPLAN_ENDPOINT_HANDLE_ACTIVE_RADIUS_PX - : isSelected - ? FLOORPLAN_ENDPOINT_HANDLE_SELECTED_RADIUS_PX - : FLOORPLAN_ENDPOINT_HANDLE_RADIUS_PX) * unitsPerPixel - const dotRadius = - (isActive - ? FLOORPLAN_ENDPOINT_HANDLE_ACTIVE_DOT_RADIUS_PX - : FLOORPLAN_ENDPOINT_HANDLE_DOT_RADIUS_PX) * unitsPerPixel - const svgPoint = toSvgPlanPoint(point) - - return ( - { - event.stopPropagation() - }} - onPointerEnter={() => onEndpointHoverChange(endpointId)} - onPointerLeave={() => onEndpointHoverChange(null)} - > - - - - - onFenceEndpointPointerDown(fence, endpoint, event)} - pointerEvents="all" - r={outerRadius} - stroke="transparent" - strokeWidth={FLOORPLAN_ENDPOINT_HIT_STROKE_WIDTH} - style={{ cursor: EDITOR_CURSOR }} - vectorEffect="non-scaling-stroke" - /> - - ) - })} - - ) -}) - -const FloorplanWallCurveHandleLayer = memo(function FloorplanWallCurveHandleLayer({ - curveHandles, - hoveredHandleId, - onHandleHoverChange, - onWallCurvePointerDown, - palette, - unitsPerPixel, -}: { - curveHandles: Array<{ - wall: WallNode - point: WallPlanPoint - isActive: boolean - }> - hoveredHandleId: string | null - onHandleHoverChange: (handleId: string | null) => void - onWallCurvePointerDown: (wall: WallNode, event: ReactPointerEvent) => void - palette: FloorplanPalette - unitsPerPixel: number -}) { - return ( - <> - {curveHandles.map(({ wall, point, isActive }) => { - const handleId = `curve:${wall.id}` - const isHovered = hoveredHandleId === handleId - const stroke = palette.curveHandleStroke - const hoverStroke = palette.curveHandleHoverStroke - const svgPoint = toSvgPlanPoint(point) - const radius = - (isActive - ? FLOORPLAN_ENDPOINT_HANDLE_SELECTED_RADIUS_PX - : FLOORPLAN_ENDPOINT_HANDLE_RADIUS_PX) * unitsPerPixel - const dotRadius = FLOORPLAN_CURVE_HANDLE_DOT_RADIUS_PX * unitsPerPixel - - return ( - { - event.stopPropagation() - }} - onPointerEnter={() => onHandleHoverChange(handleId)} - onPointerLeave={() => onHandleHoverChange(null)} - > - - - - onWallCurvePointerDown(wall, event)} - pointerEvents="all" - r={radius} - stroke="transparent" - strokeWidth={FLOORPLAN_ENDPOINT_HIT_STROKE_WIDTH} - style={{ cursor: EDITOR_CURSOR }} - vectorEffect="non-scaling-stroke" - /> - - ) - })} - - ) -}) - const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({ edgeHandles = [], hoveredHandleId, @@ -5810,335 +5384,19 @@ export function FloorplanPanel() { const floorplanStairEntries = useMemo(() => [], []) // Roof / roof-segment fully registry-driven via def.floorplan. const floorplanRoofEntries = useMemo(() => [], []) - // Selection lookups against the legacy entry lists. The active-level - // door / window / item / spawn paths are registry-driven now, so each - // source array is permanently empty and the lookups always return - // `null`. Selection chrome for those kinds comes from - // `FloorplanRegistryLayer` reading `viewState.selected`. Wrapping the - // `null` in `useMemo` (instead of a bare literal) preserves the - // declared type at consumer sites — bare `null` would narrow to - // `never` after `if (!entry) return`, breaking every `entry.field` read. - const selectedOpeningEntry = useMemo(() => null, []) - const selectedItemEntry = useMemo(() => null, []) - const selectedSpawnEntry = useMemo(() => null, []) - const selectedElevatorEntry = useMemo(() => { - if (selectedIds.length !== 1) { - return null - } - - return floorplanElevatorEntries.find(({ elevator }) => elevator.id === selectedIds[0]) ?? null - }, [floorplanElevatorEntries, selectedIds]) - const selectedItemClearanceMeasurements = useMemo(() => { - if (!selectedItemEntry) { - return [] as LinearMeasurementOverlay[] - } - - const attachTo = selectedItemEntry.item.asset.attachTo - if (attachTo === 'wall' || attachTo === 'wall-side') { - return [] as LinearMeasurementOverlay[] - } - - const polygon = selectedItemEntry.polygon - if (polygon.length < 4 || displayWallPolygons.length === 0) { - return [] as LinearMeasurementOverlay[] - } - - const centroid = polygonCentroid(polygon) - - return polygon.flatMap((startPoint, index) => { - const endPoint = polygon[(index + 1) % polygon.length] - if (!endPoint) { - return [] - } - - const edgeVector = { - x: endPoint.x - startPoint.x, - y: endPoint.y - startPoint.y, - } - const tangent = normalizePlanVector(edgeVector) - if (!tangent) { - return [] - } - - let outwardNormal: Point2D = { - x: -tangent.y, - y: tangent.x, - } - const midpoint = { - x: (startPoint.x + endPoint.x) / 2, - y: (startPoint.y + endPoint.y) / 2, - } - const centroidVector = { - x: midpoint.x - centroid.x, - y: midpoint.y - centroid.y, - } - - if (dotPlanVectors(outwardNormal, centroidVector) < 0) { - outwardNormal = { - x: -outwardNormal.x, - y: -outwardNormal.y, - } - } - - let bestHit: { - point: Point2D - distance: number - } | null = null - - for (const { polygon: wallPolygon } of displayWallPolygons) { - for (let wallIndex = 0; wallIndex < wallPolygon.length; wallIndex += 1) { - const wallStart = wallPolygon[wallIndex] - const wallEnd = wallPolygon[(wallIndex + 1) % wallPolygon.length] - if (!(wallStart && wallEnd)) { - continue - } - - const wallEdgeVector = { - x: wallEnd.x - wallStart.x, - y: wallEnd.y - wallStart.y, - } - const wallTangent = normalizePlanVector(wallEdgeVector) - if (!wallTangent) { - continue - } - - if ( - Math.abs(dotPlanVectors(tangent, wallTangent)) < - FLOORPLAN_ITEM_CLEARANCE_EDGE_PARALLEL_THRESHOLD - ) { - continue - } - - const hit = getRaySegmentIntersection(midpoint, outwardNormal, wallStart, wallEnd) - if ( - !hit || - hit.rayDistance < FLOORPLAN_ITEM_CLEARANCE_MIN_DISTANCE || - hit.rayDistance > FLOORPLAN_ITEM_CLEARANCE_MAX_DISTANCE - ) { - continue - } - - if (!bestHit || hit.rayDistance < bestHit.distance) { - bestHit = { - point: hit.point, - distance: hit.rayDistance, - } - } - } - } - - if (!bestHit) { - return [] - } - - const overlay = getLinearMeasurementOverlay( - `${selectedItemEntry.item.id}:clearance:${index}`, - midpoint, - bestHit.point, - formatMeasurement(bestHit.distance, unit, calibratedMetersPerUnit), - { - extensionOvershoot: 0, - }, - ) - - return overlay ? [overlay] : [] - }) - }, [calibratedMetersPerUnit, 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, calibratedMetersPerUnit), - { - 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, calibratedMetersPerUnit), - { - 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 - }, [calibratedMetersPerUnit, displayWallPolygons, movingNode, openingsPolygons, unit]) - const selectedWallEntry = useMemo(() => { - if (selectedIds.length !== 1) { - return null - } - - return displayWallPolygons.find(({ wall }) => wall.id === selectedIds[0]) ?? null - }, [displayWallPolygons, selectedIds]) - const selectedFenceEntry = useMemo(() => { - if (selectedIds.length !== 1) { - return null - } - - return floorplanFenceEntries.find(({ fence }) => fence.id === selectedIds[0]) ?? null - }, [floorplanFenceEntries, selectedIds]) - const selectedStairEntry = useMemo(() => { - if (selectedIds.length !== 1) { - return null - } - - return floorplanStairEntries.find(({ stair }) => stair.id === selectedIds[0]) ?? null - }, [floorplanStairEntries, selectedIds]) - const selectedRoofEntry = useMemo(() => { - if (selectedIds.length !== 1) { - return null - } - - return floorplanRoofEntries.find(({ roof }) => roof.id === selectedIds[0]) ?? null - }, [floorplanRoofEntries, selectedIds]) + // Slab / ceiling / zone are registry-driven; the polygon-handle, hole + // editor, and boundary-edit affordances live on `def.floorplanAffordances`. + // These legacy lookups stay as null stubs so the hole-editing fallbacks + // that still reference them compile cleanly. + const selectedSlabEntry = null as SlabPolygonEntry | null + const selectedCeilingEntry = null as CeilingPolygonEntry | null + const selectedZoneEntry = null as ZonePolygonEntry | null const slabById = useMemo(() => new Map(slabs.map((slab) => [slab.id, slab] as const)), [slabs]) const zoneById = useMemo(() => new Map(zones.map((zone) => [zone.id, zone] as const)), [zones]) const ceilingById = useMemo( () => new Map(ceilings.map((ceiling) => [ceiling.id, ceiling] as const)), [ceilings], ) - const selectedSlabEntry = useMemo(() => { - if (selectedIds.length !== 1) { - return null - } - - return displaySlabPolygons.find(({ slab }) => slab.id === selectedIds[0]) ?? null - }, [displaySlabPolygons, selectedIds]) - const selectedCeilingEntry = useMemo(() => { - if (selectedIds.length !== 1) { - return null - } - - return displayCeilingPolygons.find(({ ceiling }) => ceiling.id === selectedIds[0]) ?? null - }, [displayCeilingPolygons, selectedIds]) - const selectedZoneEntry = useMemo(() => { - if (!selectedZoneId) { - return null - } - - return displayZonePolygons.find(({ zone }) => zone.id === selectedZoneId) ?? null - }, [displayZonePolygons, selectedZoneId]) const isSiteEditActive = phase === 'site' const isWallBuildActive = phase === 'structure' && mode === 'build' && tool === 'wall' @@ -6414,411 +5672,6 @@ export function FloorplanPanel() { return toSvgSelectionBounds(visibleMarqueeBounds) }, [visibleMarqueeBounds]) - const wallEndpointHandles = useMemo(() => { - if (isOpeningPlacementActive || movingNode) { - return [] - } - - return displayWallPolygons.flatMap(({ wall }) => { - const isSelected = selectedIdSet.has(wall.id) - const isVisible = isSelected || wallEndpointDraft?.wallId === wall.id - if (!isVisible) { - return [] - } - - return (['start', 'end'] as const).map((endpoint) => ({ - wall, - endpoint, - point: endpoint === 'start' ? wall.start : wall.end, - isSelected, - isActive: wallEndpointDraft?.wallId === wall.id && wallEndpointDraft.endpoint === endpoint, - })) - }) - }, [displayWallPolygons, isOpeningPlacementActive, movingNode, selectedIdSet, wallEndpointDraft]) - const fenceEndpointHandles = useMemo(() => { - if ( - isOpeningPlacementActive || - movingNode || - isFenceCurveActive || - mode !== 'select' || - floorplanSelectionTool !== 'click' || - !selectedFenceEntry - ) { - return [] - } - - return (['start', 'end'] as const).map((endpoint) => ({ - fence: selectedFenceEntry.fence, - endpoint, - point: endpoint === 'start' ? selectedFenceEntry.fence.start : selectedFenceEntry.fence.end, - isSelected: true, - isActive: - movingFenceEndpoint?.fence.id === selectedFenceEntry.fence.id && - movingFenceEndpoint.endpoint === endpoint, - })) - }, [ - floorplanSelectionTool, - isFenceCurveActive, - isOpeningPlacementActive, - mode, - movingFenceEndpoint, - movingNode, - selectedFenceEntry, - ]) - const wallCurveHandles = useMemo(() => { - if ( - isOpeningPlacementActive || - movingNode || - mode !== 'select' || - floorplanSelectionTool !== 'click' || - !selectedWallEntry - ) { - return [] - } - - const hasWallChildrenBlockingCurve = (selectedWallEntry.wall.children ?? []).some((childId) => { - const childNode = levelDescendantNodeById.get(childId as AnyNodeId) - if (!childNode) { - return false - } - - if (childNode.type === 'door' || childNode.type === 'window') { - return true - } - - if (childNode.type === 'item') { - const attachTo = childNode.asset?.attachTo - return attachTo === 'wall' || attachTo === 'wall-side' - } - - return false - }) - if (hasWallChildrenBlockingCurve) { - return [] - } - - const centerPoint = getWallMidpointHandlePoint(selectedWallEntry.wall) - - return [ - { - wall: selectedWallEntry.wall, - point: [centerPoint.x, centerPoint.y] as WallPlanPoint, - isActive: wallCurveDraft?.wallId === selectedWallEntry.wall.id, - }, - ] - }, [ - floorplanSelectionTool, - isOpeningPlacementActive, - mode, - movingNode, - levelDescendantNodeById, - selectedWallEntry, - wallCurveDraft, - ]) - const canCurveSelectedWall = wallCurveHandles.length > 0 - const slabVertexHandles = useMemo(() => { - if (!shouldShowSlabBoundaryHandles) { - return [] - } - - const rawPolygon = selectedSlabEntry.polygon - - return getSlabHandlePolygon(selectedSlabEntry).map((point) => { - const vertexIndex = getClosestPolygonVertexIndex(point, rawPolygon) - - return { - nodeId: selectedSlabEntry.slab.id, - vertexIndex, - point: toWallPlanPoint(point), - isActive: - slabVertexDragState?.slabId === selectedSlabEntry.slab.id && - slabVertexDragState.vertexIndex === vertexIndex, - } - }) - }, [selectedSlabEntry, shouldShowSlabBoundaryHandles, slabVertexDragState]) - const slabMidpointHandles = useMemo(() => { - if (!(shouldShowSlabBoundaryHandles && !slabVertexDragState)) { - return [] - } - - const handlePolygon = getSlabHandlePolygon(selectedSlabEntry) - - return handlePolygon.map((point, edgeIndex, polygon) => { - const nextPoint = polygon[(edgeIndex + 1) % polygon.length] - const midpoint = { - x: (point.x + (nextPoint?.x ?? point.x)) / 2, - y: (point.y + (nextPoint?.y ?? point.y)) / 2, - } - - return { - nodeId: selectedSlabEntry.slab.id, - edgeIndex, - point: [midpoint.x, midpoint.y] as WallPlanPoint, - } - }) - }, [selectedSlabEntry, shouldShowSlabBoundaryHandles, slabVertexDragState]) - const slabEdgeHandles = useMemo(() => { - if (!shouldShowSlabBoundaryHandles) { - return [] - } - - const handlePolygon = getSlabHandlePolygon(selectedSlabEntry).map(toWallPlanPoint) - - return handlePolygon.flatMap((start, edgeIndex, polygon) => { - const end = polygon[(edgeIndex + 1) % polygon.length] - if (!end) { - return [] - } - - return [ - { - nodeId: selectedSlabEntry.slab.id, - edgeIndex, - start, - end, - isActive: - slabVertexDragState?.slabId === selectedSlabEntry.slab.id && - slabVertexDragState.mode === 'edge' && - slabVertexDragState.edgeIndex === edgeIndex, - }, - ] - }) - }, [selectedSlabEntry, shouldShowSlabBoundaryHandles, slabVertexDragState]) - const ceilingVertexHandles = useMemo(() => { - if (!shouldShowCeilingBoundaryHandles) { - return [] - } - - return selectedCeilingEntry.polygon.map((point, vertexIndex) => ({ - nodeId: selectedCeilingEntry.ceiling.id, - vertexIndex, - point: toWallPlanPoint(point), - isActive: - ceilingVertexDragState?.ceilingId === selectedCeilingEntry.ceiling.id && - ceilingVertexDragState.vertexIndex === vertexIndex, - })) - }, [ceilingVertexDragState, selectedCeilingEntry, shouldShowCeilingBoundaryHandles]) - const ceilingMidpointHandles = useMemo(() => { - if (!(shouldShowCeilingBoundaryHandles && !ceilingVertexDragState)) { - return [] - } - - return selectedCeilingEntry.polygon.map((point, edgeIndex, polygon) => { - const nextPoint = polygon[(edgeIndex + 1) % polygon.length] - - return { - nodeId: selectedCeilingEntry.ceiling.id, - edgeIndex, - point: [ - (point.x + (nextPoint?.x ?? point.x)) / 2, - (point.y + (nextPoint?.y ?? point.y)) / 2, - ] as WallPlanPoint, - } - }) - }, [ceilingVertexDragState, selectedCeilingEntry, shouldShowCeilingBoundaryHandles]) - const ceilingEdgeHandles = useMemo(() => { - if (!shouldShowCeilingBoundaryHandles) { - return [] - } - - return selectedCeilingEntry.polygon.flatMap((point, edgeIndex, polygon) => { - const nextPoint = polygon[(edgeIndex + 1) % polygon.length] - if (!nextPoint) { - return [] - } - - return [ - { - nodeId: selectedCeilingEntry.ceiling.id, - edgeIndex, - start: toWallPlanPoint(point), - end: toWallPlanPoint(nextPoint), - isActive: - ceilingVertexDragState?.ceilingId === selectedCeilingEntry.ceiling.id && - ceilingVertexDragState.mode === 'edge' && - ceilingVertexDragState.edgeIndex === edgeIndex, - }, - ] - }) - }, [ceilingVertexDragState, selectedCeilingEntry, shouldShowCeilingBoundaryHandles]) - const slabHoleVertexHandles = useMemo(() => { - if ( - !( - shouldShowSlabHoleBoundaryHandles && - selectedSlabEntry && - selectedSlabEditingHole && - selectedSlabEditingHoleIndex !== null - ) - ) { - return [] - } - - return selectedSlabEditingHole.map((point, vertexIndex) => ({ - nodeId: selectedSlabEntry.slab.id, - vertexIndex, - point: toWallPlanPoint(point), - isActive: - slabHoleVertexDragState?.slabId === selectedSlabEntry.slab.id && - slabHoleVertexDragState.holeIndex === selectedSlabEditingHoleIndex && - slabHoleVertexDragState.vertexIndex === vertexIndex, - })) - }, [ - selectedSlabEditingHole, - selectedSlabEditingHoleIndex, - selectedSlabEntry, - shouldShowSlabHoleBoundaryHandles, - slabHoleVertexDragState, - ]) - const slabHoleMidpointHandles = useMemo(() => { - if ( - !( - shouldShowSlabHoleBoundaryHandles && - selectedSlabEntry && - selectedSlabEditingHole && - !slabHoleVertexDragState - ) - ) { - return [] - } - - return selectedSlabEditingHole.map((point, edgeIndex, polygon) => { - const nextPoint = polygon[(edgeIndex + 1) % polygon.length] - - return { - nodeId: selectedSlabEntry.slab.id, - edgeIndex, - point: [ - (point.x + (nextPoint?.x ?? point.x)) / 2, - (point.y + (nextPoint?.y ?? point.y)) / 2, - ] as WallPlanPoint, - } - }) - }, [ - selectedSlabEditingHole, - selectedSlabEntry, - shouldShowSlabHoleBoundaryHandles, - slabHoleVertexDragState, - ]) - const slabHoleEdgeHandles = useMemo(() => { - if (!(shouldShowSlabHoleBoundaryHandles && selectedSlabEntry && selectedSlabEditingHole)) { - return [] - } - - return selectedSlabEditingHole.flatMap((point, edgeIndex, polygon) => { - const nextPoint = polygon[(edgeIndex + 1) % polygon.length] - if (!nextPoint) { - return [] - } - - return [ - { - nodeId: selectedSlabEntry.slab.id, - edgeIndex, - start: toWallPlanPoint(point), - end: toWallPlanPoint(nextPoint), - isActive: - slabHoleVertexDragState?.slabId === selectedSlabEntry.slab.id && - slabHoleVertexDragState.mode === 'edge' && - slabHoleVertexDragState.edgeIndex === edgeIndex, - }, - ] - }) - }, [ - selectedSlabEditingHole, - selectedSlabEntry, - shouldShowSlabHoleBoundaryHandles, - slabHoleVertexDragState, - ]) - const ceilingHoleVertexHandles = useMemo(() => { - if ( - !( - shouldShowCeilingHoleBoundaryHandles && - selectedCeilingEntry && - selectedCeilingEditingHole && - selectedCeilingEditingHoleIndex !== null - ) - ) { - return [] - } - - return selectedCeilingEditingHole.map((point, vertexIndex) => ({ - nodeId: selectedCeilingEntry.ceiling.id, - vertexIndex, - point: toWallPlanPoint(point), - isActive: - ceilingHoleVertexDragState?.ceilingId === selectedCeilingEntry.ceiling.id && - ceilingHoleVertexDragState.holeIndex === selectedCeilingEditingHoleIndex && - ceilingHoleVertexDragState.vertexIndex === vertexIndex, - })) - }, [ - ceilingHoleVertexDragState, - selectedCeilingEditingHole, - selectedCeilingEditingHoleIndex, - selectedCeilingEntry, - shouldShowCeilingHoleBoundaryHandles, - ]) - const ceilingHoleMidpointHandles = useMemo(() => { - if ( - !( - shouldShowCeilingHoleBoundaryHandles && - selectedCeilingEntry && - selectedCeilingEditingHole && - !ceilingHoleVertexDragState - ) - ) { - return [] - } - - return selectedCeilingEditingHole.map((point, edgeIndex, polygon) => { - const nextPoint = polygon[(edgeIndex + 1) % polygon.length] - - return { - nodeId: selectedCeilingEntry.ceiling.id, - edgeIndex, - point: [ - (point.x + (nextPoint?.x ?? point.x)) / 2, - (point.y + (nextPoint?.y ?? point.y)) / 2, - ] as WallPlanPoint, - } - }) - }, [ - ceilingHoleVertexDragState, - selectedCeilingEditingHole, - selectedCeilingEntry, - shouldShowCeilingHoleBoundaryHandles, - ]) - const ceilingHoleEdgeHandles = useMemo(() => { - if ( - !(shouldShowCeilingHoleBoundaryHandles && selectedCeilingEntry && selectedCeilingEditingHole) - ) { - return [] - } - - return selectedCeilingEditingHole.flatMap((point, edgeIndex, polygon) => { - const nextPoint = polygon[(edgeIndex + 1) % polygon.length] - if (!nextPoint) { - return [] - } - - return [ - { - nodeId: selectedCeilingEntry.ceiling.id, - edgeIndex, - start: toWallPlanPoint(point), - end: toWallPlanPoint(nextPoint), - isActive: - ceilingHoleVertexDragState?.ceilingId === selectedCeilingEntry.ceiling.id && - ceilingHoleVertexDragState.mode === 'edge' && - ceilingHoleVertexDragState.edgeIndex === edgeIndex, - }, - ] - }) - }, [ - ceilingHoleVertexDragState, - selectedCeilingEditingHole, - selectedCeilingEntry, - shouldShowCeilingHoleBoundaryHandles, - ]) const siteVertexHandles = useMemo(() => { if (!(shouldShowSiteBoundaryHandles && visibleSitePolygon)) { return [] @@ -6850,37 +5703,6 @@ export function FloorplanPanel() { } }) }, [shouldShowSiteBoundaryHandles, siteVertexDragState, visibleSitePolygon]) - const zoneVertexHandles = useMemo(() => { - if (!shouldShowZoneBoundaryHandles) { - return [] - } - - return selectedZoneEntry.polygon.map((point, vertexIndex) => ({ - nodeId: selectedZoneEntry.zone.id, - vertexIndex, - point: toWallPlanPoint(point), - isActive: - zoneVertexDragState?.zoneId === selectedZoneEntry.zone.id && - zoneVertexDragState.vertexIndex === vertexIndex, - })) - }, [selectedZoneEntry, shouldShowZoneBoundaryHandles, zoneVertexDragState]) - const zoneMidpointHandles = useMemo(() => { - if (!(shouldShowZoneBoundaryHandles && !zoneVertexDragState)) { - return [] - } - - return selectedZoneEntry.polygon.map((point, edgeIndex, polygon) => { - const nextPoint = polygon[(edgeIndex + 1) % polygon.length] - return { - nodeId: selectedZoneEntry.zone.id, - edgeIndex, - point: [ - (point.x + (nextPoint?.x ?? point.x)) / 2, - (point.y + (nextPoint?.y ?? point.y)) / 2, - ] as WallPlanPoint, - } - }) - }, [selectedZoneEntry, shouldShowZoneBoundaryHandles, zoneVertexDragState]) const draftPolygon = useMemo(() => { if (!(levelId && draftStart && draftEnd && isWallLongEnough(draftStart, draftEnd))) { @@ -7185,192 +6007,6 @@ export function FloorplanPanel() { () => Math.max(floorplanWorldUnitsPerPixel * 0.55, 0.0001), [floorplanWorldUnitsPerPixel], ) - const selectedOpeningActionMenuPosition = useMemo( - () => - selectedOpeningEntry - ? getFloorplanActionMenuPosition( - selectedOpeningEntry.polygon, - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null, - [floorplanSceneRotationDeg, selectedOpeningEntry, surfaceSize, viewBox], - ) - const selectedItemActionMenuPosition = useMemo( - () => - selectedItemEntry - ? getFloorplanActionMenuPosition( - selectedItemEntry.polygon, - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null, - [floorplanSceneRotationDeg, selectedItemEntry, surfaceSize, viewBox], - ) - const selectedSpawnActionMenuPosition = useMemo(() => { - if (!selectedSpawnEntry) { - return null - } - - const { position } = selectedSpawnEntry - const svg = svgRef.current - const scene = floorplanSceneRef.current - const sceneCtm = scene?.getScreenCTM() - const hasResolvedSceneRotation = Number.isFinite(floorplanSceneRotationDeg) - - if (svg && scene && sceneCtm && hasResolvedSceneRotation) { - const svgRect = svg.getBoundingClientRect() - const svgPoint = svg.createSVGPoint() - svgPoint.x = toSvgX(position.x) - svgPoint.y = toSvgY(position.y) - FLOORPLAN_SPAWN_HIT_RADIUS - - const screenPoint = svgPoint.matrixTransform(sceneCtm) - const anchorX = screenPoint.x - svgRect.left - const anchorY = screenPoint.y - svgRect.top - - return { - x: Math.min( - Math.max(anchorX, FLOORPLAN_ACTION_MENU_HORIZONTAL_PADDING), - surfaceSize.width - FLOORPLAN_ACTION_MENU_HORIZONTAL_PADDING, - ), - y: Math.max(anchorY, FLOORPLAN_ACTION_MENU_MIN_ANCHOR_Y), - } - } - - return getFloorplanActionMenuPosition( - [ - { x: position.x - FLOORPLAN_SPAWN_HIT_RADIUS, y: position.y - FLOORPLAN_SPAWN_HIT_RADIUS }, - { x: position.x + FLOORPLAN_SPAWN_HIT_RADIUS, y: position.y - FLOORPLAN_SPAWN_HIT_RADIUS }, - { x: position.x + FLOORPLAN_SPAWN_HIT_RADIUS, y: position.y + FLOORPLAN_SPAWN_HIT_RADIUS }, - { x: position.x - FLOORPLAN_SPAWN_HIT_RADIUS, y: position.y + FLOORPLAN_SPAWN_HIT_RADIUS }, - ], - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - }, [floorplanSceneRotationDeg, selectedSpawnEntry, surfaceSize, viewBox]) - const selectedElevatorActionMenuPosition = useMemo( - () => - selectedElevatorEntry - ? getFloorplanActionMenuPosition( - selectedElevatorEntry.polygon, - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null, - [floorplanSceneRotationDeg, selectedElevatorEntry, surfaceSize, viewBox], - ) - const selectedSlabActionMenuPosition = useMemo(() => { - if (slabHoleMoveDraft) { - return null - } - - if (selectedSlabEditingHole) { - return getFloorplanActionMenuPosition( - selectedSlabEditingHole, - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - } - - return selectedSlabEntry - ? getFloorplanActionMenuPosition( - getSlabHandlePolygon(selectedSlabEntry), - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null - }, [ - floorplanSceneRotationDeg, - selectedSlabEditingHole, - selectedSlabEntry, - slabHoleMoveDraft, - surfaceSize, - viewBox, - ]) - const selectedCeilingActionMenuPosition = useMemo(() => { - if (ceilingHoleMoveDraft) { - return null - } - - if (selectedCeilingEditingHole) { - return getFloorplanActionMenuPosition( - selectedCeilingEditingHole, - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - } - - return selectedCeilingEntry - ? getFloorplanActionMenuPosition( - selectedCeilingEntry.polygon, - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null - }, [ - ceilingHoleMoveDraft, - floorplanSceneRotationDeg, - selectedCeilingEditingHole, - selectedCeilingEntry, - surfaceSize, - viewBox, - ]) - const selectedWallActionMenuPosition = useMemo( - () => - selectedWallEntry - ? getFloorplanActionMenuPosition( - selectedWallEntry.polygon, - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null, - [floorplanSceneRotationDeg, selectedWallEntry, surfaceSize, viewBox], - ) - const selectedFenceActionMenuPosition = useMemo( - () => - selectedFenceEntry - ? getFloorplanActionMenuPosition( - selectedFenceEntry.centerline, - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null, - [floorplanSceneRotationDeg, selectedFenceEntry, surfaceSize, viewBox], - ) - const selectedStairActionMenuPosition = useMemo( - () => - selectedStairEntry - ? getFloorplanActionMenuPosition( - selectedStairEntry.hitPolygons.flat(), - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null, - [floorplanSceneRotationDeg, selectedStairEntry, surfaceSize, viewBox], - ) - const selectedRoofActionMenuPosition = useMemo( - () => - selectedRoofEntry - ? getFloorplanActionMenuPosition( - selectedRoofEntry.segments.flatMap(({ polygon }) => polygon), - viewBox, - surfaceSize, - floorplanSceneRotationDeg, - ) - : null, - [floorplanSceneRotationDeg, selectedRoofEntry, surfaceSize, viewBox], - ) const floorplanCursorAnchorPosition = useMemo(() => { if ( cursorPoint && @@ -10720,351 +9356,6 @@ export function FloorplanPanel() { [setPreviewSelectedIds], ) - const syncDeleteHoveredId = useCallback( - (nodeId: string | null) => { - if (!isDeleteMode) { - return - } - - useViewer.getState().setHoveredId(nodeId as AnyNodeId | null) - }, - [isDeleteMode], - ) - - const handleWallHoverChange = useCallback( - (wallId: WallNode['id'] | null) => { - setHoveredWallId(wallId) - syncDeleteHoveredId(wallId) - }, - [syncDeleteHoveredId], - ) - const handleFenceHoverChange = useCallback( - (fenceId: FenceNode['id'] | null) => { - setHoveredFenceId(fenceId) - syncDeleteHoveredId(fenceId) - }, - [syncDeleteHoveredId], - ) - - const handleOpeningHoverChange = useCallback( - (openingId: OpeningNode['id'] | null) => { - setHoveredOpeningId(openingId) - syncDeleteHoveredId(openingId) - }, - [syncDeleteHoveredId], - ) - - const handleSlabHoverChange = useCallback( - (slabId: SlabNode['id'] | null) => { - setHoveredSlabId(slabId) - syncDeleteHoveredId(slabId) - }, - [syncDeleteHoveredId], - ) - - const handleCeilingHoverChange = useCallback( - (ceilingId: CeilingNode['id'] | null) => { - setHoveredCeilingId(ceilingId) - syncDeleteHoveredId(ceilingId) - }, - [syncDeleteHoveredId], - ) - - const handleItemHoverChange = useCallback( - (itemId: ItemNode['id'] | null) => { - setHoveredItemId(itemId) - syncDeleteHoveredId(itemId) - }, - [syncDeleteHoveredId], - ) - - const handleSpawnHoverChange = useCallback( - (spawnId: SpawnNode['id'] | null) => { - setHoveredSpawnId(spawnId) - syncDeleteHoveredId(spawnId) - }, - [syncDeleteHoveredId], - ) - - const handleStairHoverChange = useCallback( - (stairId: StairNode['id'] | null) => { - setHoveredStairId(stairId) - syncDeleteHoveredId(stairId) - }, - [syncDeleteHoveredId], - ) - - const handleElevatorHoverChange = useCallback( - (elevatorId: ElevatorNode['id'] | null) => { - setHoveredElevatorId(elevatorId) - syncDeleteHoveredId(elevatorId) - }, - [syncDeleteHoveredId], - ) - - const handleZoneHoverChange = useCallback( - (zoneId: ZoneNodeType['id'] | null) => { - setHoveredZoneId(zoneId) - syncDeleteHoveredId(zoneId) - }, - [syncDeleteHoveredId], - ) - const handleFloorplanItemHoverEnter = useCallback( - (itemId: ItemNode['id']) => { - handleFenceHoverChange(null) - handleOpeningHoverChange(null) - handleWallHoverChange(null) - handleSlabHoverChange(null) - handleCeilingHoverChange(null) - handleStairHoverChange(null) - handleElevatorHoverChange(null) - handleSpawnHoverChange(null) - handleZoneHoverChange(null) - handleItemHoverChange(itemId) - }, - [ - handleElevatorHoverChange, - handleFenceHoverChange, - handleItemHoverChange, - handleOpeningHoverChange, - handleCeilingHoverChange, - handleSlabHoverChange, - handleSpawnHoverChange, - handleStairHoverChange, - handleWallHoverChange, - handleZoneHoverChange, - ], - ) - const handleFloorplanFenceHoverEnter = useCallback( - (fenceId: FenceNode['id']) => { - handleItemHoverChange(null) - handleOpeningHoverChange(null) - handleWallHoverChange(null) - handleSlabHoverChange(null) - handleCeilingHoverChange(null) - handleStairHoverChange(null) - handleElevatorHoverChange(null) - handleSpawnHoverChange(null) - handleZoneHoverChange(null) - handleFenceHoverChange(fenceId) - }, - [ - handleElevatorHoverChange, - handleFenceHoverChange, - handleItemHoverChange, - handleOpeningHoverChange, - handleCeilingHoverChange, - handleSlabHoverChange, - handleSpawnHoverChange, - handleStairHoverChange, - handleWallHoverChange, - handleZoneHoverChange, - ], - ) - const handleFloorplanStairHoverEnter = useCallback( - (stairId: StairNode['id']) => { - handleItemHoverChange(null) - handleFenceHoverChange(null) - handleOpeningHoverChange(null) - handleSlabHoverChange(null) - handleCeilingHoverChange(null) - handleWallHoverChange(null) - handleSpawnHoverChange(null) - handleElevatorHoverChange(null) - handleZoneHoverChange(null) - handleStairHoverChange(stairId) - }, - [ - handleElevatorHoverChange, - handleFenceHoverChange, - handleItemHoverChange, - handleOpeningHoverChange, - handleCeilingHoverChange, - handleSlabHoverChange, - handleSpawnHoverChange, - handleStairHoverChange, - handleWallHoverChange, - handleZoneHoverChange, - ], - ) - const handleFloorplanSpawnHoverEnter = useCallback( - (spawnId: SpawnNode['id']) => { - handleItemHoverChange(null) - handleFenceHoverChange(null) - handleOpeningHoverChange(null) - handleSlabHoverChange(null) - handleCeilingHoverChange(null) - handleWallHoverChange(null) - handleStairHoverChange(null) - handleElevatorHoverChange(null) - handleZoneHoverChange(null) - handleSpawnHoverChange(spawnId) - }, - [ - handleCeilingHoverChange, - handleElevatorHoverChange, - handleFenceHoverChange, - handleItemHoverChange, - handleOpeningHoverChange, - handleSlabHoverChange, - handleSpawnHoverChange, - handleStairHoverChange, - handleWallHoverChange, - handleZoneHoverChange, - ], - ) - const handleFloorplanElevatorHoverEnter = useCallback( - (elevatorId: ElevatorNode['id']) => { - handleItemHoverChange(null) - handleFenceHoverChange(null) - handleOpeningHoverChange(null) - handleSlabHoverChange(null) - handleCeilingHoverChange(null) - handleWallHoverChange(null) - handleStairHoverChange(null) - handleSpawnHoverChange(null) - handleZoneHoverChange(null) - handleElevatorHoverChange(elevatorId) - }, - [ - handleCeilingHoverChange, - handleElevatorHoverChange, - handleFenceHoverChange, - handleItemHoverChange, - handleOpeningHoverChange, - handleSlabHoverChange, - handleSpawnHoverChange, - handleStairHoverChange, - handleWallHoverChange, - handleZoneHoverChange, - ], - ) - - const handleWallSelect = useCallback( - (wall: WallNode) => { - commitFloorplanSelection([wall.id]) - }, - [commitFloorplanSelection], - ) - - const handleWallClick = useCallback( - (wall: WallNode, event: ReactMouseEvent) => { - const centerX = (wall.start[0] + wall.end[0]) / 2 - const centerZ = (wall.start[1] + wall.end[1]) / 2 - const halfLength = Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]) / 2 - const localY = isOpeningPlacementActive ? floorplanOpeningLocalY : 0 - - setSelectedReferenceId(null) - emitter.emit('wall:click', { - node: wall, - position: [centerX, 0, centerZ], - localPosition: [halfLength, localY, 0], - stopPropagation: () => event.stopPropagation(), - nativeEvent: event.nativeEvent as any, - } as any) - }, - [floorplanOpeningLocalY, isOpeningPlacementActive, setSelectedReferenceId], - ) - - const handleWallDoubleClick = useCallback( - (wall: WallNode, event: ReactMouseEvent) => { - const centerX = (wall.start[0] + wall.end[0]) / 2 - const centerZ = (wall.start[1] + wall.end[1]) / 2 - const halfLength = Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]) / 2 - - emitter.emit('wall:double-click', { - node: wall, - position: [centerX, 0, centerZ], - localPosition: [halfLength, 0, 0], - stopPropagation: () => event.stopPropagation(), - nativeEvent: event.nativeEvent as any, - } as any) - emitter.emit('camera-controls:focus', { nodeId: wall.id }) - }, - [], - ) - const handleFenceClick = useCallback( - (fence: FenceNode, event: ReactMouseEvent) => { - const centerX = (fence.start[0] + fence.end[0]) / 2 - const centerZ = (fence.start[1] + fence.end[1]) / 2 - const halfLength = - Math.hypot(fence.end[0] - fence.start[0], fence.end[1] - fence.start[1]) / 2 - - setSelectedReferenceId(null) - emitter.emit('fence:click', { - node: fence, - position: [centerX, 0, centerZ], - localPosition: [halfLength, 0, 0], - stopPropagation: () => event.stopPropagation(), - nativeEvent: event.nativeEvent as any, - } as any) - }, - [setSelectedReferenceId], - ) - const handleFenceDoubleClick = useCallback( - (fence: FenceNode, event: ReactMouseEvent) => { - const centerX = (fence.start[0] + fence.end[0]) / 2 - const centerZ = (fence.start[1] + fence.end[1]) / 2 - const halfLength = - Math.hypot(fence.end[0] - fence.start[0], fence.end[1] - fence.start[1]) / 2 - - emitter.emit('fence:double-click', { - node: fence, - position: [centerX, 0, centerZ], - localPosition: [halfLength, 0, 0], - stopPropagation: () => event.stopPropagation(), - nativeEvent: event.nativeEvent as any, - } as any) - emitter.emit('camera-controls:focus', { nodeId: fence.id }) - }, - [], - ) - const emitFloorplanNodeClick = useCallback( - ( - nodeId: - | ItemNode['id'] - | OpeningNode['id'] - | SlabNode['id'] - | CeilingNode['id'] - | ElevatorNode['id'] - | SpawnNode['id'] - | StairNode['id'] - | ZoneNodeType['id'], - eventType: 'click' | 'double-click', - event: ReactMouseEvent, - ) => { - const node = useScene.getState().nodes[nodeId as AnyNodeId] - if ( - !( - node && - (node.type === 'slab' || - node.type === 'ceiling' || - node.type === 'door' || - node.type === 'window' || - node.type === 'elevator' || - node.type === 'item' || - node.type === 'spawn' || - node.type === 'stair' || - node.type === 'zone') - ) - ) { - return - } - - setSelectedReferenceId(null) - emitter.emit( - `${node.type}:${eventType}` as any, - { - localPosition: [0, 0, 0], - nativeEvent: event.nativeEvent as any, - node, - position: [0, 0, 0], - stopPropagation: () => event.stopPropagation(), - } as any, - ) - }, - [setSelectedReferenceId], - ) const handleGuideSelect = useCallback( (guideId: GuideNode['id']) => { setSelectedReferenceId(guideId) @@ -11191,1760 +9482,6 @@ export function FloorplanPanel() { [canInteractWithGuides, getSvgPointFromClientPoint, guideUi, selectedGuideId], ) - const handleOpeningSelect = useCallback( - (openingId: OpeningNode['id'], event: ReactMouseEvent) => { - emitFloorplanNodeClick(openingId, 'click', event) - }, - [emitFloorplanNodeClick], - ) - const handleOpeningPointerDown = useCallback( - (openingId: OpeningNode['id'], event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - const opening = selectedOpeningEntry?.opening - if (!opening || opening.id !== openingId) { - return - } - - event.preventDefault() - event.stopPropagation() - - // Suppress the click event that follows this pointer interaction so it - // doesn't re-select or interfere with placement. - const suppressClick = (clickEvent: MouseEvent) => { - clickEvent.stopImmediatePropagation() - clickEvent.preventDefault() - window.removeEventListener('click', suppressClick, true) - } - window.addEventListener('click', suppressClick, true) - requestAnimationFrame(() => { - window.removeEventListener('click', suppressClick, true) - }) - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(opening) - setSelection({ selectedIds: [] }) - }, - [selectedOpeningEntry, setMovingNode, setSelection], - ) - const handleSlabSelect = useCallback( - (slabId: SlabNode['id'], event: ReactMouseEvent) => { - emitFloorplanNodeClick(slabId, 'click', event) - }, - [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) - }, - [emitFloorplanNodeClick], - ) - const handleItemSelect = useCallback( - (itemId: ItemNode['id'], event: ReactMouseEvent) => { - emitFloorplanNodeClick(itemId, 'click', event) - }, - [emitFloorplanNodeClick], - ) - const handleSpawnSelect = useCallback( - (spawnId: SpawnNode['id'], event: ReactMouseEvent) => { - emitFloorplanNodeClick(spawnId, 'click', event) - }, - [emitFloorplanNodeClick], - ) - const handleStairSelect = useCallback( - (stairId: StairNode['id'], event: ReactMouseEvent) => { - emitFloorplanNodeClick(stairId, 'click', event) - }, - [emitFloorplanNodeClick], - ) - const handleElevatorSelect = useCallback( - (elevator: ElevatorNode, event: ReactMouseEvent) => { - emitFloorplanNodeClick(elevator.id, 'click', event) - }, - [emitFloorplanNodeClick], - ) - const handleElevatorPointerDown = useCallback( - (elevatorId: ElevatorNode['id'], event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - const elevator = selectedElevatorEntry?.elevator - if (!elevator || elevator.id !== elevatorId) { - return - } - - event.preventDefault() - event.stopPropagation() - - const suppressClick = (clickEvent: MouseEvent) => { - clickEvent.stopImmediatePropagation() - clickEvent.preventDefault() - window.removeEventListener('click', suppressClick, true) - } - window.addEventListener('click', suppressClick, true) - requestAnimationFrame(() => { - window.removeEventListener('click', suppressClick, true) - }) - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(elevator) - setSelection({ selectedIds: [] }) - }, - [selectedElevatorEntry, setMovingNode, setSelection], - ) - const handleZoneLabelClick = useCallback( - (zoneId: ZoneNodeType['id'], _event: ReactMouseEvent) => { - const currentZoneId = useViewer.getState().selection.zoneId - if (currentZoneId === zoneId) { - // Already selected → enter text editing (second click) - emitter.emit('zone:edit-label' as any, { zoneId }) - return - } - // Not selected → select zone + switch to zone mode - useEditor.getState().setPhase('structure') - useEditor.getState().setStructureLayer('zones') - useEditor.getState().setMode('select') - setSelection({ zoneId }) - }, - [setSelection], - ) - 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 }) - }, []) - const handleItemDoubleClick = useCallback( - (item: ItemNode, event: ReactMouseEvent) => { - emitFloorplanNodeClick(item.id, 'double-click', event) - emitter.emit('camera-controls:focus', { nodeId: item.id }) - }, - [emitFloorplanNodeClick], - ) - const handleSpawnDoubleClick = useCallback( - (spawn: SpawnNode, event: ReactMouseEvent) => { - emitFloorplanNodeClick(spawn.id, 'double-click', event) - emitter.emit('camera-controls:focus', { nodeId: spawn.id }) - }, - [emitFloorplanNodeClick], - ) - const handleSpawnPointerDown = useCallback( - (spawnId: SpawnNode['id'], event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - const spawn = selectedSpawnEntry?.spawn - if (!spawn || spawn.id !== spawnId) { - return - } - - event.preventDefault() - event.stopPropagation() - - const suppressClick = (clickEvent: MouseEvent) => { - clickEvent.stopImmediatePropagation() - clickEvent.preventDefault() - window.removeEventListener('click', suppressClick, true) - } - window.addEventListener('click', suppressClick, true) - requestAnimationFrame(() => { - window.removeEventListener('click', suppressClick, true) - }) - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(spawn) - setSelection({ selectedIds: [] }) - }, - [selectedSpawnEntry, setMovingNode, setSelection], - ) - const handleSelectedSpawnMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const spawn = selectedSpawnEntry?.spawn - if (!spawn) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(spawn) - setSelection({ selectedIds: [] }) - }, - [selectedSpawnEntry, setMovingNode, setSelection], - ) - const handleSelectedSpawnDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const spawn = selectedSpawnEntry?.spawn - if (!spawn) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(spawn.id as AnyNodeId) - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedSpawnEntry, setSelection], - ) - const handleSelectedElevatorMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const elevator = selectedElevatorEntry?.elevator - if (!elevator) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(elevator) - setSelection({ selectedIds: [] }) - }, - [selectedElevatorEntry, setMovingNode, setSelection], - ) - const handleSelectedElevatorDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const elevator = selectedElevatorEntry?.elevator - if (!elevator) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(elevator.id as AnyNodeId) - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedElevatorEntry, setSelection], - ) - const handleItemPointerDown = useCallback( - (itemId: ItemNode['id'], event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - const item = selectedItemEntry?.item - if (!item || item.id !== itemId) { - return - } - - event.preventDefault() - event.stopPropagation() - - // Suppress the click event that follows this pointer interaction so it - // doesn't re-select or interfere with placement. - const suppressClick = (clickEvent: MouseEvent) => { - clickEvent.stopImmediatePropagation() - clickEvent.preventDefault() - window.removeEventListener('click', suppressClick, true) - } - window.addEventListener('click', suppressClick, true) - requestAnimationFrame(() => { - window.removeEventListener('click', suppressClick, true) - }) - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(item) - setSelection({ selectedIds: [] }) - }, - [selectedItemEntry, setMovingNode, setSelection], - ) - const handleSelectedItemMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const item = selectedItemEntry?.item - if (!item) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(item) - setSelection({ selectedIds: [] }) - }, - [selectedItemEntry, setMovingNode, setSelection], - ) - const duplicateSelectedItem = useCallback(() => { - const item = selectedItemEntry?.item - if (!item) { - return - } - - sfxEmitter.emit('sfx:item-pick') - - const cloned = structuredClone(item) as Record - delete cloned.id - cloned.metadata = { - ...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}), - isNew: true, - } - cloned.children = [] - - try { - const duplicate = ItemNodeSchema.parse(cloned) - setMovingNode(duplicate) - setSelection({ selectedIds: [] }) - } catch (error) { - console.error('Failed to duplicate item', error) - } - }, [selectedItemEntry, setMovingNode, setSelection]) - const handleSelectedItemDuplicate = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - duplicateSelectedItem() - }, - [duplicateSelectedItem], - ) - const handleSelectedItemDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const item = selectedItemEntry?.item - if (!item) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(item.id as AnyNodeId) - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedItemEntry, setSelection], - ) - const handleSelectedWallMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const wall = selectedWallEntry?.wall - if (!wall) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(wall) - setSelection({ selectedIds: [] }) - }, - [selectedWallEntry, setMovingNode, setSelection], - ) - const duplicateSelectedWall = useCallback(() => { - const wall = selectedWallEntry?.wall - if (!wall?.parentId) { - return - } - - sfxEmitter.emit('sfx:item-pick') - - const cloned = structuredClone(wall) as Record - delete cloned.id - cloned.children = [] - cloned.metadata = { - ...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}), - isNew: true, - } - - const temporal = useScene.temporal.getState() - temporal.pause() - try { - const duplicate = WallNodeSchema.parse(cloned) - useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId) - setMovingNode(duplicate) - setSelection({ selectedIds: [] }) - } catch (error) { - console.error('Failed to duplicate wall', error) - } finally { - temporal.resume() - } - }, [selectedWallEntry, setMovingNode, setSelection]) - const handleSelectedWallDuplicate = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - duplicateSelectedWall() - }, - [duplicateSelectedWall], - ) - const handleSelectedWallCurve = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const wall = selectedWallEntry?.wall - if (!(wall && canCurveSelectedWall)) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setCurvingWall(wall) - setSelection({ selectedIds: [] }) - }, - [canCurveSelectedWall, selectedWallEntry, setCurvingWall, setSelection], - ) - const handleSelectedWallDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const wall = selectedWallEntry?.wall - if (!wall) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(wall.id as AnyNodeId) - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedWallEntry, setSelection], - ) - const handleSelectedSlabMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const slab = selectedSlabEntry?.slab - if (!slab) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(slab) - setSelection({ selectedIds: [] }) - }, - [selectedSlabEntry, setMovingNode, setSelection], - ) - const handleSelectedSlabAddHole = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const slab = selectedSlabEntry?.slab - if (!(slab && slab.polygon.length > 0)) { - return - } - - const [sumX, sumZ] = slab.polygon.reduce( - ([currentX, currentZ], [x, z]) => [currentX + x, currentZ + z], - [0, 0], - ) - const cx = sumX / slab.polygon.length - const cz = sumZ / slab.polygon.length - const holeSize = 0.5 - const newHole: Array<[number, number]> = [ - [cx - holeSize, cz - holeSize], - [cx + holeSize, cz - holeSize], - [cx + holeSize, cz + holeSize], - [cx - holeSize, cz + holeSize], - ] - const currentHoles = slab.holes ?? [] - const currentMetadata = currentHoles.map( - (_, index) => slab.holeMetadata?.[index] ?? { source: 'manual' as const }, - ) - - updateNode(slab.id, { - holes: [...currentHoles, newHole], - holeMetadata: [...currentMetadata, { source: 'manual' }], - }) - setEditingHole({ nodeId: slab.id, holeIndex: currentHoles.length }) - sfxEmitter.emit('sfx:structure-build') - }, - [selectedSlabEntry, setEditingHole, updateNode], - ) - const handleSelectedSlabHoleMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const slab = selectedSlabEntry?.slab - const holeIndex = selectedSlabEditingHoleIndex - const hole = selectedSlabEditingHole - if (!(slab && holeIndex !== null && hole && hole.length > 0)) { - return - } - - const planPoint = getPlanPointFromClientPoint(event.clientX, event.clientY) - const [sumX, sumY] = hole.reduce( - ([currentX, currentY], point) => [currentX + point.x, currentY + point.y], - [0, 0], - ) - const startPlanPoint = - planPoint ?? ([sumX / hole.length, sumY / hole.length] as WallPlanPoint) - const originalPolygon = hole.map(toWallPlanPoint) - - setSlabHoleBoundaryDraft(null) - setSlabHoleVertexDragState(null) - setSlabHoleMoveDraft({ - slabId: slab.id, - holeIndex, - polygon: originalPolygon, - originalPolygon, - startPlanPoint, - }) - setCursorPoint(startPlanPoint) - sfxEmitter.emit('sfx:item-pick') - }, - [ - getPlanPointFromClientPoint, - selectedSlabEditingHole, - selectedSlabEditingHoleIndex, - selectedSlabEntry, - ], - ) - const handleSelectedSlabHoleDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const slab = selectedSlabEntry?.slab - const holeIndex = selectedSlabEditingHoleIndex - if (!(slab && holeIndex !== null)) { - return - } - - const currentHoles = slab.holes ?? [] - if ( - !currentHoles[holeIndex] || - (slab.holeMetadata?.[holeIndex]?.source ?? 'manual') !== 'manual' - ) { - return - } - - const currentMetadata = currentHoles.map( - (_, index) => slab.holeMetadata?.[index] ?? { source: 'manual' as const }, - ) - updateNode(slab.id, { - holes: currentHoles.filter((_, index) => index !== holeIndex), - holeMetadata: currentMetadata.filter((_, index) => index !== holeIndex), - }) - setEditingHole(null) - setSlabHoleBoundaryDraft(null) - setSlabHoleMoveDraft(null) - setSlabHoleVertexDragState(null) - sfxEmitter.emit('sfx:item-delete') - }, - [selectedSlabEditingHoleIndex, selectedSlabEntry, setEditingHole, updateNode], - ) - const handleSelectedSlabDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const slab = selectedSlabEntry?.slab - if (!slab) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(slab.id as AnyNodeId) - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedSlabEntry, setSelection], - ) - const handleSelectedCeilingMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const ceiling = selectedCeilingEntry?.ceiling - if (!ceiling) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(ceiling) - setSelection({ selectedIds: [] }) - }, - [selectedCeilingEntry, setMovingNode, setSelection], - ) - const handleSelectedCeilingAddHole = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const ceiling = selectedCeilingEntry?.ceiling - if (!(ceiling && ceiling.polygon.length > 0)) { - return - } - - const [sumX, sumZ] = ceiling.polygon.reduce( - ([currentX, currentZ], [x, z]) => [currentX + x, currentZ + z], - [0, 0], - ) - const cx = sumX / ceiling.polygon.length - const cz = sumZ / ceiling.polygon.length - const holeSize = 0.5 - const newHole: Array<[number, number]> = [ - [cx - holeSize, cz - holeSize], - [cx + holeSize, cz - holeSize], - [cx + holeSize, cz + holeSize], - [cx - holeSize, cz + holeSize], - ] - const currentHoles = ceiling.holes ?? [] - const currentMetadata = currentHoles.map( - (_, index) => ceiling.holeMetadata?.[index] ?? { source: 'manual' as const }, - ) - - updateNode(ceiling.id, { - holes: [...currentHoles, newHole], - holeMetadata: [...currentMetadata, { source: 'manual' }], - }) - setEditingHole({ nodeId: ceiling.id, holeIndex: currentHoles.length }) - sfxEmitter.emit('sfx:structure-build') - }, - [selectedCeilingEntry, setEditingHole, updateNode], - ) - const handleSelectedCeilingHoleMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const ceiling = selectedCeilingEntry?.ceiling - const holeIndex = selectedCeilingEditingHoleIndex - const hole = selectedCeilingEditingHole - if (!(ceiling && holeIndex !== null && hole && hole.length > 0)) { - return - } - - const planPoint = getPlanPointFromClientPoint(event.clientX, event.clientY) - const [sumX, sumY] = hole.reduce( - ([currentX, currentY], point) => [currentX + point.x, currentY + point.y], - [0, 0], - ) - const startPlanPoint = - planPoint ?? ([sumX / hole.length, sumY / hole.length] as WallPlanPoint) - const originalPolygon = hole.map(toWallPlanPoint) - - setCeilingHoleBoundaryDraft(null) - setCeilingHoleVertexDragState(null) - setCeilingHoleMoveDraft({ - ceilingId: ceiling.id, - holeIndex, - polygon: originalPolygon, - originalPolygon, - startPlanPoint, - }) - setCursorPoint(startPlanPoint) - sfxEmitter.emit('sfx:item-pick') - }, - [ - getPlanPointFromClientPoint, - selectedCeilingEditingHole, - selectedCeilingEditingHoleIndex, - selectedCeilingEntry, - ], - ) - const handleSelectedCeilingHoleDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const ceiling = selectedCeilingEntry?.ceiling - const holeIndex = selectedCeilingEditingHoleIndex - if (!(ceiling && holeIndex !== null)) { - return - } - - const currentHoles = ceiling.holes ?? [] - if ( - !currentHoles[holeIndex] || - (ceiling.holeMetadata?.[holeIndex]?.source ?? 'manual') !== 'manual' - ) { - return - } - - const currentMetadata = currentHoles.map( - (_, index) => ceiling.holeMetadata?.[index] ?? { source: 'manual' as const }, - ) - updateNode(ceiling.id, { - holes: currentHoles.filter((_, index) => index !== holeIndex), - holeMetadata: currentMetadata.filter((_, index) => index !== holeIndex), - }) - setEditingHole(null) - setCeilingHoleBoundaryDraft(null) - setCeilingHoleMoveDraft(null) - setCeilingHoleVertexDragState(null) - sfxEmitter.emit('sfx:item-delete') - }, - [selectedCeilingEditingHoleIndex, selectedCeilingEntry, setEditingHole, updateNode], - ) - const handleSelectedCeilingDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const ceiling = selectedCeilingEntry?.ceiling - if (!ceiling) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(ceiling.id as AnyNodeId) - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedCeilingEntry, setSelection], - ) - const handleSelectedFenceMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const fence = selectedFenceEntry?.fence - if (!fence) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(fence) - setSelection({ selectedIds: [] }) - }, - [selectedFenceEntry, setMovingNode, setSelection], - ) - const handleSelectedFenceDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const fence = selectedFenceEntry?.fence - if (!fence) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(fence.id as AnyNodeId) - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedFenceEntry, setSelection], - ) - const handleFencePointerDown = useCallback( - (fenceId: FenceNode['id'], event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - const fence = selectedFenceEntry?.fence - if (!fence || fence.id !== fenceId) { - return - } - - event.preventDefault() - event.stopPropagation() - - pendingFenceDragRef.current = { - pointerId: event.pointerId, - fenceId, - startClientX: event.clientX, - startClientY: event.clientY, - } - }, - [selectedFenceEntry], - ) - const handleFenceEndpointPointerDown = useCallback( - (fence: FenceNode, endpoint: WallEndpoint, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - pendingFenceDragRef.current = null - setHoveredEndpointId(null) - - if (mode !== 'select') { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingFenceEndpoint({ fence, endpoint }) - }, - [mode, setMovingFenceEndpoint], - ) - const handleStairDoubleClick = useCallback( - (stair: StairNode, event: ReactMouseEvent) => { - emitFloorplanNodeClick(stair.id, 'double-click', event) - emitter.emit('camera-controls:focus', { nodeId: stair.id }) - }, - [emitFloorplanNodeClick], - ) - const handleStairPointerDown = useCallback( - (stairId: StairNode['id'], event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - const stair = selectedStairEntry?.stair - if (!stair || stair.id !== stairId) { - return - } - - event.preventDefault() - event.stopPropagation() - - const suppressClick = (clickEvent: MouseEvent) => { - clickEvent.stopImmediatePropagation() - clickEvent.preventDefault() - window.removeEventListener('click', suppressClick, true) - } - window.addEventListener('click', suppressClick, true) - requestAnimationFrame(() => { - window.removeEventListener('click', suppressClick, true) - }) - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(stair) - setSelection({ selectedIds: [] }) - }, - [selectedStairEntry, setMovingNode, setSelection], - ) - const handleSelectedOpeningMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const opening = selectedOpeningEntry?.opening - if (!opening) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(opening) - setSelection({ selectedIds: [] }) - }, - [selectedOpeningEntry, setMovingNode, setSelection], - ) - const duplicateSelectedOpening = useCallback(() => { - const opening = selectedOpeningEntry?.opening - if (!opening?.parentId) { - return - } - - sfxEmitter.emit('sfx:item-pick') - useScene.temporal.getState().pause() - - const cloned = structuredClone(opening) as Record - delete cloned.id - cloned.metadata = { - ...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}), - isNew: true, - } - - const duplicate = opening.type === 'door' ? DoorNode.parse(cloned) : WindowNode.parse(cloned) - - useScene.getState().createNode(duplicate, opening.parentId as AnyNodeId) - setMovingNode(duplicate) - setSelection({ selectedIds: [] }) - }, [selectedOpeningEntry, setMovingNode, setSelection]) - const handleSelectedOpeningDuplicate = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - duplicateSelectedOpening() - }, - [duplicateSelectedOpening], - ) - const handleSelectedOpeningDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const opening = selectedOpeningEntry?.opening - if (!opening) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(opening.id as AnyNodeId) - if (opening.parentId) { - useScene.getState().dirtyNodes.add(opening.parentId as AnyNodeId) - } - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedOpeningEntry, setSelection], - ) - const handleSelectedStairMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const stair = selectedStairEntry?.stair - if (!stair) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(stair) - setSelection({ selectedIds: [] }) - }, - [selectedStairEntry, setMovingNode, setSelection], - ) - const duplicateSelectedStair = useCallback(() => { - const stair = selectedStairEntry?.stair - if (!stair) { - return - } - - sfxEmitter.emit('sfx:item-pick') - useScene.temporal.getState().pause() - - try { - duplicateStairSubtree(stair.id as AnyNodeId, { mode: 'move' }) - } catch (error) { - console.error('Failed to duplicate stair', error) - } - }, [selectedStairEntry, setSelection]) - const handleSelectedStairDuplicate = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - duplicateSelectedStair() - }, - [duplicateSelectedStair], - ) - const handleSelectedStairDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const stair = selectedStairEntry?.stair - if (!stair) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(stair.id as AnyNodeId) - if (stair.parentId) { - useScene.getState().dirtyNodes.add(stair.parentId as AnyNodeId) - } - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedStairEntry, setSelection], - ) - const handleSelectedRoofMove = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const roof = selectedRoofEntry?.roof - if (!roof) { - return - } - - sfxEmitter.emit('sfx:item-pick') - setMovingNode(roof) - setSelection({ selectedIds: [] }) - }, - [selectedRoofEntry, setMovingNode, setSelection], - ) - const duplicateSelectedRoof = useCallback(() => { - const roof = selectedRoofEntry?.roof - if (!roof) { - return - } - - sfxEmitter.emit('sfx:item-pick') - - try { - duplicateRoofSubtree(roof.id as AnyNodeId, { mode: 'move' }) - } catch (error) { - console.error('Failed to duplicate roof', error) - } - }, [selectedRoofEntry]) - const handleSelectedRoofDuplicate = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - duplicateSelectedRoof() - }, - [duplicateSelectedRoof], - ) - const handleSelectedRoofDelete = useCallback( - (event: ReactMouseEvent) => { - event.stopPropagation() - - const roof = selectedRoofEntry?.roof - if (!roof) { - return - } - - sfxEmitter.emit('sfx:item-delete') - deleteNode(roof.id as AnyNodeId) - setSelection({ selectedIds: [] }) - }, - [deleteNode, selectedRoofEntry, setSelection], - ) - - const handleWallEndpointPointerDown = useCallback( - (wall: WallNode, endpoint: WallEndpoint, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredEndpointId(null) - - const movingPoint = endpoint === 'start' ? wall.start : wall.end - - if (isWallBuildActive) { - handleWallPlacementPoint(movingPoint) - return - } - - if (mode !== 'select') { - return - } - - clearWallPlacementDraft() - handleWallSelect(wall) - - const fixedPoint = endpoint === 'start' ? wall.end : wall.start - const originalStart = [...wall.start] as WallPlanPoint - const originalEnd = [...wall.end] as WallPlanPoint - const linkedWalls = getLinkedWallSnapshots(walls, wall.id, originalStart, originalEnd) - - wallEndpointDragRef.current = { - pointerId: event.pointerId, - wallId: wall.id, - endpoint, - fixedPoint, - currentPoint: movingPoint, - originalStart, - originalEnd, - linkedWalls, - } - - setWallEndpointDraft( - buildWallEndpointDraft(wall.id, endpoint, fixedPoint, movingPoint, linkedWalls), - ) - setCursorPoint(movingPoint) - }, - [ - clearWallPlacementDraft, - handleWallPlacementPoint, - handleWallSelect, - isWallBuildActive, - mode, - walls, - ], - ) - const handleWallCurvePointerDown = useCallback( - (wall: WallNode, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredWallCurveHandleId(null) - - if (isWallBuildActive || mode !== 'select') { - return - } - - clearWallPlacementDraft() - handleWallSelect(wall) - clearWallEndpointDrag() - - const currentCurveOffset = normalizeWallCurveOffset(wall, wall.curveOffset ?? 0) - wallCurveDragRef.current = { - pointerId: event.pointerId, - wallId: wall.id, - currentCurveOffset, - } - setWallCurveDraft({ - wallId: wall.id, - curveOffset: currentCurveOffset, - }) - const center = getWallMidpointHandlePoint(wall) - setCursorPoint([center.x, center.y]) - }, - [clearWallEndpointDrag, clearWallPlacementDraft, handleWallSelect, isWallBuildActive, mode], - ) - const handleSlabVertexPointerDown = useCallback( - (slabId: SlabNode['id'], vertexIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredSlabHandleId(null) - - const slabEntry = displaySlabPolygons.find(({ slab }) => slab.id === slabId) - const vertexPoint = slabEntry?.polygon[vertexIndex] - const handlePolygon = slabEntry ? getSlabHandlePolygon(slabEntry) : [] - const handlePoint = - vertexPoint && handlePolygon.length > 0 - ? handlePolygon[getClosestPolygonVertexIndex(vertexPoint, handlePolygon)] - : null - if (!(slabEntry && vertexPoint && handlePoint)) { - return - } - - const visualOffsets = getSlabVisualOffsets(slabEntry) - - setSlabBoundaryDraft({ - slabId, - polygon: slabEntry.polygon.map(toWallPlanPoint), - visualOffsets, - }) - setSlabVertexDragState({ - pointerId: event.pointerId, - slabId, - vertexIndex, - visualOffset: { - x: handlePoint.x - vertexPoint.x, - y: handlePoint.y - vertexPoint.y, - }, - }) - setCursorPoint(toWallPlanPoint(handlePoint)) - }, - [displaySlabPolygons], - ) - const handleSlabVertexDoubleClick = useCallback( - (slabId: SlabNode['id'], vertexIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - - const slab = slabById.get(slabId) - if (!(slab && slab.polygon.length > 3)) { - return - } - - slabBoundaryDraftRef.current = null - clearSlabBoundaryInteraction() - - updateNode(slabId, { - polygon: slab.polygon.filter((_, index) => index !== vertexIndex), - }) - }, - [clearSlabBoundaryInteraction, slabById, updateNode], - ) - const handleSlabMidpointPointerDown = useCallback( - ( - slabId: SlabNode['id'], - handleEdgeIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredSlabHandleId(null) - - const slabEntry = displaySlabPolygons.find(({ slab }) => slab.id === slabId) - if (!slabEntry) { - return - } - - const basePolygon = slabEntry.polygon.map(toWallPlanPoint) - const handlePolygon = getSlabHandlePolygon(slabEntry) - const handleStartPoint = handlePolygon[handleEdgeIndex] - const handleEndPoint = handlePolygon[(handleEdgeIndex + 1) % handlePolygon.length] - const insertedHandlePoint: WallPlanPoint = - handleStartPoint && handleEndPoint - ? [ - (handleStartPoint.x + handleEndPoint.x) / 2, - (handleStartPoint.y + handleEndPoint.y) / 2, - ] - : (basePolygon[handleEdgeIndex] ?? basePolygon[0] ?? ([0, 0] as WallPlanPoint)) - const edgeIndex = getClosestPolygonEdgeIndex( - toPoint2D(insertedHandlePoint), - slabEntry.polygon, - ) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - const insertedPoint: WallPlanPoint = [ - (startPoint[0] + endPoint[0]) / 2, - (startPoint[1] + endPoint[1]) / 2, - ] - const insertIndex = edgeIndex + 1 - const nextPolygon = [ - ...basePolygon.slice(0, insertIndex), - insertedPoint, - ...basePolygon.slice(insertIndex), - ] - const visualOffsets = getSlabVisualOffsets(slabEntry) - const insertedVisualOffset = { - x: insertedHandlePoint[0] - insertedPoint[0], - y: insertedHandlePoint[1] - insertedPoint[1], - } - const nextVisualOffsets = [ - ...visualOffsets.slice(0, insertIndex), - insertedVisualOffset, - ...visualOffsets.slice(insertIndex), - ] - - setSlabBoundaryDraft({ - slabId, - polygon: nextPolygon, - visualOffsets: nextVisualOffsets, - }) - setSlabVertexDragState({ - pointerId: event.pointerId, - slabId, - vertexIndex: insertIndex, - visualOffset: insertedVisualOffset, - }) - setCursorPoint(insertedHandlePoint) - }, - [displaySlabPolygons], - ) - const handleSlabEdgePointerDown = useCallback( - (slabId: SlabNode['id'], handleEdgeIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredSlabHandleId(null) - - const slabEntry = displaySlabPolygons.find(({ slab }) => slab.id === slabId) - if (!slabEntry) { - return - } - - const basePolygon = slabEntry.polygon.map(toWallPlanPoint) - const handlePolygon = getSlabHandlePolygon(slabEntry) - const handleStartPoint = handlePolygon[handleEdgeIndex] - const handleEndPoint = handlePolygon[(handleEdgeIndex + 1) % handlePolygon.length] - if (!(handleStartPoint && handleEndPoint)) { - return - } - - const handleMidpoint = { - x: (handleStartPoint.x + handleEndPoint.x) / 2, - y: (handleStartPoint.y + handleEndPoint.y) / 2, - } - const edgeIndex = getClosestPolygonEdgeIndex(handleMidpoint, slabEntry.polygon) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - - const edgeNormal = getFloorplanEdgeNormal(startPoint, endPoint) - if (!edgeNormal) { - return - } - - const initialPlanPoint = - getPlanPointFromClientPoint(event.clientX, event.clientY) ?? - ([(startPoint[0] + endPoint[0]) / 2, (startPoint[1] + endPoint[1]) / 2] as WallPlanPoint) - - setSlabBoundaryDraft({ - slabId, - polygon: basePolygon, - visualOffsets: getSlabVisualOffsets(slabEntry), - }) - setSlabVertexDragState({ - pointerId: event.pointerId, - slabId, - mode: 'edge', - vertexIndex: edgeIndex, - visualOffset: { x: 0, y: 0 }, - edgeIndex, - edgeNormal, - initialPlanPoint, - initialPolygon: basePolygon, - }) - setCursorPoint(initialPlanPoint) - }, - [displaySlabPolygons, getPlanPointFromClientPoint], - ) - const handleCeilingVertexPointerDown = useCallback( - ( - ceilingId: CeilingNode['id'], - vertexIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredCeilingHandleId(null) - - const ceilingEntry = displayCeilingPolygons.find(({ ceiling }) => ceiling.id === ceilingId) - const vertexPoint = ceilingEntry?.polygon[vertexIndex] - if (!(ceilingEntry && vertexPoint)) { - return - } - - setCeilingBoundaryDraft({ - ceilingId, - polygon: ceilingEntry.polygon.map(toWallPlanPoint), - }) - setCeilingVertexDragState({ - pointerId: event.pointerId, - ceilingId, - vertexIndex, - }) - setCursorPoint(toWallPlanPoint(vertexPoint)) - }, - [displayCeilingPolygons], - ) - const handleCeilingVertexDoubleClick = useCallback( - ( - ceilingId: CeilingNode['id'], - vertexIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - - const ceiling = ceilingById.get(ceilingId) - if (!(ceiling && ceiling.polygon.length > 3)) { - return - } - - ceilingBoundaryDraftRef.current = null - clearCeilingBoundaryInteraction() - - updateNode(ceilingId, { - polygon: ceiling.polygon.filter((_, index) => index !== vertexIndex), - }) - }, - [ceilingById, clearCeilingBoundaryInteraction, updateNode], - ) - const handleCeilingMidpointPointerDown = useCallback( - ( - ceilingId: CeilingNode['id'], - edgeIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredCeilingHandleId(null) - - const ceilingEntry = displayCeilingPolygons.find(({ ceiling }) => ceiling.id === ceilingId) - if (!ceilingEntry) { - return - } - - const basePolygon = ceilingEntry.polygon.map(toWallPlanPoint) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - - const insertedPoint: WallPlanPoint = [ - (startPoint[0] + endPoint[0]) / 2, - (startPoint[1] + endPoint[1]) / 2, - ] - const insertIndex = edgeIndex + 1 - const nextPolygon = [ - ...basePolygon.slice(0, insertIndex), - insertedPoint, - ...basePolygon.slice(insertIndex), - ] - - setCeilingBoundaryDraft({ - ceilingId, - polygon: nextPolygon, - }) - setCeilingVertexDragState({ - pointerId: event.pointerId, - ceilingId, - vertexIndex: insertIndex, - }) - setCursorPoint(insertedPoint) - }, - [displayCeilingPolygons], - ) - const handleCeilingEdgePointerDown = useCallback( - (ceilingId: CeilingNode['id'], edgeIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredCeilingHandleId(null) - - const ceilingEntry = displayCeilingPolygons.find(({ ceiling }) => ceiling.id === ceilingId) - if (!ceilingEntry) { - return - } - - const basePolygon = ceilingEntry.polygon.map(toWallPlanPoint) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - - const edgeNormal = getFloorplanEdgeNormal(startPoint, endPoint) - if (!edgeNormal) { - return - } - - const initialPlanPoint = - getPlanPointFromClientPoint(event.clientX, event.clientY) ?? - ([(startPoint[0] + endPoint[0]) / 2, (startPoint[1] + endPoint[1]) / 2] as WallPlanPoint) - - setCeilingBoundaryDraft({ - ceilingId, - polygon: basePolygon, - }) - setCeilingVertexDragState({ - pointerId: event.pointerId, - ceilingId, - mode: 'edge', - vertexIndex: edgeIndex, - edgeIndex, - edgeNormal, - initialPlanPoint, - initialPolygon: basePolygon, - }) - setCursorPoint(initialPlanPoint) - }, - [displayCeilingPolygons, getPlanPointFromClientPoint], - ) - const handleSlabHoleVertexPointerDown = useCallback( - (slabId: SlabNode['id'], vertexIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredSlabHandleId(null) - - const slabEntry = displaySlabPolygons.find(({ slab }) => slab.id === slabId) - const holeIndex = editingHole?.nodeId === slabId ? editingHole.holeIndex : null - const hole = holeIndex !== null ? slabEntry?.holes[holeIndex] : null - const vertexPoint = hole?.[vertexIndex] - if (!(slabEntry && holeIndex !== null && hole && vertexPoint)) { - return - } - - setSlabHoleBoundaryDraft({ - slabId, - holeIndex, - polygon: hole.map(toWallPlanPoint), - }) - setSlabHoleVertexDragState({ - pointerId: event.pointerId, - slabId, - holeIndex, - vertexIndex, - }) - setCursorPoint(toWallPlanPoint(vertexPoint)) - }, - [displaySlabPolygons, editingHole], - ) - const handleSlabHoleVertexDoubleClick = useCallback( - (slabId: SlabNode['id'], vertexIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - - const slab = slabById.get(slabId) - const holeIndex = editingHole?.nodeId === slabId ? editingHole.holeIndex : null - const hole = holeIndex !== null ? slab?.holes?.[holeIndex] : null - if (!(slab && holeIndex !== null && hole && hole.length > 3)) { - return - } - - slabHoleBoundaryDraftRef.current = null - clearSlabHoleBoundaryInteraction() - - const nextHoles = [...(slab.holes ?? [])] - nextHoles[holeIndex] = hole.filter((_, index) => index !== vertexIndex) - updateNode(slabId, { - holes: nextHoles, - }) - }, - [clearSlabHoleBoundaryInteraction, editingHole, slabById, updateNode], - ) - const handleSlabHoleMidpointPointerDown = useCallback( - (slabId: SlabNode['id'], edgeIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredSlabHandleId(null) - - const slabEntry = displaySlabPolygons.find(({ slab }) => slab.id === slabId) - const holeIndex = editingHole?.nodeId === slabId ? editingHole.holeIndex : null - const hole = holeIndex !== null ? slabEntry?.holes[holeIndex] : null - if (!(slabEntry && holeIndex !== null && hole)) { - return - } - - const basePolygon = hole.map(toWallPlanPoint) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - - const insertedPoint: WallPlanPoint = [ - (startPoint[0] + endPoint[0]) / 2, - (startPoint[1] + endPoint[1]) / 2, - ] - const insertIndex = edgeIndex + 1 - const nextPolygon = [ - ...basePolygon.slice(0, insertIndex), - insertedPoint, - ...basePolygon.slice(insertIndex), - ] - - setSlabHoleBoundaryDraft({ - slabId, - holeIndex, - polygon: nextPolygon, - }) - setSlabHoleVertexDragState({ - pointerId: event.pointerId, - slabId, - holeIndex, - vertexIndex: insertIndex, - }) - setCursorPoint(insertedPoint) - }, - [displaySlabPolygons, editingHole], - ) - const handleSlabHoleEdgePointerDown = useCallback( - (slabId: SlabNode['id'], edgeIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredSlabHandleId(null) - - const slabEntry = displaySlabPolygons.find(({ slab }) => slab.id === slabId) - const holeIndex = editingHole?.nodeId === slabId ? editingHole.holeIndex : null - const hole = holeIndex !== null ? slabEntry?.holes[holeIndex] : null - if (!(slabEntry && holeIndex !== null && hole)) { - return - } - - const basePolygon = hole.map(toWallPlanPoint) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - - const edgeNormal = getFloorplanEdgeNormal(startPoint, endPoint) - if (!edgeNormal) { - return - } - - const initialPlanPoint = - getPlanPointFromClientPoint(event.clientX, event.clientY) ?? - ([(startPoint[0] + endPoint[0]) / 2, (startPoint[1] + endPoint[1]) / 2] as WallPlanPoint) - - setSlabHoleBoundaryDraft({ - slabId, - holeIndex, - polygon: basePolygon, - }) - setSlabHoleVertexDragState({ - pointerId: event.pointerId, - slabId, - holeIndex, - mode: 'edge', - vertexIndex: edgeIndex, - edgeIndex, - edgeNormal, - initialPlanPoint, - initialPolygon: basePolygon, - }) - setCursorPoint(initialPlanPoint) - }, - [displaySlabPolygons, editingHole, getPlanPointFromClientPoint], - ) - const handleCeilingHoleVertexPointerDown = useCallback( - ( - ceilingId: CeilingNode['id'], - vertexIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredCeilingHandleId(null) - - const ceilingEntry = displayCeilingPolygons.find(({ ceiling }) => ceiling.id === ceilingId) - const holeIndex = editingHole?.nodeId === ceilingId ? editingHole.holeIndex : null - const hole = holeIndex !== null ? ceilingEntry?.holes[holeIndex] : null - const vertexPoint = hole?.[vertexIndex] - if (!(ceilingEntry && holeIndex !== null && hole && vertexPoint)) { - return - } - - setCeilingHoleBoundaryDraft({ - ceilingId, - holeIndex, - polygon: hole.map(toWallPlanPoint), - }) - setCeilingHoleVertexDragState({ - pointerId: event.pointerId, - ceilingId, - holeIndex, - vertexIndex, - }) - setCursorPoint(toWallPlanPoint(vertexPoint)) - }, - [displayCeilingPolygons, editingHole], - ) - const handleCeilingHoleVertexDoubleClick = useCallback( - ( - ceilingId: CeilingNode['id'], - vertexIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - - const ceiling = ceilingById.get(ceilingId) - const holeIndex = editingHole?.nodeId === ceilingId ? editingHole.holeIndex : null - const hole = holeIndex !== null ? ceiling?.holes?.[holeIndex] : null - if (!(ceiling && holeIndex !== null && hole && hole.length > 3)) { - return - } - - ceilingHoleBoundaryDraftRef.current = null - clearCeilingHoleBoundaryInteraction() - - const nextHoles = [...(ceiling.holes ?? [])] - nextHoles[holeIndex] = hole.filter((_, index) => index !== vertexIndex) - updateNode(ceilingId, { - holes: nextHoles, - }) - }, - [ceilingById, clearCeilingHoleBoundaryInteraction, editingHole, updateNode], - ) - const handleCeilingHoleMidpointPointerDown = useCallback( - ( - ceilingId: CeilingNode['id'], - edgeIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredCeilingHandleId(null) - - const ceilingEntry = displayCeilingPolygons.find(({ ceiling }) => ceiling.id === ceilingId) - const holeIndex = editingHole?.nodeId === ceilingId ? editingHole.holeIndex : null - const hole = holeIndex !== null ? ceilingEntry?.holes[holeIndex] : null - if (!(ceilingEntry && holeIndex !== null && hole)) { - return - } - - const basePolygon = hole.map(toWallPlanPoint) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - - const insertedPoint: WallPlanPoint = [ - (startPoint[0] + endPoint[0]) / 2, - (startPoint[1] + endPoint[1]) / 2, - ] - const insertIndex = edgeIndex + 1 - const nextPolygon = [ - ...basePolygon.slice(0, insertIndex), - insertedPoint, - ...basePolygon.slice(insertIndex), - ] - - setCeilingHoleBoundaryDraft({ - ceilingId, - holeIndex, - polygon: nextPolygon, - }) - setCeilingHoleVertexDragState({ - pointerId: event.pointerId, - ceilingId, - holeIndex, - vertexIndex: insertIndex, - }) - setCursorPoint(insertedPoint) - }, - [displayCeilingPolygons, editingHole], - ) - const handleCeilingHoleEdgePointerDown = useCallback( - (ceilingId: CeilingNode['id'], edgeIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredCeilingHandleId(null) - - const ceilingEntry = displayCeilingPolygons.find(({ ceiling }) => ceiling.id === ceilingId) - const holeIndex = editingHole?.nodeId === ceilingId ? editingHole.holeIndex : null - const hole = holeIndex !== null ? ceilingEntry?.holes[holeIndex] : null - if (!(ceilingEntry && holeIndex !== null && hole)) { - return - } - - const basePolygon = hole.map(toWallPlanPoint) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - - const edgeNormal = getFloorplanEdgeNormal(startPoint, endPoint) - if (!edgeNormal) { - return - } - - const initialPlanPoint = - getPlanPointFromClientPoint(event.clientX, event.clientY) ?? - ([(startPoint[0] + endPoint[0]) / 2, (startPoint[1] + endPoint[1]) / 2] as WallPlanPoint) - - setCeilingHoleBoundaryDraft({ - ceilingId, - holeIndex, - polygon: basePolygon, - }) - setCeilingHoleVertexDragState({ - pointerId: event.pointerId, - ceilingId, - holeIndex, - mode: 'edge', - vertexIndex: edgeIndex, - edgeIndex, - edgeNormal, - initialPlanPoint, - initialPolygon: basePolygon, - }) - setCursorPoint(initialPlanPoint) - }, - [displayCeilingPolygons, editingHole, getPlanPointFromClientPoint], - ) const handleSiteVertexPointerDown = useCallback( (siteId: SiteNode['id'], vertexIndex: number, event: ReactPointerEvent) => { if (event.button !== 0) { @@ -13047,168 +9584,17 @@ export function FloorplanPanel() { }, [displaySitePolygon], ) - const handleZoneVertexPointerDown = useCallback( - ( - zoneId: ZoneNodeType['id'], - vertexIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredZoneHandleId(null) - - const zoneEntry = displayZonePolygons.find(({ zone }) => zone.id === zoneId) - const vertexPoint = zoneEntry?.polygon[vertexIndex] - if (!(zoneEntry && vertexPoint)) { - return - } - - setZoneBoundaryDraft({ - zoneId, - polygon: zoneEntry.polygon.map(toWallPlanPoint), - }) - setZoneVertexDragState({ - pointerId: event.pointerId, - zoneId, - vertexIndex, - }) - setCursorPoint(toWallPlanPoint(vertexPoint)) - }, - [displayZonePolygons], - ) - const handleZoneVertexDoubleClick = useCallback( - ( - zoneId: ZoneNodeType['id'], - vertexIndex: number, - event: ReactPointerEvent, - ) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - - const zone = zoneById.get(zoneId) - if (!(zone && zone.polygon.length > 3)) { - return - } - - zoneBoundaryDraftRef.current = null - clearZoneBoundaryInteraction() - - updateNode(zoneId, { - polygon: zone.polygon.filter((_, index) => index !== vertexIndex), - }) - }, - [clearZoneBoundaryInteraction, updateNode, zoneById], - ) - const handleZoneMidpointPointerDown = useCallback( - (zoneId: ZoneNodeType['id'], edgeIndex: number, event: ReactPointerEvent) => { - if (event.button !== 0) { - return - } - - event.preventDefault() - event.stopPropagation() - setHoveredZoneHandleId(null) - - const zoneEntry = displayZonePolygons.find(({ zone }) => zone.id === zoneId) - if (!zoneEntry) { - return - } - - const basePolygon = zoneEntry.polygon.map(toWallPlanPoint) - const startPoint = basePolygon[edgeIndex] - const endPoint = basePolygon[(edgeIndex + 1) % basePolygon.length] - if (!(startPoint && endPoint)) { - return - } - - const insertedPoint: WallPlanPoint = [ - (startPoint[0] + endPoint[0]) / 2, - (startPoint[1] + endPoint[1]) / 2, - ] - const insertIndex = edgeIndex + 1 - const nextPolygon = [ - ...basePolygon.slice(0, insertIndex), - insertedPoint, - ...basePolygon.slice(insertIndex), - ] - - setZoneBoundaryDraft({ - zoneId, - polygon: nextPolygon, - }) - setZoneVertexDragState({ - pointerId: event.pointerId, - zoneId, - vertexIndex: insertIndex, - }) - setCursorPoint(insertedPoint) - }, - [displayZonePolygons], - ) const handlePointerLeave = useCallback(() => { - if ( - !( - panStateRef.current || - wallEndpointDragRef.current || - ceilingVertexDragState || - ceilingHoleMoveDraft || - ceilingHoleVertexDragState || - siteVertexDragState || - slabHoleMoveDraft || - slabHoleVertexDragState || - slabVertexDragState || - zoneVertexDragState - ) - ) { + if (!(panStateRef.current || wallEndpointDragRef.current || siteVertexDragState)) { setCursorPoint(null) } - handleOpeningHoverChange(null) - handleItemHoverChange(null) - handleWallHoverChange(null) - handleSlabHoverChange(null) - handleCeilingHoverChange(null) - handleSpawnHoverChange(null) - handleStairHoverChange(null) - handleElevatorHoverChange(null) - handleZoneHoverChange(null) - setHoveredEndpointId(null) setHoveredSiteHandleId(null) - setHoveredSlabHandleId(null) - setHoveredCeilingHandleId(null) - setHoveredZoneHandleId(null) if (hoveredWallIdRef.current) { emitFloorplanWallLeave(hoveredWallIdRef.current) hoveredWallIdRef.current = null } - }, [ - emitFloorplanWallLeave, - handleCeilingHoverChange, - handleElevatorHoverChange, - handleItemHoverChange, - handleOpeningHoverChange, - handleSlabHoverChange, - handleSpawnHoverChange, - handleStairHoverChange, - handleWallHoverChange, - handleZoneHoverChange, - ceilingVertexDragState, - ceilingHoleMoveDraft, - ceilingHoleVertexDragState, - siteVertexDragState, - slabHoleMoveDraft, - slabHoleVertexDragState, - slabVertexDragState, - zoneVertexDragState, - ]) + }, [emitFloorplanWallLeave, siteVertexDragState]) // Lightweight flag that mirrors the conditions under which // FloorplanCursorIndicatorOverlay renders — used to gate cursor-position @@ -13300,15 +9686,6 @@ export function FloorplanPanel() { }) } setCursorPoint(snappedPoint) - handleItemHoverChange(null) - handleOpeningHoverChange(null) - handleWallHoverChange(null) - handleSlabHoverChange(null) - handleSpawnHoverChange(null) - handleStairHoverChange(null) - handleElevatorHoverChange(null) - handleZoneHoverChange(null) - setHoveredEndpointId(null) floorplanMarqueeSnapPointRef.current = snappedPoint syncPreviewSelectedIds([]) setFloorplanMarqueeState({ @@ -13321,18 +9698,7 @@ export function FloorplanPanel() { event.currentTarget.setPointerCapture(event.pointerId) }, - [ - getPlanPointFromClientPoint, - handleElevatorHoverChange, - handleItemHoverChange, - handleOpeningHoverChange, - handleSlabHoverChange, - handleSpawnHoverChange, - handleStairHoverChange, - handleWallHoverChange, - handleZoneHoverChange, - syncPreviewSelectedIds, - ], + [getPlanPointFromClientPoint, syncPreviewSelectedIds], ) const handleMarqueePointerMove = useCallback( @@ -13486,20 +9852,7 @@ export function FloorplanPanel() { } setFloorplanCursorPosition(null) - handleOpeningHoverChange(null) - handleWallHoverChange(null) - handleSlabHoverChange(null) - handleZoneHoverChange(null) - setHoveredEndpointId(null) - }, [ - handleOpeningHoverChange, - handleSlabHoverChange, - handleWallHoverChange, - handleZoneHoverChange, - isMarqueeSelectionToolActive, - mode, - syncPreviewSelectedIds, - ]) + }, [isMarqueeSelectionToolActive, mode, syncPreviewSelectedIds]) useEffect(() => { if (mode !== 'delete') { @@ -13639,45 +9992,6 @@ export function FloorplanPanel() { setStructureLayer, site, ]) - const hasDuplicatableFloorplanSelection = Boolean( - selectedItemEntry || - selectedOpeningEntry || - selectedStairEntry || - selectedRoofEntry || - selectedWallEntry, - ) - const handleDuplicateFloorplanSelection = useCallback(() => { - if (selectedWallEntry) { - duplicateSelectedWall() - return - } - if (selectedOpeningEntry) { - duplicateSelectedOpening() - return - } - if (selectedItemEntry) { - duplicateSelectedItem() - return - } - if (selectedStairEntry) { - duplicateSelectedStair() - return - } - if (selectedRoofEntry) { - duplicateSelectedRoof() - } - }, [ - duplicateSelectedWall, - duplicateSelectedItem, - duplicateSelectedOpening, - duplicateSelectedRoof, - duplicateSelectedStair, - selectedItemEntry, - selectedOpeningEntry, - selectedRoofEntry, - selectedStairEntry, - selectedWallEntry, - ]) const activeDraftAnchorPoint = referenceScaleDraft?.start ?? draftStart ?? @@ -13723,10 +10037,6 @@ export function FloorplanPanel() { ref={containerRef} > -
)} - {/* Floating Move / Duplicate / Delete buttons for registered - kinds (shelf today; others as Phase 5 ports add def.floorplan). - Renders nothing for legacy kinds — they keep the layer above. */} + kinds. All kinds are registry-driven now, so this is the + only action menu the floor plan mounts. */} {referenceScaleDraft && ( @@ -14022,36 +10265,28 @@ export function FloorplanPanel() { - - {/* Stair is fully registry-driven for committed nodes (`def.floorplan` on the stair kind). This layer only carries the in-flight stair preview, which lives outside the scene graph and so isn't visible to `FloorplanRegistryLayer`. When the preview entry is - absent the array is empty and the layer renders nothing. */} + absent the array is empty and the layer renders nothing. + Hover / select / double-click props are noops — the + preview isn't interactive, and committed stairs route + through `FloorplanRegistryLayer`. */} - - - - - {/* Zone labels: always visible so users can click to select zones from any mode */} - - - - - - - - - - handleSlabEdgePointerDown(nodeId as SlabNode['id'], edgeIndex, event) - } - onHandleHoverChange={setHoveredSlabHandleId} - onMidpointPointerDown={(nodeId, edgeIndex, event) => - handleSlabMidpointPointerDown(nodeId as SlabNode['id'], edgeIndex, event) - } - onVertexDoubleClick={(nodeId, vertexIndex, event) => - handleSlabVertexDoubleClick(nodeId as SlabNode['id'], vertexIndex, event) - } - onVertexPointerDown={(nodeId, vertexIndex, event) => - handleSlabVertexPointerDown(nodeId as SlabNode['id'], vertexIndex, event) - } - palette={palette} - unitsPerPixel={floorplanUnitsPerPixel} - vertexHandles={slabVertexHandles} - /> - - - handleSlabHoleEdgePointerDown(nodeId as SlabNode['id'], edgeIndex, event) - } - onHandleHoverChange={setHoveredSlabHandleId} - onMidpointPointerDown={(nodeId, edgeIndex, event) => - handleSlabHoleMidpointPointerDown(nodeId as SlabNode['id'], edgeIndex, event) - } - onVertexDoubleClick={(nodeId, vertexIndex, event) => - handleSlabHoleVertexDoubleClick(nodeId as SlabNode['id'], vertexIndex, event) - } - onVertexPointerDown={(nodeId, vertexIndex, event) => - handleSlabHoleVertexPointerDown(nodeId as SlabNode['id'], vertexIndex, event) - } - palette={palette} - unitsPerPixel={floorplanUnitsPerPixel} - vertexHandles={slabHoleVertexHandles} - /> - - - handleCeilingEdgePointerDown(nodeId as CeilingNode['id'], edgeIndex, event) - } - onHandleHoverChange={setHoveredCeilingHandleId} - onMidpointPointerDown={(nodeId, edgeIndex, event) => - handleCeilingMidpointPointerDown(nodeId as CeilingNode['id'], edgeIndex, event) - } - onVertexDoubleClick={(nodeId, vertexIndex, event) => - handleCeilingVertexDoubleClick(nodeId as CeilingNode['id'], vertexIndex, event) - } - onVertexPointerDown={(nodeId, vertexIndex, event) => - handleCeilingVertexPointerDown(nodeId as CeilingNode['id'], vertexIndex, event) - } - palette={palette} - unitsPerPixel={floorplanUnitsPerPixel} - vertexHandles={ceilingVertexHandles} - /> - - - handleCeilingHoleEdgePointerDown(nodeId as CeilingNode['id'], edgeIndex, event) - } - onHandleHoverChange={setHoveredCeilingHandleId} - onMidpointPointerDown={(nodeId, edgeIndex, event) => - handleCeilingHoleMidpointPointerDown( - nodeId as CeilingNode['id'], - edgeIndex, - event, - ) - } - onVertexDoubleClick={(nodeId, vertexIndex, event) => - handleCeilingHoleVertexDoubleClick( - nodeId as CeilingNode['id'], - vertexIndex, - event, - ) - } - onVertexPointerDown={(nodeId, vertexIndex, event) => - handleCeilingHoleVertexPointerDown( - nodeId as CeilingNode['id'], - vertexIndex, - event, - ) - } - palette={palette} - unitsPerPixel={floorplanUnitsPerPixel} - vertexHandles={ceilingHoleVertexHandles} - /> - - - handleZoneMidpointPointerDown(nodeId as ZoneNodeType['id'], edgeIndex, event) - } - onVertexDoubleClick={(nodeId, vertexIndex, event) => - handleZoneVertexDoubleClick(nodeId as ZoneNodeType['id'], vertexIndex, event) - } - onVertexPointerDown={(nodeId, vertexIndex, event) => - handleZoneVertexPointerDown(nodeId as ZoneNodeType['id'], vertexIndex, event) - } - palette={palette} - unitsPerPixel={floorplanUnitsPerPixel} - vertexHandles={zoneVertexHandles} - /> + {/* Wall / fence endpoint, wall curve, slab / ceiling / + zone vertex+midpoint+edge handles are all driven by the + registry's `def.floorplanAffordances` and rendered as + part of `FloorplanRegistryLayer`. The legacy handle + layers that lived here received empty handle arrays + post-migration and rendered nothing. */} {selectedGuide && showGuides && (