@pascal-app/editor 0.9.2 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +265 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
|
@@ -32,17 +32,18 @@ import {
|
|
|
32
32
|
type Point2D,
|
|
33
33
|
type RoofNode,
|
|
34
34
|
type RoofSegmentNode,
|
|
35
|
+
resolveSlabPlacementElevation,
|
|
35
36
|
type SiteNode,
|
|
36
37
|
type SlabNode,
|
|
37
38
|
SlabNode as SlabNodeSchema,
|
|
38
|
-
type SpawnNode,
|
|
39
39
|
type StairNode,
|
|
40
40
|
StairNode as StairNodeSchema,
|
|
41
|
-
type StairSegmentNode,
|
|
42
41
|
StairSegmentNode as StairSegmentNodeSchema,
|
|
43
42
|
sampleWallCenterline,
|
|
44
43
|
sceneRegistry,
|
|
45
44
|
snapPointAlongAngleRay,
|
|
45
|
+
spatialGridManager,
|
|
46
|
+
terrainSupportLift,
|
|
46
47
|
useInteractive,
|
|
47
48
|
useLiveNodeOverrides,
|
|
48
49
|
useLiveTransforms,
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
type ComponentProps,
|
|
63
64
|
memo,
|
|
64
65
|
type MouseEvent as ReactMouseEvent,
|
|
66
|
+
type ReactNode,
|
|
65
67
|
type PointerEvent as ReactPointerEvent,
|
|
66
68
|
useCallback,
|
|
67
69
|
useEffect,
|
|
@@ -86,14 +88,16 @@ import {
|
|
|
86
88
|
type FloorplanNodeTransform as SharedFloorplanNodeTransform,
|
|
87
89
|
worldToFloorplanLocalPoint,
|
|
88
90
|
} from '../../lib/floorplan'
|
|
91
|
+
import { groundHeightAt } from '../../lib/ground-surface'
|
|
89
92
|
import { guideEmitter } from '../../lib/guide-events'
|
|
90
93
|
import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
|
|
91
94
|
import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements'
|
|
92
95
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
93
|
-
import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
|
|
96
|
+
import { SITE_BOUNDARY_DRAG_LABEL, siteBoundaryHandlesEnabled } from '../../lib/site-boundary'
|
|
94
97
|
import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
|
|
95
98
|
import { cn } from '../../lib/utils'
|
|
96
99
|
import { snapBuildingLocalToWorldGrid } from '../../lib/world-grid-snap'
|
|
100
|
+
import { subscribeNavigationSyncPose } from '../../store/navigation-sync-pose-store'
|
|
97
101
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
98
102
|
import type { GuideUiState, NavigationSyncPose } from '../../store/use-editor'
|
|
99
103
|
import useEditor, {
|
|
@@ -117,6 +121,7 @@ import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOver
|
|
|
117
121
|
import { FloorplanGroupActionMenu } from '../editor-2d/floorplan-group-action-menu'
|
|
118
122
|
import { FloorplanSiteKeyHandler } from '../editor-2d/floorplan-hotkey-handlers'
|
|
119
123
|
import { FloorplanMeasurementToolLayer } from '../editor-2d/floorplan-measurement-tool-layer'
|
|
124
|
+
import { FloorplanRegisteredToolLayer } from '../editor-2d/floorplan-registered-tool-layer'
|
|
120
125
|
import { FloorplanRegistryActionMenu } from '../editor-2d/floorplan-registry-action-menu'
|
|
121
126
|
import { FloorplanRegistryMoveOverlay } from '../editor-2d/floorplan-registry-move-overlay'
|
|
122
127
|
import {
|
|
@@ -178,6 +183,7 @@ import {
|
|
|
178
183
|
chainEndJoinsExistingWall,
|
|
179
184
|
createWallOnCurrentLevel,
|
|
180
185
|
isSegmentLongEnough,
|
|
186
|
+
resolveTerrainWallConstructionOptions,
|
|
181
187
|
snapWallDraftPoint,
|
|
182
188
|
snapWallDraftPointDetailed,
|
|
183
189
|
snapPointToGrid as snapWallPointToGrid,
|
|
@@ -189,6 +195,23 @@ import {
|
|
|
189
195
|
import { PALETTE_COLORS } from '../ui/primitives/color-dot'
|
|
190
196
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
|
|
191
197
|
import { resolveFloorplanBackgroundSelection } from './floorplan-background-selection'
|
|
198
|
+
import {
|
|
199
|
+
subscribeFloorplanCameraNavigation,
|
|
200
|
+
useFloorplanCameraSyncBridge,
|
|
201
|
+
} from './floorplan-camera-sync'
|
|
202
|
+
import {
|
|
203
|
+
canApplyFloorplanNavigationSync,
|
|
204
|
+
canZoomFloorplanDuringNavigation,
|
|
205
|
+
createFloorplanNavigationSyncScheduler,
|
|
206
|
+
type FloorplanNavigationSyncScheduler,
|
|
207
|
+
type FloorplanPresentationViewBox,
|
|
208
|
+
finalizeFloorplanNavigation,
|
|
209
|
+
flushFloorplanRotationPresentationRestore,
|
|
210
|
+
getFloorplanRotationOverscanViewBox,
|
|
211
|
+
queueFloorplanRotationPresentationRestore,
|
|
212
|
+
resolveFloorplanPresentationViewBox,
|
|
213
|
+
setFloorplanCompassRotation,
|
|
214
|
+
} from './floorplan-navigation-presentation'
|
|
192
215
|
import { useFloorplanBackgroundPlacement } from './use-floorplan-background-placement'
|
|
193
216
|
import { useFloorplanHitTesting } from './use-floorplan-hit-testing'
|
|
194
217
|
import { useFloorplanSceneData } from './use-floorplan-scene-data'
|
|
@@ -318,6 +341,36 @@ type FloorplanRotationState = {
|
|
|
318
341
|
startClientX: number
|
|
319
342
|
initialUserRotationDeg: number
|
|
320
343
|
viewportCenterLocal: SvgPoint
|
|
344
|
+
svg: SVGSVGElement
|
|
345
|
+
svgStyle: {
|
|
346
|
+
transform: string
|
|
347
|
+
transformOrigin: string
|
|
348
|
+
willChange: string
|
|
349
|
+
}
|
|
350
|
+
latestUserRotationDeg: number
|
|
351
|
+
latestViewport: FloorplanViewport
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
type FloorplanNavigationSyncPresentationState = {
|
|
355
|
+
initialUserRotationDeg: number
|
|
356
|
+
initialSceneRotationDeg: number
|
|
357
|
+
latestUserRotationDeg: number
|
|
358
|
+
latestSceneRotationDeg: number
|
|
359
|
+
latestViewport: FloorplanViewport
|
|
360
|
+
svg: SVGSVGElement
|
|
361
|
+
svgStyle: {
|
|
362
|
+
transform: string
|
|
363
|
+
transformOrigin: string
|
|
364
|
+
willChange: string
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function restoreFloorplanNavigationSyncPresentation(
|
|
369
|
+
presentationState: FloorplanNavigationSyncPresentationState,
|
|
370
|
+
) {
|
|
371
|
+
presentationState.svg.style.transform = presentationState.svgStyle.transform
|
|
372
|
+
presentationState.svg.style.transformOrigin = presentationState.svgStyle.transformOrigin
|
|
373
|
+
presentationState.svg.style.willChange = presentationState.svgStyle.willChange
|
|
321
374
|
}
|
|
322
375
|
|
|
323
376
|
type FloorplanScreenSelectionState = {
|
|
@@ -414,21 +467,6 @@ type PendingFenceDragState = {
|
|
|
414
467
|
startClientY: number
|
|
415
468
|
}
|
|
416
469
|
|
|
417
|
-
type ElevatorResizeHandle =
|
|
418
|
-
| 'width-negative'
|
|
419
|
-
| 'width-positive'
|
|
420
|
-
| 'depth-negative'
|
|
421
|
-
| 'depth-positive'
|
|
422
|
-
|
|
423
|
-
type ElevatorResizeDragState = {
|
|
424
|
-
center: Point2D
|
|
425
|
-
elevatorId: ElevatorNode['id']
|
|
426
|
-
handle: ElevatorResizeHandle
|
|
427
|
-
pointerId: number
|
|
428
|
-
rotation: number
|
|
429
|
-
shaftWallThickness: number
|
|
430
|
-
}
|
|
431
|
-
|
|
432
470
|
const GUIDE_CORNERS = ['nw', 'ne', 'se', 'sw'] as const
|
|
433
471
|
|
|
434
472
|
type GuideCorner = (typeof GUIDE_CORNERS)[number]
|
|
@@ -593,22 +631,11 @@ type SitePolygonEntry = {
|
|
|
593
631
|
points: string
|
|
594
632
|
}
|
|
595
633
|
|
|
596
|
-
type ZonePolygonEntry = {
|
|
597
|
-
zone: ZoneNodeType
|
|
598
|
-
polygon: Point2D[]
|
|
599
|
-
points: string
|
|
600
|
-
}
|
|
601
|
-
|
|
602
634
|
type FloorplanLineSegment = {
|
|
603
635
|
start: Point2D
|
|
604
636
|
end: Point2D
|
|
605
637
|
}
|
|
606
638
|
|
|
607
|
-
type FloorplanPolygonEntry = {
|
|
608
|
-
points: string
|
|
609
|
-
polygon: Point2D[]
|
|
610
|
-
}
|
|
611
|
-
|
|
612
639
|
type FloorplanItemEntry = {
|
|
613
640
|
dimensionPolygon: Point2D[]
|
|
614
641
|
item: ItemNode
|
|
@@ -624,52 +651,6 @@ type FloorplanItemEntry = {
|
|
|
624
651
|
depth: number
|
|
625
652
|
}
|
|
626
653
|
|
|
627
|
-
type FloorplanSpawnEntry = {
|
|
628
|
-
spawn: SpawnNode
|
|
629
|
-
position: Point2D
|
|
630
|
-
rotation: number
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
type FloorplanColumnEntry = {
|
|
634
|
-
column: ColumnNode
|
|
635
|
-
points: string
|
|
636
|
-
polygon: Point2D[]
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
type FloorplanElevatorServedLevel = {
|
|
640
|
-
id: LevelNode['id']
|
|
641
|
-
isCurrent: boolean
|
|
642
|
-
isDisabled: boolean
|
|
643
|
-
isQueued: boolean
|
|
644
|
-
isServiceOnly: boolean
|
|
645
|
-
isTarget: boolean
|
|
646
|
-
label: string
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
type FloorplanElevatorEntry = {
|
|
650
|
-
cabCenterLocalY: number
|
|
651
|
-
cabDepth: number
|
|
652
|
-
cabWidth: number
|
|
653
|
-
center: Point2D
|
|
654
|
-
doorStyle: ElevatorNode['doorStyle']
|
|
655
|
-
doorWidth: number
|
|
656
|
-
elevator: ElevatorNode
|
|
657
|
-
frontEdge: FloorplanLineSegment
|
|
658
|
-
frontNormal: Point2D
|
|
659
|
-
isCarOnLevel: boolean
|
|
660
|
-
isQueuedLevel: boolean
|
|
661
|
-
isTargetLevel: boolean
|
|
662
|
-
outerHalfDepth: number
|
|
663
|
-
outerHalfWidth: number
|
|
664
|
-
points: string
|
|
665
|
-
polygon: Point2D[]
|
|
666
|
-
rotation: number
|
|
667
|
-
servedLevels: FloorplanElevatorServedLevel[]
|
|
668
|
-
shaftDepth: number
|
|
669
|
-
shaftWallThickness: number
|
|
670
|
-
shaftWidth: number
|
|
671
|
-
}
|
|
672
|
-
|
|
673
654
|
type ReferenceFloorData = {
|
|
674
655
|
ceilingPolygons: CeilingPolygonEntry[]
|
|
675
656
|
columnEntries: ReferenceFloorColumnEntry[]
|
|
@@ -714,42 +695,6 @@ const REFERENCE_REGISTRY_KINDS = new Set<AnyNode['type']>([
|
|
|
714
695
|
'elevator',
|
|
715
696
|
])
|
|
716
697
|
|
|
717
|
-
type FloorplanStairSegmentEntry = {
|
|
718
|
-
centerLine: FloorplanLineSegment | null
|
|
719
|
-
innerPoints: string
|
|
720
|
-
innerPolygon: Point2D[]
|
|
721
|
-
segment: StairSegmentNode
|
|
722
|
-
points: string
|
|
723
|
-
polygon: Point2D[]
|
|
724
|
-
treadBars: FloorplanPolygonEntry[]
|
|
725
|
-
treadThickness: number
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
type FloorplanStairArrowEntry = {
|
|
729
|
-
head: Point2D[]
|
|
730
|
-
polyline: Point2D[]
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
type FloorplanStairEntry = {
|
|
734
|
-
arrow: FloorplanStairArrowEntry | null
|
|
735
|
-
hitPolygons: Point2D[][]
|
|
736
|
-
stair: StairNode
|
|
737
|
-
segments: FloorplanStairSegmentEntry[]
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
type FloorplanRoofSegmentEntry = {
|
|
741
|
-
segment: RoofSegmentNode
|
|
742
|
-
polygon: Point2D[]
|
|
743
|
-
points: string
|
|
744
|
-
ridgeLine: FloorplanLineSegment | null
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
type FloorplanRoofEntry = {
|
|
748
|
-
roof: RoofNode
|
|
749
|
-
center: Point2D
|
|
750
|
-
segments: FloorplanRoofSegmentEntry[]
|
|
751
|
-
}
|
|
752
|
-
|
|
753
698
|
type FloorplanPalette = {
|
|
754
699
|
surface: string
|
|
755
700
|
minorGrid: string
|
|
@@ -894,18 +839,6 @@ function resolveFloorplanViewWidth(
|
|
|
894
839
|
return clamp(requestedWidth, Math.min(minWidth, currentWidth), Math.max(maxWidth, currentWidth))
|
|
895
840
|
}
|
|
896
841
|
|
|
897
|
-
function roundPlanMeters(value: number) {
|
|
898
|
-
return Math.round(value * 100) / 100
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function getElevatorResizeAxis(handle: ElevatorResizeHandle) {
|
|
902
|
-
return handle.startsWith('width') ? 'width' : 'depth'
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
function getElevatorResizeSign(handle: ElevatorResizeHandle) {
|
|
906
|
-
return handle.endsWith('positive') ? 1 : -1
|
|
907
|
-
}
|
|
908
|
-
|
|
909
842
|
function getSelectionModifierKeys(event?: {
|
|
910
843
|
metaKey?: boolean
|
|
911
844
|
ctrlKey?: boolean
|
|
@@ -2718,9 +2651,10 @@ function formatMeasurement(
|
|
|
2718
2651
|
value: number,
|
|
2719
2652
|
unit: 'metric' | 'imperial',
|
|
2720
2653
|
metersPerUnit: number | null = null,
|
|
2654
|
+
metricNotation: 'meters' | 'millimeters' = 'meters',
|
|
2721
2655
|
) {
|
|
2722
2656
|
const measuredValue = metersPerUnit && metersPerUnit > 0 ? value * metersPerUnit : value
|
|
2723
|
-
return formatLinearMeasurement(measuredValue, unit)
|
|
2657
|
+
return formatLinearMeasurement(measuredValue, unit, metricNotation)
|
|
2724
2658
|
}
|
|
2725
2659
|
|
|
2726
2660
|
function formatNumber(value: number, fractionDigits = 2) {
|
|
@@ -3613,6 +3547,7 @@ function FloorplanReferenceScaleDraftLine({
|
|
|
3613
3547
|
unitsPerPixel: number
|
|
3614
3548
|
}) {
|
|
3615
3549
|
const cursor = useFloorplanDraftPreview((s) => s.cursorPoint)
|
|
3550
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
3616
3551
|
if (!cursor) {
|
|
3617
3552
|
return null
|
|
3618
3553
|
}
|
|
@@ -3624,6 +3559,8 @@ function FloorplanReferenceScaleDraftLine({
|
|
|
3624
3559
|
label={`Ref ${formatMeasurement(
|
|
3625
3560
|
Math.hypot(cursor[0] - start[0], cursor[1] - start[1]),
|
|
3626
3561
|
unit,
|
|
3562
|
+
null,
|
|
3563
|
+
metricNotation,
|
|
3627
3564
|
)}`}
|
|
3628
3565
|
palette={palette}
|
|
3629
3566
|
start={start}
|
|
@@ -4005,6 +3942,7 @@ const FloorplanSiteEdgeLabelLayer = memo(function FloorplanSiteEdgeLabelLayer({
|
|
|
4005
3942
|
unit: 'metric' | 'imperial'
|
|
4006
3943
|
unitsPerPixel: number
|
|
4007
3944
|
}) {
|
|
3945
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
4008
3946
|
if (!(shouldShow && sitePolygon && sitePolygon.polygon.length >= 2)) {
|
|
4009
3947
|
return null
|
|
4010
3948
|
}
|
|
@@ -4053,7 +3991,7 @@ const FloorplanSiteEdgeLabelLayer = memo(function FloorplanSiteEdgeLabelLayer({
|
|
|
4053
3991
|
labelAngleDeg += 180
|
|
4054
3992
|
}
|
|
4055
3993
|
|
|
4056
|
-
const label = formatMeasurement(length, unit)
|
|
3994
|
+
const label = formatMeasurement(length, unit, null, metricNotation)
|
|
4057
3995
|
const width = Math.max(label.length * upx * 6.4 + padX * 2, minWidth)
|
|
4058
3996
|
const height = fontSize + padY * 2
|
|
4059
3997
|
|
|
@@ -4109,262 +4047,6 @@ const FloorplanSiteEdgeLabelLayer = memo(function FloorplanSiteEdgeLabelLayer({
|
|
|
4109
4047
|
)
|
|
4110
4048
|
})
|
|
4111
4049
|
|
|
4112
|
-
const FloorplanZoneLayer = memo(function FloorplanZoneLayer({
|
|
4113
|
-
canSelectZones,
|
|
4114
|
-
hoveredZoneId,
|
|
4115
|
-
isDeleteMode,
|
|
4116
|
-
onZoneHoverChange,
|
|
4117
|
-
onZoneSelect,
|
|
4118
|
-
palette,
|
|
4119
|
-
selectedZoneId,
|
|
4120
|
-
zonePolygons,
|
|
4121
|
-
}: {
|
|
4122
|
-
canSelectZones: boolean
|
|
4123
|
-
hoveredZoneId: ZoneNodeType['id'] | null
|
|
4124
|
-
isDeleteMode: boolean
|
|
4125
|
-
onZoneHoverChange: (zoneId: ZoneNodeType['id'] | null) => void
|
|
4126
|
-
onZoneSelect: (zoneId: ZoneNodeType['id'], event: ReactMouseEvent<SVGElement>) => void
|
|
4127
|
-
palette: FloorplanPalette
|
|
4128
|
-
selectedZoneId: ZoneNodeType['id'] | null
|
|
4129
|
-
zonePolygons: ZonePolygonEntry[]
|
|
4130
|
-
}) {
|
|
4131
|
-
return (
|
|
4132
|
-
<>
|
|
4133
|
-
{zonePolygons.map(({ zone, points }) => {
|
|
4134
|
-
const isSelected = selectedZoneId === zone.id
|
|
4135
|
-
const isHovered = hoveredZoneId === zone.id
|
|
4136
|
-
const isDeleteHovered = isDeleteMode && isHovered
|
|
4137
|
-
|
|
4138
|
-
return (
|
|
4139
|
-
<g key={zone.id}>
|
|
4140
|
-
<polygon
|
|
4141
|
-
fill={isDeleteHovered ? palette.deleteFill : zone.color}
|
|
4142
|
-
fillOpacity={isDeleteHovered ? 0.22 : isSelected ? 0.28 : 0.16}
|
|
4143
|
-
pointerEvents="none"
|
|
4144
|
-
points={points}
|
|
4145
|
-
stroke={
|
|
4146
|
-
isDeleteHovered
|
|
4147
|
-
? palette.deleteStroke
|
|
4148
|
-
: isSelected
|
|
4149
|
-
? palette.selectedStroke
|
|
4150
|
-
: zone.color
|
|
4151
|
-
}
|
|
4152
|
-
strokeLinejoin="round"
|
|
4153
|
-
strokeOpacity={isDeleteHovered || isSelected ? 0.96 : 0.72}
|
|
4154
|
-
strokeWidth={isDeleteHovered || isSelected ? '0.08' : '0.05'}
|
|
4155
|
-
vectorEffect="non-scaling-stroke"
|
|
4156
|
-
/>
|
|
4157
|
-
{canSelectZones && (
|
|
4158
|
-
<polygon
|
|
4159
|
-
fill="none"
|
|
4160
|
-
onClick={(event) => {
|
|
4161
|
-
event.stopPropagation()
|
|
4162
|
-
onZoneSelect(zone.id, event)
|
|
4163
|
-
}}
|
|
4164
|
-
onPointerEnter={() => onZoneHoverChange(zone.id)}
|
|
4165
|
-
onPointerLeave={() => onZoneHoverChange(null)}
|
|
4166
|
-
pointerEvents="stroke"
|
|
4167
|
-
points={points}
|
|
4168
|
-
stroke="transparent"
|
|
4169
|
-
strokeLinejoin="round"
|
|
4170
|
-
strokeWidth={FLOORPLAN_WALL_HIT_STROKE_WIDTH}
|
|
4171
|
-
style={{ cursor: EDITOR_CURSOR }}
|
|
4172
|
-
vectorEffect="non-scaling-stroke"
|
|
4173
|
-
/>
|
|
4174
|
-
)}
|
|
4175
|
-
</g>
|
|
4176
|
-
)
|
|
4177
|
-
})}
|
|
4178
|
-
</>
|
|
4179
|
-
)
|
|
4180
|
-
})
|
|
4181
|
-
|
|
4182
|
-
const FLOORPLAN_ZONE_LABEL_FONT_SIZE = 0.2
|
|
4183
|
-
|
|
4184
|
-
function FloorplanZoneLabelInput({
|
|
4185
|
-
centroid,
|
|
4186
|
-
svgRef,
|
|
4187
|
-
viewBox,
|
|
4188
|
-
zone,
|
|
4189
|
-
onDone,
|
|
4190
|
-
}: {
|
|
4191
|
-
centroid: { x: number; y: number }
|
|
4192
|
-
svgRef: React.RefObject<SVGSVGElement | null>
|
|
4193
|
-
viewBox: { minX: number; minY: number; width: number; height: number }
|
|
4194
|
-
zone: ZoneNodeType
|
|
4195
|
-
onDone: () => void
|
|
4196
|
-
}) {
|
|
4197
|
-
const updateNode = useScene((s) => s.updateNode)
|
|
4198
|
-
const [value, setValue] = useState(zone.name)
|
|
4199
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
4200
|
-
|
|
4201
|
-
useEffect(() => {
|
|
4202
|
-
requestAnimationFrame(() => {
|
|
4203
|
-
inputRef.current?.focus()
|
|
4204
|
-
inputRef.current?.select()
|
|
4205
|
-
})
|
|
4206
|
-
}, [])
|
|
4207
|
-
|
|
4208
|
-
const save = useCallback(() => {
|
|
4209
|
-
const trimmed = value.trim()
|
|
4210
|
-
if (trimmed && trimmed !== zone.name) {
|
|
4211
|
-
updateNode(zone.id, { name: trimmed })
|
|
4212
|
-
}
|
|
4213
|
-
onDone()
|
|
4214
|
-
}, [value, zone.id, zone.name, updateNode, onDone])
|
|
4215
|
-
|
|
4216
|
-
// Convert SVG coordinates to screen pixel position
|
|
4217
|
-
const svgEl = svgRef.current
|
|
4218
|
-
if (!svgEl) return null
|
|
4219
|
-
const rect = svgEl.getBoundingClientRect()
|
|
4220
|
-
const screenX = ((centroid.x - viewBox.minX) / viewBox.width) * rect.width + rect.left
|
|
4221
|
-
const screenY = ((centroid.y - viewBox.minY) / viewBox.height) * rect.height + rect.top
|
|
4222
|
-
|
|
4223
|
-
return createPortal(
|
|
4224
|
-
<input
|
|
4225
|
-
onBlur={save}
|
|
4226
|
-
onChange={(e) => setValue(e.target.value)}
|
|
4227
|
-
onClick={(e) => e.stopPropagation()}
|
|
4228
|
-
onKeyDown={(e) => {
|
|
4229
|
-
e.stopPropagation()
|
|
4230
|
-
if (e.key === 'Enter') {
|
|
4231
|
-
e.preventDefault()
|
|
4232
|
-
save()
|
|
4233
|
-
}
|
|
4234
|
-
if (e.key === 'Escape') {
|
|
4235
|
-
e.preventDefault()
|
|
4236
|
-
onDone()
|
|
4237
|
-
}
|
|
4238
|
-
}}
|
|
4239
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
4240
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
4241
|
-
ref={inputRef}
|
|
4242
|
-
style={{
|
|
4243
|
-
position: 'fixed',
|
|
4244
|
-
left: screenX,
|
|
4245
|
-
top: screenY,
|
|
4246
|
-
transform: 'translate(-50%, -50%)',
|
|
4247
|
-
border: 'none',
|
|
4248
|
-
borderBottom: `1px solid ${zone.color}`,
|
|
4249
|
-
background: 'transparent',
|
|
4250
|
-
color: 'white',
|
|
4251
|
-
textShadow: `-1px -1px 0 ${zone.color}, 1px -1px 0 ${zone.color}, -1px 1px 0 ${zone.color}, 1px 1px 0 ${zone.color}`,
|
|
4252
|
-
outline: 'none',
|
|
4253
|
-
textAlign: 'center',
|
|
4254
|
-
fontSize: '14px',
|
|
4255
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
4256
|
-
padding: '2px 4px',
|
|
4257
|
-
margin: 0,
|
|
4258
|
-
zIndex: 100,
|
|
4259
|
-
width: `${Math.max((value || zone.name || '').length + 2, 6)}ch`,
|
|
4260
|
-
}}
|
|
4261
|
-
type="text"
|
|
4262
|
-
value={value}
|
|
4263
|
-
/>,
|
|
4264
|
-
document.body,
|
|
4265
|
-
)
|
|
4266
|
-
}
|
|
4267
|
-
|
|
4268
|
-
// Pencil icon as an SVG path (Lucide pencil simplified), rendered relative to the label
|
|
4269
|
-
const PENCIL_ICON_SIZE = FLOORPLAN_ZONE_LABEL_FONT_SIZE * 0.6
|
|
4270
|
-
|
|
4271
|
-
function FloorplanZoneLabel({
|
|
4272
|
-
centroid,
|
|
4273
|
-
onHoverChange,
|
|
4274
|
-
onLabelClick,
|
|
4275
|
-
zone,
|
|
4276
|
-
}: {
|
|
4277
|
-
centroid: { x: number; y: number }
|
|
4278
|
-
onHoverChange: (zoneId: ZoneNodeType['id'] | null) => void
|
|
4279
|
-
onLabelClick: (zoneId: ZoneNodeType['id'], event: ReactMouseEvent<SVGElement>) => void
|
|
4280
|
-
zone: ZoneNodeType
|
|
4281
|
-
}) {
|
|
4282
|
-
const [hovered, setHovered] = useState(false)
|
|
4283
|
-
const textRef = useRef<SVGTextElement>(null)
|
|
4284
|
-
const [textWidth, setTextWidth] = useState(0)
|
|
4285
|
-
const mode = useEditor((s) => s.mode)
|
|
4286
|
-
const deleteNode = useScene((s) => s.deleteNode)
|
|
4287
|
-
const setSelection = useViewer((s) => s.setSelection)
|
|
4288
|
-
|
|
4289
|
-
useEffect(() => {
|
|
4290
|
-
if (textRef.current) {
|
|
4291
|
-
setTextWidth(textRef.current.getComputedTextLength())
|
|
4292
|
-
}
|
|
4293
|
-
}, [])
|
|
4294
|
-
|
|
4295
|
-
const isDeleteMode = mode === 'delete'
|
|
4296
|
-
|
|
4297
|
-
return (
|
|
4298
|
-
<g
|
|
4299
|
-
cursor="pointer"
|
|
4300
|
-
onClick={(e) => {
|
|
4301
|
-
e.stopPropagation()
|
|
4302
|
-
if (isDeleteMode) {
|
|
4303
|
-
sfxEmitter.emit('sfx:structure-delete')
|
|
4304
|
-
deleteNode(zone.id as AnyNodeId)
|
|
4305
|
-
setSelection({ zoneId: null })
|
|
4306
|
-
return
|
|
4307
|
-
}
|
|
4308
|
-
onLabelClick(zone.id, e)
|
|
4309
|
-
}}
|
|
4310
|
-
onPointerEnter={() => {
|
|
4311
|
-
setHovered(true)
|
|
4312
|
-
onHoverChange(zone.id)
|
|
4313
|
-
}}
|
|
4314
|
-
onPointerLeave={() => {
|
|
4315
|
-
setHovered(false)
|
|
4316
|
-
onHoverChange(null)
|
|
4317
|
-
}}
|
|
4318
|
-
pointerEvents="auto"
|
|
4319
|
-
style={{ userSelect: 'none' }}
|
|
4320
|
-
>
|
|
4321
|
-
<text
|
|
4322
|
-
dominantBaseline="central"
|
|
4323
|
-
fill={isDeleteMode && hovered ? '#fecaca' : 'white'}
|
|
4324
|
-
fontFamily="system-ui, -apple-system, sans-serif"
|
|
4325
|
-
fontSize={FLOORPLAN_ZONE_LABEL_FONT_SIZE}
|
|
4326
|
-
fontWeight="500"
|
|
4327
|
-
paintOrder="stroke"
|
|
4328
|
-
ref={textRef}
|
|
4329
|
-
stroke={isDeleteMode && hovered ? '#dc2626' : zone.color}
|
|
4330
|
-
strokeLinecap="round"
|
|
4331
|
-
strokeLinejoin="round"
|
|
4332
|
-
strokeWidth={FLOORPLAN_ZONE_LABEL_FONT_SIZE * 0.35}
|
|
4333
|
-
textAnchor="middle"
|
|
4334
|
-
x={centroid.x}
|
|
4335
|
-
y={centroid.y}
|
|
4336
|
-
>
|
|
4337
|
-
{zone.name}
|
|
4338
|
-
</text>
|
|
4339
|
-
{/* Pencil icon — visible on hover */}
|
|
4340
|
-
{hovered && textWidth > 0 && (
|
|
4341
|
-
<g
|
|
4342
|
-
transform={`translate(${centroid.x + textWidth / 2 + PENCIL_ICON_SIZE * 0.5}, ${centroid.y - PENCIL_ICON_SIZE / 2})`}
|
|
4343
|
-
>
|
|
4344
|
-
<g transform={`scale(${PENCIL_ICON_SIZE / 24})`}>
|
|
4345
|
-
<path
|
|
4346
|
-
d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"
|
|
4347
|
-
fill="none"
|
|
4348
|
-
paintOrder="stroke"
|
|
4349
|
-
stroke={zone.color}
|
|
4350
|
-
strokeLinecap="round"
|
|
4351
|
-
strokeLinejoin="round"
|
|
4352
|
-
strokeWidth={3}
|
|
4353
|
-
/>
|
|
4354
|
-
<path
|
|
4355
|
-
d="m15 5 4 4"
|
|
4356
|
-
fill="none"
|
|
4357
|
-
stroke={zone.color}
|
|
4358
|
-
strokeLinecap="round"
|
|
4359
|
-
strokeWidth={3}
|
|
4360
|
-
/>
|
|
4361
|
-
</g>
|
|
4362
|
-
</g>
|
|
4363
|
-
)}
|
|
4364
|
-
</g>
|
|
4365
|
-
)
|
|
4366
|
-
}
|
|
4367
|
-
|
|
4368
4050
|
const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({
|
|
4369
4051
|
edgeHandles = [],
|
|
4370
4052
|
hoveredHandleId,
|
|
@@ -5028,6 +4710,7 @@ function FloorplanLinearDraftLayer({
|
|
|
5028
4710
|
unitsPerPixel: number
|
|
5029
4711
|
sceneRotationDeg: number
|
|
5030
4712
|
}) {
|
|
4713
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
5031
4714
|
const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd)
|
|
5032
4715
|
const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
|
|
5033
4716
|
const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
|
|
@@ -5146,7 +4829,7 @@ function FloorplanLinearDraftLayer({
|
|
|
5146
4829
|
}
|
|
5147
4830
|
|
|
5148
4831
|
return {
|
|
5149
|
-
lengthLabel: formatMeasurement(length, unit),
|
|
4832
|
+
lengthLabel: formatMeasurement(length, unit, null, metricNotation),
|
|
5150
4833
|
midpoint: [
|
|
5151
4834
|
(wallDraftStart[0] + wallDraftEnd[0]) / 2,
|
|
5152
4835
|
(wallDraftStart[1] + wallDraftEnd[1]) / 2,
|
|
@@ -5154,10 +4837,84 @@ function FloorplanLinearDraftLayer({
|
|
|
5154
4837
|
direction: [dx / length, dy / length] as WallPlanPoint,
|
|
5155
4838
|
angleLabels,
|
|
5156
4839
|
}
|
|
5157
|
-
}, [isWallBuildActive, unit, wallDraftEnd, wallDraftStart, walls])
|
|
4840
|
+
}, [isWallBuildActive, metricNotation, unit, wallDraftEnd, wallDraftStart, walls])
|
|
4841
|
+
|
|
4842
|
+
// Axis guides for wall and fence drafts — parity with the 3D tools'
|
|
4843
|
+
// `DraftAxisGuides`: an X/Z cross through the draft start, and a single
|
|
4844
|
+
// long line PERPENDICULAR to the segment through the moving endpoint (a
|
|
4845
|
+
// second cross would collide with the start cross on axis-aligned
|
|
4846
|
+
// segments). Long solid lines in world space — cheap for the SVG renderer,
|
|
4847
|
+
// unlike dashed strokes which tessellate per dash over 2000 m. Stroke
|
|
4848
|
+
// width is budgeted in screen pixels via `unitsPerPixel`, matching the
|
|
4849
|
+
// alignment-guide layer.
|
|
4850
|
+
const draftAxisGuideLines = useMemo(() => {
|
|
4851
|
+
const lines: Array<{ x1: number; y1: number; x2: number; y2: number }> = []
|
|
4852
|
+
const pushCross = (point: WallPlanPoint) => {
|
|
4853
|
+
lines.push(
|
|
4854
|
+
{
|
|
4855
|
+
x1: point[0] - DRAFT_AXIS_GUIDE_EXTENT,
|
|
4856
|
+
y1: point[1],
|
|
4857
|
+
x2: point[0] + DRAFT_AXIS_GUIDE_EXTENT,
|
|
4858
|
+
y2: point[1],
|
|
4859
|
+
},
|
|
4860
|
+
{
|
|
4861
|
+
x1: point[0],
|
|
4862
|
+
y1: point[1] - DRAFT_AXIS_GUIDE_EXTENT,
|
|
4863
|
+
x2: point[0],
|
|
4864
|
+
y2: point[1] + DRAFT_AXIS_GUIDE_EXTENT,
|
|
4865
|
+
},
|
|
4866
|
+
)
|
|
4867
|
+
}
|
|
4868
|
+
const pushDraft = (start: WallPlanPoint, end: WallPlanPoint) => {
|
|
4869
|
+
pushCross(start)
|
|
4870
|
+
const dx = end[0] - start[0]
|
|
4871
|
+
const dy = end[1] - start[1]
|
|
4872
|
+
const length = Math.hypot(dx, dy)
|
|
4873
|
+
if (length < 1e-6) return
|
|
4874
|
+
const nx = -dy / length
|
|
4875
|
+
const ny = dx / length
|
|
4876
|
+
lines.push({
|
|
4877
|
+
x1: end[0] - nx * DRAFT_AXIS_GUIDE_EXTENT,
|
|
4878
|
+
y1: end[1] - ny * DRAFT_AXIS_GUIDE_EXTENT,
|
|
4879
|
+
x2: end[0] + nx * DRAFT_AXIS_GUIDE_EXTENT,
|
|
4880
|
+
y2: end[1] + ny * DRAFT_AXIS_GUIDE_EXTENT,
|
|
4881
|
+
})
|
|
4882
|
+
}
|
|
4883
|
+
if (isWallBuildActive && wallDraftStart && wallDraftEnd) {
|
|
4884
|
+
pushDraft(wallDraftStart, wallDraftEnd)
|
|
4885
|
+
}
|
|
4886
|
+
if (isFenceBuildActive && fenceDraftStart && fenceDraftEnd) {
|
|
4887
|
+
pushDraft(fenceDraftStart, fenceDraftEnd)
|
|
4888
|
+
}
|
|
4889
|
+
return lines.length > 0 ? lines : null
|
|
4890
|
+
}, [
|
|
4891
|
+
isFenceBuildActive,
|
|
4892
|
+
isWallBuildActive,
|
|
4893
|
+
fenceDraftEnd,
|
|
4894
|
+
fenceDraftStart,
|
|
4895
|
+
wallDraftEnd,
|
|
4896
|
+
wallDraftStart,
|
|
4897
|
+
])
|
|
5158
4898
|
|
|
5159
4899
|
return (
|
|
5160
4900
|
<>
|
|
4901
|
+
{draftAxisGuideLines && (
|
|
4902
|
+
<g pointerEvents="none">
|
|
4903
|
+
{draftAxisGuideLines.map((line, index) => (
|
|
4904
|
+
<line
|
|
4905
|
+
key={index}
|
|
4906
|
+
stroke="#818cf8"
|
|
4907
|
+
strokeOpacity={0.5}
|
|
4908
|
+
strokeWidth={unitsPerPixel}
|
|
4909
|
+
x1={line.x1}
|
|
4910
|
+
x2={line.x2}
|
|
4911
|
+
y1={line.y1}
|
|
4912
|
+
y2={line.y2}
|
|
4913
|
+
/>
|
|
4914
|
+
))}
|
|
4915
|
+
</g>
|
|
4916
|
+
)}
|
|
4917
|
+
|
|
5161
4918
|
<FloorplanDraftLayer
|
|
5162
4919
|
anchorFill={draftStroke}
|
|
5163
4920
|
draftAnchorPoints={EMPTY_DRAFT_ANCHOR_POINTS}
|
|
@@ -5186,6 +4943,8 @@ function FloorplanLinearDraftLayer({
|
|
|
5186
4943
|
}
|
|
5187
4944
|
|
|
5188
4945
|
const EMPTY_DRAFT_ANCHOR_POINTS: Array<{ x: number; y: number; isPrimary: boolean }> = []
|
|
4946
|
+
/** World-space half-length of the 2D draft axis guide lines (matches the 3D tools' 2000 m guides). */
|
|
4947
|
+
const DRAFT_AXIS_GUIDE_EXTENT = 1000
|
|
5189
4948
|
|
|
5190
4949
|
export function FloorplanPanel({
|
|
5191
4950
|
/**
|
|
@@ -5195,15 +4954,20 @@ export function FloorplanPanel({
|
|
|
5195
4954
|
* in 2d, 3d, and split modes, while this panel itself may be display:none.
|
|
5196
4955
|
*/
|
|
5197
4956
|
compassHost,
|
|
4957
|
+
floorplanSceneSlot,
|
|
5198
4958
|
}: {
|
|
5199
4959
|
compassHost?: HTMLElement | null
|
|
4960
|
+
floorplanSceneSlot?: ReactNode
|
|
5200
4961
|
}) {
|
|
4962
|
+
useFloorplanCameraSyncBridge()
|
|
5201
4963
|
const viewportHostRef = useRef<HTMLDivElement>(null)
|
|
5202
4964
|
const svgRef = useRef<SVGSVGElement>(null)
|
|
4965
|
+
const floorplanBackgroundRef = useRef<SVGRectElement>(null)
|
|
5203
4966
|
const floorplanSceneRef = useRef<SVGGElement>(null)
|
|
5204
4967
|
const floorplanContentRef = useRef<SVGGElement>(null)
|
|
5205
4968
|
const panStateRef = useRef<PanState | null>(null)
|
|
5206
4969
|
const floorplanRotationStateRef = useRef<FloorplanRotationState | null>(null)
|
|
4970
|
+
const pendingFloorplanRotationRestoreRef = useRef<FloorplanRotationState | null>(null)
|
|
5207
4971
|
const floorplanSpacePanPressedRef = useRef(false)
|
|
5208
4972
|
const floorplanNavigationClickSuppressedRef = useRef(false)
|
|
5209
4973
|
const guideInteractionRef = useRef<GuideInteractionState | null>(null)
|
|
@@ -5227,6 +4991,31 @@ export function FloorplanPanel({
|
|
|
5227
4991
|
const latestFittedViewportRef = useRef<FloorplanViewport | null>(null)
|
|
5228
4992
|
const floorplanViewAnimationFrameRef = useRef<number | null>(null)
|
|
5229
4993
|
const floorplanViewAnimationTargetRef = useRef<FloorplanViewAnimationTarget | null>(null)
|
|
4994
|
+
const floorplanZoomCommitTimerRef = useRef<number | null>(null)
|
|
4995
|
+
const floorplanRenderScaleCommitTimerRef = useRef<number | null>(null)
|
|
4996
|
+
const floorplanViewportInteractionInProgressRef = useRef(false)
|
|
4997
|
+
const floorplanImperativeViewBoxRef = useRef<FloorplanPresentationViewBox | null>(null)
|
|
4998
|
+
const floorplanNavigationSyncPresentationRef =
|
|
4999
|
+
useRef<FloorplanNavigationSyncPresentationState | null>(null)
|
|
5000
|
+
const applyFloorplanNavigationSyncPresentationRef = useRef<(pose: NavigationSyncPose) => void>(
|
|
5001
|
+
() => {},
|
|
5002
|
+
)
|
|
5003
|
+
const commitFloorplanNavigationSyncPresentationRef = useRef<(pose: NavigationSyncPose) => void>(
|
|
5004
|
+
() => {},
|
|
5005
|
+
)
|
|
5006
|
+
const floorplanNavigationSyncSchedulerRef =
|
|
5007
|
+
useRef<FloorplanNavigationSyncScheduler<NavigationSyncPose> | null>(null)
|
|
5008
|
+
const latestFloorplanRenderUnitsPerPixelRef = useRef(1)
|
|
5009
|
+
const floorplanZoomPoseRef = useRef<{
|
|
5010
|
+
localCenter: SvgPoint
|
|
5011
|
+
userRotationDeg: number
|
|
5012
|
+
viewWidth: number
|
|
5013
|
+
} | null>(null)
|
|
5014
|
+
const floorplanPanPoseRef = useRef<{
|
|
5015
|
+
localCenter: SvgPoint
|
|
5016
|
+
userRotationDeg: number
|
|
5017
|
+
viewWidth: number
|
|
5018
|
+
} | null>(null)
|
|
5230
5019
|
const latestNavigationSyncPoseRef = useRef<NavigationSyncPose | null>(
|
|
5231
5020
|
useEditor.getState().navigationSyncPose,
|
|
5232
5021
|
)
|
|
@@ -5241,6 +5030,7 @@ export function FloorplanPanel({
|
|
|
5241
5030
|
const setPreviewSelectedIds = useViewer((state) => state.setPreviewSelectedIds)
|
|
5242
5031
|
const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
|
5243
5032
|
const unit = useViewer((state) => state.unit)
|
|
5033
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
5244
5034
|
const showGrid = useViewer((state) => state.showGrid)
|
|
5245
5035
|
const showGuides = useViewer((state) => state.showGuides)
|
|
5246
5036
|
const setShowGuides = useViewer((state) => state.setShowGuides)
|
|
@@ -5331,9 +5121,14 @@ export function FloorplanPanel({
|
|
|
5331
5121
|
const buildingRotationDeg = (buildingRotationY * 180) / Math.PI
|
|
5332
5122
|
const floorplanSceneRotationDeg =
|
|
5333
5123
|
FLOORPLAN_VIEW_ROTATION_DEG + floorplanUserRotationDeg - buildingRotationDeg
|
|
5124
|
+
const getFloorplanSceneRotationDeg = useCallback(
|
|
5125
|
+
() =>
|
|
5126
|
+
FLOORPLAN_VIEW_ROTATION_DEG + latestFloorplanUserRotationDegRef.current - buildingRotationDeg,
|
|
5127
|
+
[buildingRotationDeg],
|
|
5128
|
+
)
|
|
5334
5129
|
// Only sync ref from state when floorplan is open (state is source of truth).
|
|
5335
5130
|
// When hidden, the imperative 3D path owns the ref and must not be clobbered.
|
|
5336
|
-
if (isFloorplanOpenRef.current) {
|
|
5131
|
+
if (isFloorplanOpenRef.current && !floorplanViewportInteractionInProgressRef.current) {
|
|
5337
5132
|
latestFloorplanUserRotationDegRef.current = floorplanUserRotationDeg
|
|
5338
5133
|
}
|
|
5339
5134
|
|
|
@@ -5343,6 +5138,8 @@ export function FloorplanPanel({
|
|
|
5343
5138
|
// Shims keep the `setXDraftEnd(value | prev => …)` call sites unchanged.
|
|
5344
5139
|
const [draftStart, setDraftStart] = useState<WallPlanPoint | null>(null)
|
|
5345
5140
|
const [wallChainFirstVertex, setWallChainFirstVertex] = useState<WallPlanPoint | null>(null)
|
|
5141
|
+
const wallConstructionOptionsRef =
|
|
5142
|
+
useRef<ReturnType<typeof resolveTerrainWallConstructionOptions>>(undefined)
|
|
5346
5143
|
// Walls committed by the current 2D-only chain — exclusion set for the
|
|
5347
5144
|
// T-junction chain-termination test (mirrors the 3D tool's `chainWallIds`).
|
|
5348
5145
|
const wallChainWallIdsRef = useRef<string[]>([])
|
|
@@ -5371,6 +5168,19 @@ export function FloorplanPanel({
|
|
|
5371
5168
|
)
|
|
5372
5169
|
const [ceilingDraftPoints, setCeilingDraftPoints] = useState<WallPlanPoint[]>([])
|
|
5373
5170
|
const [slabDraftPoints, setSlabDraftPoints] = useState<WallPlanPoint[]>([])
|
|
5171
|
+
const slabPlacementBaseRef = useRef<number | null>(null)
|
|
5172
|
+
// Mirror the per-click draft START anchors into the shared draft store so
|
|
5173
|
+
// out-of-tree consumers (such as host-owned preview publishers) see the whole open
|
|
5174
|
+
// segment without subscribing to this panel's state.
|
|
5175
|
+
useEffect(() => {
|
|
5176
|
+
useFloorplanDraftPreview.getState().setWallDraftStart(draftStart)
|
|
5177
|
+
}, [draftStart])
|
|
5178
|
+
useEffect(() => {
|
|
5179
|
+
useFloorplanDraftPreview.getState().setFenceDraftStart(fenceDraftStart)
|
|
5180
|
+
}, [fenceDraftStart])
|
|
5181
|
+
useEffect(() => {
|
|
5182
|
+
useFloorplanDraftPreview.getState().setRoofDraftStart(roofDraftStart)
|
|
5183
|
+
}, [roofDraftStart])
|
|
5374
5184
|
const [zoneDraftPoints, setZoneDraftPoints] = useState<WallPlanPoint[]>([])
|
|
5375
5185
|
const [siteBoundaryDraft, setSiteBoundaryDraft] = useState<SiteBoundaryDraft | null>(null)
|
|
5376
5186
|
const [siteVertexDragState, setSiteVertexDragState] = useState<SiteVertexDragState | null>(null)
|
|
@@ -5424,24 +5234,7 @@ export function FloorplanPanel({
|
|
|
5424
5234
|
)
|
|
5425
5235
|
const [wallEndpointDraft, setWallEndpointDraft] = useState<WallEndpointDraft | null>(null)
|
|
5426
5236
|
const [wallCurveDraft, setWallCurveDraft] = useState<WallCurveDraft | null>(null)
|
|
5427
|
-
const [hoveredOpeningId, setHoveredOpeningId] = useState<OpeningNode['id'] | null>(null)
|
|
5428
|
-
const [hoveredWallId, setHoveredWallId] = useState<WallNode['id'] | null>(null)
|
|
5429
|
-
const [hoveredFenceId, setHoveredFenceId] = useState<FenceNode['id'] | null>(null)
|
|
5430
|
-
const [hoveredSlabId, setHoveredSlabId] = useState<SlabNode['id'] | null>(null)
|
|
5431
|
-
const [hoveredCeilingId, setHoveredCeilingId] = useState<CeilingNode['id'] | null>(null)
|
|
5432
|
-
const [hoveredItemId, setHoveredItemId] = useState<ItemNode['id'] | null>(null)
|
|
5433
|
-
const [hoveredSpawnId, setHoveredSpawnId] = useState<SpawnNode['id'] | null>(null)
|
|
5434
|
-
const [hoveredStairId, setHoveredStairId] = useState<StairNode['id'] | null>(null)
|
|
5435
|
-
const [hoveredElevatorId, setHoveredElevatorId] = useState<ElevatorNode['id'] | null>(null)
|
|
5436
|
-
const [elevatorResizeDragState, setElevatorResizeDragState] =
|
|
5437
|
-
useState<ElevatorResizeDragState | null>(null)
|
|
5438
|
-
const [hoveredZoneId, setHoveredZoneId] = useState<ZoneNodeType['id'] | null>(null)
|
|
5439
|
-
const [hoveredEndpointId, setHoveredEndpointId] = useState<string | null>(null)
|
|
5440
|
-
const [hoveredWallCurveHandleId, setHoveredWallCurveHandleId] = useState<string | null>(null)
|
|
5441
5237
|
const [hoveredSiteHandleId, setHoveredSiteHandleId] = useState<string | null>(null)
|
|
5442
|
-
const [hoveredSlabHandleId, setHoveredSlabHandleId] = useState<string | null>(null)
|
|
5443
|
-
const [hoveredCeilingHandleId, setHoveredCeilingHandleId] = useState<string | null>(null)
|
|
5444
|
-
const [hoveredZoneHandleId, setHoveredZoneHandleId] = useState<string | null>(null)
|
|
5445
5238
|
const [hoveredGuideCorner, setHoveredGuideCorner] = useState<GuideCorner | null>(null)
|
|
5446
5239
|
const floorplanSelectionTool = useEditor((s) => s.floorplanSelectionTool)
|
|
5447
5240
|
const setFloorplanSelectionTool = useEditor((s) => s.setFloorplanSelectionTool)
|
|
@@ -5530,12 +5323,15 @@ export function FloorplanPanel({
|
|
|
5530
5323
|
const [isPanelReady, setIsPanelReady] = useState(false)
|
|
5531
5324
|
const [surfaceSize, setSurfaceSize] = useState({ width: 1, height: 1 })
|
|
5532
5325
|
const [viewport, setViewport] = useState<FloorplanViewport | null>(null)
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5326
|
+
const [floorplanRenderUnitsPerPixel, setFloorplanRenderUnitsPerPixel] = useState<number | null>(
|
|
5327
|
+
null,
|
|
5328
|
+
)
|
|
5329
|
+
if (!floorplanViewportInteractionInProgressRef.current) {
|
|
5330
|
+
latestViewportRef.current = viewport
|
|
5331
|
+
}
|
|
5332
|
+
// Tight bbox of the registry-rendered floor-plan content, read via SVG
|
|
5333
|
+
// `getBBox()` after geometry changes settle. `fittedViewport` combines it
|
|
5334
|
+
// with the editor-owned site boundary below.
|
|
5539
5335
|
const [measuredSceneBBox, setMeasuredSceneBBox] = useState<{
|
|
5540
5336
|
x: number
|
|
5541
5337
|
y: number
|
|
@@ -5776,28 +5572,9 @@ export function FloorplanPanel({
|
|
|
5776
5572
|
|
|
5777
5573
|
return hasPreviewWalls ? nextFloorplanWallById : floorplanWallById
|
|
5778
5574
|
}, [displayWallById, floorplanWallById, wallCurveDraft, wallEndpointDraft])
|
|
5779
|
-
// Fence is fully registry-driven (`def.floorplan` + `buildFenceFloorplan`).
|
|
5780
|
-
// The legacy entry list is permanently empty; kept as a typed stable
|
|
5781
|
-
// reference so downstream prop sites stay typed without each having to
|
|
5782
|
-
// declare its own `[]`.
|
|
5783
|
-
const floorplanFenceEntries = useMemo<FloorplanFenceEntry[]>(() => [], [])
|
|
5784
|
-
// Wall is fully registry-driven. Empty stable arrays for the legacy
|
|
5785
|
-
// entry lists; consumers' map / iteration paths become no-ops.
|
|
5786
|
-
const wallPolygons = useMemo<WallPolygonEntry[]>(() => [], [])
|
|
5787
|
-
const displayWallPolygons = useMemo<WallPolygonEntry[]>(() => [], [])
|
|
5788
|
-
|
|
5789
|
-
// Doors + windows fully registry-driven via `def.floorplan`.
|
|
5790
|
-
const openingsPolygons = useMemo<OpeningPolygonEntry[]>(() => [], [])
|
|
5791
|
-
// Slab + ceiling fully registry-driven via `def.floorplan`. Same
|
|
5792
|
-
// empty-stable-array pattern.
|
|
5793
|
-
const slabPolygons = useMemo<SlabPolygonEntry[]>(() => [], [])
|
|
5794
|
-
const displaySlabPolygons = useMemo<SlabPolygonEntry[]>(() => [], [])
|
|
5795
|
-
const ceilingPolygons = useMemo<CeilingPolygonEntry[]>(() => [], [])
|
|
5796
|
-
const displayCeilingPolygons = useMemo<CeilingPolygonEntry[]>(() => [], [])
|
|
5797
5575
|
// Ceilings on the active level, projected to 2D polygons for hit-testing
|
|
5798
|
-
// ceiling-item placement clicks/moves.
|
|
5799
|
-
//
|
|
5800
|
-
// memo is the placement-side counterpart, separate from rendering.
|
|
5576
|
+
// ceiling-item placement clicks/moves. Committed ceilings render through
|
|
5577
|
+
// the registry; this memo is placement data, not a rendering fallback.
|
|
5801
5578
|
const ceilingHitEntries = useMemo(
|
|
5802
5579
|
() =>
|
|
5803
5580
|
ceilings.map((ceiling) => ({
|
|
@@ -5809,11 +5586,6 @@ export function FloorplanPanel({
|
|
|
5809
5586
|
})),
|
|
5810
5587
|
[ceilings],
|
|
5811
5588
|
)
|
|
5812
|
-
// Zone fully registry-driven via `def.floorplan`.
|
|
5813
|
-
const zonePolygons = useMemo<ZonePolygonEntry[]>(() => [], [])
|
|
5814
|
-
const displayZonePolygons = useMemo<ZonePolygonEntry[]>(() => [], [])
|
|
5815
|
-
// Column fully registry-driven via `def.floorplan`.
|
|
5816
|
-
const floorplanColumnEntries = useMemo<FloorplanColumnEntry[]>(() => [], [])
|
|
5817
5589
|
const levelDescendantNodeById = useMemo(
|
|
5818
5590
|
() => new Map(levelDescendantNodes.map((node) => [node.id, node] as const)),
|
|
5819
5591
|
[levelDescendantNodes],
|
|
@@ -5836,11 +5608,6 @@ export function FloorplanPanel({
|
|
|
5836
5608
|
),
|
|
5837
5609
|
[levelDescendantNodes],
|
|
5838
5610
|
)
|
|
5839
|
-
// Spawn + item fully registry-driven.
|
|
5840
|
-
const floorplanSpawnEntries = useMemo<FloorplanSpawnEntry[]>(() => [], [])
|
|
5841
|
-
const floorplanItemEntries = useMemo<FloorplanItemEntry[]>(() => [], [])
|
|
5842
|
-
// Elevator fully registry-driven via `def.floorplan`.
|
|
5843
|
-
const floorplanElevatorEntries = useMemo<FloorplanElevatorEntry[]>(() => [], [])
|
|
5844
5611
|
const referenceFloorLevel = useMemo(() => {
|
|
5845
5612
|
if (!(showReferenceFloor && levelNode)) {
|
|
5846
5613
|
return null
|
|
@@ -6083,26 +5850,6 @@ export function FloorplanPanel({
|
|
|
6083
5850
|
wallPolygons,
|
|
6084
5851
|
}
|
|
6085
5852
|
}, [referenceFloorDescendants, referenceFloorLevel])
|
|
6086
|
-
// Pending-mesh check was a flag the legacy active-level item entries
|
|
6087
|
-
// raised when their polygon was the dimension fallback (waiting for
|
|
6088
|
-
// the GLB to load to produce a tighter convex hull). Items are now
|
|
6089
|
-
// registry-rendered, so the active-level entry list is always empty
|
|
6090
|
-
// and this flag is permanently false.
|
|
6091
|
-
const hasPendingItemMeshFootprints = false
|
|
6092
|
-
// Stair fully registry-driven via `def.floorplan` (the parent walks
|
|
6093
|
-
// its `stair-segment` children inside `buildStairFloorplan` to handle
|
|
6094
|
-
// the cumulative-transform chain). `FloorplanRegistryLayer` renders
|
|
6095
|
-
// the result; this legacy list stays empty.
|
|
6096
|
-
const floorplanStairEntries = useMemo<FloorplanStairEntry[]>(() => [], [])
|
|
6097
|
-
// Roof / roof-segment fully registry-driven via def.floorplan.
|
|
6098
|
-
const floorplanRoofEntries = useMemo<FloorplanRoofEntry[]>(() => [], [])
|
|
6099
|
-
// Slab / ceiling / zone are registry-driven; the polygon-handle, hole
|
|
6100
|
-
// editor, and boundary-edit affordances live on `def.floorplanAffordances`.
|
|
6101
|
-
// These legacy lookups stay as null stubs so the hole-editing fallbacks
|
|
6102
|
-
// that still reference them compile cleanly.
|
|
6103
|
-
const selectedSlabEntry = null as SlabPolygonEntry | null
|
|
6104
|
-
const selectedCeilingEntry = null as CeilingPolygonEntry | null
|
|
6105
|
-
const selectedZoneEntry = null as ZonePolygonEntry | null
|
|
6106
5853
|
const slabById = useMemo(() => new Map(slabs.map((slab) => [slab.id, slab] as const)), [slabs])
|
|
6107
5854
|
const zoneById = useMemo(() => new Map(zones.map((zone) => [zone.id, zone] as const)), [zones])
|
|
6108
5855
|
const ceilingById = useMemo(
|
|
@@ -6317,13 +6064,12 @@ export function FloorplanPanel({
|
|
|
6317
6064
|
isFloorplanItemContextActive
|
|
6318
6065
|
const visibleSitePolygon = displaySitePolygon
|
|
6319
6066
|
const canUseSiteBoundaryVertexHandles =
|
|
6320
|
-
visibleSitePolygon !== null && (
|
|
6067
|
+
visibleSitePolygon !== null && siteBoundaryHandlesEnabled({ mode, phase })
|
|
6321
6068
|
const isSiteBoundaryHighlighted = isSiteEditActive || siteVertexDragState !== null
|
|
6322
6069
|
const shouldShowSiteEdgeLabels =
|
|
6323
6070
|
Boolean(visibleSitePolygon) &&
|
|
6324
6071
|
activeHandleDrag?.nodeId === visibleSitePolygon?.site.id &&
|
|
6325
6072
|
activeHandleDrag?.label === SITE_BOUNDARY_DRAG_LABEL
|
|
6326
|
-
const visibleZonePolygons = displayZonePolygons
|
|
6327
6073
|
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds])
|
|
6328
6074
|
const highlightedFloorplanIdSet = useMemo(
|
|
6329
6075
|
() => new Set([...selectedIds, ...previewSelectedIds]),
|
|
@@ -6402,38 +6148,60 @@ export function FloorplanPanel({
|
|
|
6402
6148
|
slabDraftPoints,
|
|
6403
6149
|
zoneDraftPoints,
|
|
6404
6150
|
])
|
|
6151
|
+
const activePolygonDraftType = isCeilingBuildActive
|
|
6152
|
+
? 'ceiling'
|
|
6153
|
+
: isZoneBuildActive
|
|
6154
|
+
? 'zone'
|
|
6155
|
+
: isSlabBuildActive
|
|
6156
|
+
? 'slab'
|
|
6157
|
+
: null
|
|
6158
|
+
useEffect(() => {
|
|
6159
|
+
useFloorplanDraftPreview
|
|
6160
|
+
.getState()
|
|
6161
|
+
.setPolygonDraft(activePolygonDraftType, activePolygonDraftPoints)
|
|
6162
|
+
}, [activePolygonDraftPoints, activePolygonDraftType])
|
|
6405
6163
|
// The cursor-following polygon-draft preview (slab / zone / ceiling) moved into
|
|
6406
6164
|
// `FloorplanDraftCursorLayer`, which reads the live cursor from the draft store
|
|
6407
6165
|
// so it re-renders per move without re-rendering this panel.
|
|
6408
6166
|
|
|
6409
6167
|
const svgAspectRatio = surfaceSize.width / surfaceSize.height || 1
|
|
6168
|
+
const applyFloorplanViewportImperatively = useCallback(
|
|
6169
|
+
(nextViewport: FloorplanViewport) => {
|
|
6170
|
+
const nextHeight = nextViewport.width / svgAspectRatio
|
|
6171
|
+
const nextMinX = nextViewport.centerX - nextViewport.width / 2
|
|
6172
|
+
const nextMinY = nextViewport.centerY - nextHeight / 2
|
|
6173
|
+
const nextViewBox = {
|
|
6174
|
+
minX: nextMinX,
|
|
6175
|
+
minY: nextMinY,
|
|
6176
|
+
width: nextViewport.width,
|
|
6177
|
+
height: nextHeight,
|
|
6178
|
+
}
|
|
6179
|
+
const nextPaintViewBox = getFloorplanRotationOverscanViewBox(nextViewBox)
|
|
6180
|
+
floorplanImperativeViewBoxRef.current = nextViewBox
|
|
6181
|
+
hasUserAdjustedViewportRef.current = true
|
|
6182
|
+
latestViewportRef.current = nextViewport
|
|
6183
|
+
svgRef.current?.setAttribute(
|
|
6184
|
+
'viewBox',
|
|
6185
|
+
`${nextMinX} ${nextMinY} ${nextViewport.width} ${nextHeight}`,
|
|
6186
|
+
)
|
|
6187
|
+
const background = floorplanBackgroundRef.current
|
|
6188
|
+
if (background) {
|
|
6189
|
+
background.setAttribute('x', String(nextPaintViewBox.minX))
|
|
6190
|
+
background.setAttribute('y', String(nextPaintViewBox.minY))
|
|
6191
|
+
background.setAttribute('width', String(nextPaintViewBox.width))
|
|
6192
|
+
background.setAttribute('height', String(nextPaintViewBox.height))
|
|
6193
|
+
}
|
|
6194
|
+
},
|
|
6195
|
+
[svgAspectRatio],
|
|
6196
|
+
)
|
|
6410
6197
|
|
|
6411
6198
|
const fittedViewport = useMemo(() => {
|
|
6412
|
-
// Collect bounds from the legacy polygon arrays first. Most are empty
|
|
6413
|
-
// stubs (rendering moved to the registry layer), but we still honor
|
|
6414
|
-
// anything that does emit points so the fit is correct during the
|
|
6415
|
-
// brief window before `measuredSceneBBox` is populated.
|
|
6416
|
-
const legacyPoints = [
|
|
6417
|
-
...(visibleSitePolygon ? visibleSitePolygon.polygon : []),
|
|
6418
|
-
...displayCeilingPolygons.flatMap((entry) => entry.polygon),
|
|
6419
|
-
...displaySlabPolygons.flatMap((entry) => entry.polygon),
|
|
6420
|
-
...floorplanElevatorEntries.flatMap((entry) => entry.polygon),
|
|
6421
|
-
...floorplanFenceEntries.flatMap((entry) => entry.centerline),
|
|
6422
|
-
...floorplanItemEntries.flatMap((entry) => entry.polygon),
|
|
6423
|
-
...floorplanRoofEntries.flatMap((entry) =>
|
|
6424
|
-
entry.segments.flatMap((segmentEntry) => segmentEntry.polygon),
|
|
6425
|
-
),
|
|
6426
|
-
...floorplanStairEntries.flatMap((entry) => entry.hitPolygons.flat()),
|
|
6427
|
-
...visibleZonePolygons.flatMap((entry) => entry.polygon),
|
|
6428
|
-
...wallPolygons.flatMap((entry) => entry.polygon),
|
|
6429
|
-
]
|
|
6430
|
-
|
|
6431
6199
|
let minX = Number.POSITIVE_INFINITY
|
|
6432
6200
|
let maxX = Number.NEGATIVE_INFINITY
|
|
6433
6201
|
let minY = Number.POSITIVE_INFINITY
|
|
6434
6202
|
let maxY = Number.NEGATIVE_INFINITY
|
|
6435
6203
|
|
|
6436
|
-
for (const point of
|
|
6204
|
+
for (const point of visibleSitePolygon?.polygon ?? []) {
|
|
6437
6205
|
const svgPoint = rotateSvgPoint(toSvgPoint(point), floorplanSceneRotationDeg)
|
|
6438
6206
|
minX = Math.min(minX, svgPoint.x)
|
|
6439
6207
|
maxX = Math.max(maxX, svgPoint.x)
|
|
@@ -6481,59 +6249,74 @@ export function FloorplanPanel({
|
|
|
6481
6249
|
centerY,
|
|
6482
6250
|
width,
|
|
6483
6251
|
}
|
|
6484
|
-
}, [
|
|
6485
|
-
displayCeilingPolygons,
|
|
6486
|
-
displaySlabPolygons,
|
|
6487
|
-
floorplanElevatorEntries,
|
|
6488
|
-
floorplanFenceEntries,
|
|
6489
|
-
floorplanItemEntries,
|
|
6490
|
-
floorplanRoofEntries,
|
|
6491
|
-
floorplanSceneRotationDeg,
|
|
6492
|
-
floorplanStairEntries,
|
|
6493
|
-
measuredSceneBBox,
|
|
6494
|
-
svgAspectRatio,
|
|
6495
|
-
visibleSitePolygon,
|
|
6496
|
-
visibleZonePolygons,
|
|
6497
|
-
wallPolygons,
|
|
6498
|
-
])
|
|
6252
|
+
}, [floorplanSceneRotationDeg, measuredSceneBBox, svgAspectRatio, visibleSitePolygon])
|
|
6499
6253
|
latestFittedViewportRef.current = fittedViewport
|
|
6500
6254
|
|
|
6501
|
-
// Measure the
|
|
6502
|
-
//
|
|
6503
|
-
//
|
|
6504
|
-
//
|
|
6505
|
-
// We measure the content-only sub-group (not the full scene group) to
|
|
6506
|
-
// exclude the grid layer, whose extent tracks the viewBox and would
|
|
6507
|
-
// otherwise create a measure→fit→measure update loop.
|
|
6255
|
+
// Measure the content-only subtree after its geometry settles. ViewBox-only
|
|
6256
|
+
// navigation does not change these bounds and must not force `getBBox()` on
|
|
6257
|
+
// every animation frame.
|
|
6258
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: visibility remounts the observed SVG subtree.
|
|
6508
6259
|
useLayoutEffect(() => {
|
|
6509
6260
|
const el = floorplanContentRef.current
|
|
6510
6261
|
if (!el) return
|
|
6511
|
-
let
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6262
|
+
let scheduledFrame: number | null = null
|
|
6263
|
+
let mutationVersion = 0
|
|
6264
|
+
let observedVersion = 0
|
|
6265
|
+
const measure = () => {
|
|
6266
|
+
let bbox: { x: number; y: number; width: number; height: number }
|
|
6267
|
+
try {
|
|
6268
|
+
const measured = el.getBBox()
|
|
6269
|
+
bbox = {
|
|
6270
|
+
x: measured.x,
|
|
6271
|
+
y: measured.y,
|
|
6272
|
+
width: measured.width,
|
|
6273
|
+
height: measured.height,
|
|
6274
|
+
}
|
|
6275
|
+
} catch {
|
|
6276
|
+
return
|
|
6277
|
+
}
|
|
6278
|
+
if (bbox.width <= 0 && bbox.height <= 0) return
|
|
6279
|
+
setMeasuredSceneBBox((prev) => {
|
|
6280
|
+
if (
|
|
6281
|
+
prev &&
|
|
6282
|
+
prev.x === bbox.x &&
|
|
6283
|
+
prev.y === bbox.y &&
|
|
6284
|
+
prev.width === bbox.width &&
|
|
6285
|
+
prev.height === bbox.height
|
|
6286
|
+
) {
|
|
6287
|
+
return prev
|
|
6288
|
+
}
|
|
6289
|
+
return bbox
|
|
6290
|
+
})
|
|
6522
6291
|
}
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
prev.y === bbox.y &&
|
|
6529
|
-
prev.width === bbox.width &&
|
|
6530
|
-
prev.height === bbox.height
|
|
6531
|
-
) {
|
|
6532
|
-
return prev
|
|
6292
|
+
const flushWhenSettled = () => {
|
|
6293
|
+
if (observedVersion !== mutationVersion) {
|
|
6294
|
+
observedVersion = mutationVersion
|
|
6295
|
+
scheduledFrame = requestAnimationFrame(flushWhenSettled)
|
|
6296
|
+
return
|
|
6533
6297
|
}
|
|
6534
|
-
|
|
6298
|
+
scheduledFrame = null
|
|
6299
|
+
measure()
|
|
6300
|
+
}
|
|
6301
|
+
const observer = new MutationObserver(() => {
|
|
6302
|
+
mutationVersion += 1
|
|
6303
|
+
if (scheduledFrame !== null) return
|
|
6304
|
+
observedVersion = mutationVersion - 1
|
|
6305
|
+
scheduledFrame = requestAnimationFrame(flushWhenSettled)
|
|
6535
6306
|
})
|
|
6536
|
-
|
|
6307
|
+
|
|
6308
|
+
measure()
|
|
6309
|
+
observer.observe(el, {
|
|
6310
|
+
attributes: true,
|
|
6311
|
+
characterData: true,
|
|
6312
|
+
childList: true,
|
|
6313
|
+
subtree: true,
|
|
6314
|
+
})
|
|
6315
|
+
return () => {
|
|
6316
|
+
observer.disconnect()
|
|
6317
|
+
if (scheduledFrame !== null) cancelAnimationFrame(scheduledFrame)
|
|
6318
|
+
}
|
|
6319
|
+
}, [isFloorplanOpen])
|
|
6537
6320
|
|
|
6538
6321
|
const applyFloorplanNavigationState = useCallback(
|
|
6539
6322
|
(nextViewport: FloorplanViewport, userRotationDeg: number) => {
|
|
@@ -6550,6 +6333,136 @@ export function FloorplanPanel({
|
|
|
6550
6333
|
[],
|
|
6551
6334
|
)
|
|
6552
6335
|
|
|
6336
|
+
const applyFloorplanNavigationSyncPresentation = useCallback(
|
|
6337
|
+
(pose: NavigationSyncPose) => {
|
|
6338
|
+
const currentViewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
6339
|
+
const svg = svgRef.current
|
|
6340
|
+
if (!(currentViewport && svg)) return
|
|
6341
|
+
|
|
6342
|
+
let presentationState = floorplanNavigationSyncPresentationRef.current
|
|
6343
|
+
if (!presentationState) {
|
|
6344
|
+
const initialUserRotationDeg = latestFloorplanUserRotationDegRef.current
|
|
6345
|
+
const initialSceneRotationDeg =
|
|
6346
|
+
FLOORPLAN_VIEW_ROTATION_DEG + initialUserRotationDeg - buildingRotationDeg
|
|
6347
|
+
presentationState = {
|
|
6348
|
+
initialUserRotationDeg,
|
|
6349
|
+
initialSceneRotationDeg,
|
|
6350
|
+
latestUserRotationDeg: initialUserRotationDeg,
|
|
6351
|
+
latestSceneRotationDeg: initialSceneRotationDeg,
|
|
6352
|
+
latestViewport: currentViewport,
|
|
6353
|
+
svg,
|
|
6354
|
+
svgStyle: {
|
|
6355
|
+
transform: svg.style.transform,
|
|
6356
|
+
transformOrigin: svg.style.transformOrigin,
|
|
6357
|
+
willChange: svg.style.willChange,
|
|
6358
|
+
},
|
|
6359
|
+
}
|
|
6360
|
+
floorplanNavigationSyncPresentationRef.current = presentationState
|
|
6361
|
+
floorplanViewportInteractionInProgressRef.current = true
|
|
6362
|
+
svg.style.transformOrigin = 'center'
|
|
6363
|
+
svg.style.willChange = 'transform'
|
|
6364
|
+
}
|
|
6365
|
+
|
|
6366
|
+
const nextUserRotationDeg = floorplanRotationFromCameraAzimuth(
|
|
6367
|
+
pose.azimuth,
|
|
6368
|
+
presentationState.latestUserRotationDeg,
|
|
6369
|
+
)
|
|
6370
|
+
const localCenter = worldToFloorplanLocalPoint(
|
|
6371
|
+
pose.target[0],
|
|
6372
|
+
pose.target[2],
|
|
6373
|
+
buildingPosition,
|
|
6374
|
+
buildingRotationY,
|
|
6375
|
+
)
|
|
6376
|
+
const nextWidth = resolveFloorplanViewWidth(
|
|
6377
|
+
pose.viewWidth,
|
|
6378
|
+
currentViewport.width,
|
|
6379
|
+
latestFittedViewportRef.current,
|
|
6380
|
+
false,
|
|
6381
|
+
)
|
|
6382
|
+
const nextSceneRotationDeg =
|
|
6383
|
+
FLOORPLAN_VIEW_ROTATION_DEG + nextUserRotationDeg - buildingRotationDeg
|
|
6384
|
+
const nextCenterSvg = rotateSvgPoint(localCenter, nextSceneRotationDeg)
|
|
6385
|
+
const nextViewport = {
|
|
6386
|
+
centerX: nextCenterSvg.x,
|
|
6387
|
+
centerY: nextCenterSvg.y,
|
|
6388
|
+
width: nextWidth,
|
|
6389
|
+
}
|
|
6390
|
+
const presentationCenterSvg = rotateSvgPoint(
|
|
6391
|
+
localCenter,
|
|
6392
|
+
presentationState.initialSceneRotationDeg,
|
|
6393
|
+
)
|
|
6394
|
+
|
|
6395
|
+
applyFloorplanViewportImperatively({
|
|
6396
|
+
centerX: presentationCenterSvg.x,
|
|
6397
|
+
centerY: presentationCenterSvg.y,
|
|
6398
|
+
width: nextWidth,
|
|
6399
|
+
})
|
|
6400
|
+
svg.style.transform = `rotate(${
|
|
6401
|
+
nextUserRotationDeg - presentationState.initialUserRotationDeg
|
|
6402
|
+
}deg)`
|
|
6403
|
+
latestFloorplanUserRotationDegRef.current = nextUserRotationDeg
|
|
6404
|
+
latestViewportRef.current = nextViewport
|
|
6405
|
+
presentationState.latestUserRotationDeg = nextUserRotationDeg
|
|
6406
|
+
presentationState.latestSceneRotationDeg = nextSceneRotationDeg
|
|
6407
|
+
presentationState.latestViewport = nextViewport
|
|
6408
|
+
},
|
|
6409
|
+
[applyFloorplanViewportImperatively, buildingPosition, buildingRotationDeg, buildingRotationY],
|
|
6410
|
+
)
|
|
6411
|
+
|
|
6412
|
+
const commitFloorplanNavigationSyncPresentation = useCallback(
|
|
6413
|
+
(_pose: NavigationSyncPose) => {
|
|
6414
|
+
const presentationState = floorplanNavigationSyncPresentationRef.current
|
|
6415
|
+
if (!presentationState) return
|
|
6416
|
+
|
|
6417
|
+
applyFloorplanViewportImperatively(presentationState.latestViewport)
|
|
6418
|
+
const scene = floorplanSceneRef.current
|
|
6419
|
+
if (scene) {
|
|
6420
|
+
if (presentationState.latestSceneRotationDeg === 0) {
|
|
6421
|
+
scene.removeAttribute('transform')
|
|
6422
|
+
} else {
|
|
6423
|
+
scene.setAttribute('transform', `rotate(${presentationState.latestSceneRotationDeg})`)
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
restoreFloorplanNavigationSyncPresentation(presentationState)
|
|
6427
|
+
floorplanNavigationSyncPresentationRef.current = null
|
|
6428
|
+
floorplanViewportInteractionInProgressRef.current = false
|
|
6429
|
+
floorplanImperativeViewBoxRef.current = null
|
|
6430
|
+
setFloorplanUserRotationDeg((current) =>
|
|
6431
|
+
current === presentationState.latestUserRotationDeg
|
|
6432
|
+
? current
|
|
6433
|
+
: presentationState.latestUserRotationDeg,
|
|
6434
|
+
)
|
|
6435
|
+
setViewport((current) =>
|
|
6436
|
+
floorplanViewportEquals(current, presentationState.latestViewport)
|
|
6437
|
+
? current
|
|
6438
|
+
: presentationState.latestViewport,
|
|
6439
|
+
)
|
|
6440
|
+
},
|
|
6441
|
+
[applyFloorplanViewportImperatively],
|
|
6442
|
+
)
|
|
6443
|
+
|
|
6444
|
+
applyFloorplanNavigationSyncPresentationRef.current = applyFloorplanNavigationSyncPresentation
|
|
6445
|
+
commitFloorplanNavigationSyncPresentationRef.current = commitFloorplanNavigationSyncPresentation
|
|
6446
|
+
if (!floorplanNavigationSyncSchedulerRef.current) {
|
|
6447
|
+
floorplanNavigationSyncSchedulerRef.current = createFloorplanNavigationSyncScheduler({
|
|
6448
|
+
applyPresentation: (pose: NavigationSyncPose) =>
|
|
6449
|
+
applyFloorplanNavigationSyncPresentationRef.current(pose),
|
|
6450
|
+
commit: (pose: NavigationSyncPose) =>
|
|
6451
|
+
commitFloorplanNavigationSyncPresentationRef.current(pose),
|
|
6452
|
+
})
|
|
6453
|
+
}
|
|
6454
|
+
const floorplanNavigationSyncScheduler = floorplanNavigationSyncSchedulerRef.current
|
|
6455
|
+
const discardFloorplanNavigationSyncPresentation = useCallback(() => {
|
|
6456
|
+
floorplanNavigationSyncScheduler.discard()
|
|
6457
|
+
const presentationState = floorplanNavigationSyncPresentationRef.current
|
|
6458
|
+
if (presentationState) {
|
|
6459
|
+
restoreFloorplanNavigationSyncPresentation(presentationState)
|
|
6460
|
+
floorplanNavigationSyncPresentationRef.current = null
|
|
6461
|
+
}
|
|
6462
|
+
floorplanViewportInteractionInProgressRef.current = false
|
|
6463
|
+
floorplanImperativeViewBoxRef.current = null
|
|
6464
|
+
}, [floorplanNavigationSyncScheduler])
|
|
6465
|
+
|
|
6553
6466
|
const stopFloorplanViewAnimation = useCallback(() => {
|
|
6554
6467
|
if (floorplanViewAnimationFrameRef.current !== null) {
|
|
6555
6468
|
window.cancelAnimationFrame(floorplanViewAnimationFrameRef.current)
|
|
@@ -6679,39 +6592,26 @@ export function FloorplanPanel({
|
|
|
6679
6592
|
|
|
6680
6593
|
const syncFloorplanViewportToNavigationPose = useCallback(
|
|
6681
6594
|
(pose: NavigationSyncPose) => {
|
|
6682
|
-
//
|
|
6683
|
-
// React state
|
|
6684
|
-
// doing it every camera-zoom frame for an invisible panel was a needless
|
|
6685
|
-
// per-frame stall. The catch-up effect below re-syncs on reopen.
|
|
6595
|
+
// The transient camera stream owns refs + imperative SVG presentation.
|
|
6596
|
+
// React state is committed only by the scheduler after the stream settles.
|
|
6686
6597
|
if (!isFloorplanOpenRef.current) {
|
|
6687
6598
|
return
|
|
6688
6599
|
}
|
|
6689
|
-
|
|
6600
|
+
const localNavigationInProgress =
|
|
6601
|
+
floorplanViewportInteractionInProgressRef.current &&
|
|
6602
|
+
floorplanNavigationSyncPresentationRef.current === null
|
|
6603
|
+
if (!canApplyFloorplanNavigationSync(localNavigationInProgress)) {
|
|
6690
6604
|
return
|
|
6691
6605
|
}
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
)
|
|
6697
|
-
const localCenter = worldToFloorplanLocalPoint(
|
|
6698
|
-
pose.target[0],
|
|
6699
|
-
pose.target[2],
|
|
6700
|
-
buildingPosition,
|
|
6701
|
-
buildingRotationY,
|
|
6702
|
-
)
|
|
6703
|
-
|
|
6704
|
-
applyFloorplanNavigationView(localCenter, nextUserRotationDeg, pose.viewWidth, {
|
|
6705
|
-
clampViewWidth: false,
|
|
6706
|
-
})
|
|
6707
|
-
},
|
|
6708
|
-
[applyFloorplanNavigationView, buildingPosition, buildingRotationY],
|
|
6709
|
-
)
|
|
6606
|
+
floorplanNavigationSyncScheduler.update(pose)
|
|
6607
|
+
},
|
|
6608
|
+
[floorplanNavigationSyncScheduler],
|
|
6609
|
+
)
|
|
6710
6610
|
|
|
6711
6611
|
useEffect(() => {
|
|
6712
6612
|
if (!isFloorplanOpen) return
|
|
6713
6613
|
|
|
6714
|
-
const pose =
|
|
6614
|
+
const pose = latestNavigationSyncPoseRef.current
|
|
6715
6615
|
if (!pose) {
|
|
6716
6616
|
return
|
|
6717
6617
|
}
|
|
@@ -6722,6 +6622,18 @@ export function FloorplanPanel({
|
|
|
6722
6622
|
}
|
|
6723
6623
|
}, [syncFloorplanViewportToNavigationPose, isFloorplanOpen])
|
|
6724
6624
|
|
|
6625
|
+
useEffect(() => {
|
|
6626
|
+
if (isFloorplanOpen) return
|
|
6627
|
+
discardFloorplanNavigationSyncPresentation()
|
|
6628
|
+
}, [discardFloorplanNavigationSyncPresentation, isFloorplanOpen])
|
|
6629
|
+
|
|
6630
|
+
useEffect(
|
|
6631
|
+
() => () => {
|
|
6632
|
+
discardFloorplanNavigationSyncPresentation()
|
|
6633
|
+
},
|
|
6634
|
+
[discardFloorplanNavigationSyncPresentation],
|
|
6635
|
+
)
|
|
6636
|
+
|
|
6725
6637
|
const cancelHiddenCompassAnimation = useCallback(() => {
|
|
6726
6638
|
if (hiddenCompassAnimationRef.current !== null) {
|
|
6727
6639
|
cancelAnimationFrame(hiddenCompassAnimationRef.current)
|
|
@@ -6729,11 +6641,9 @@ export function FloorplanPanel({
|
|
|
6729
6641
|
}
|
|
6730
6642
|
}, [])
|
|
6731
6643
|
|
|
6732
|
-
// Align-north while the panel is hidden publishes
|
|
6733
|
-
//
|
|
6734
|
-
//
|
|
6735
|
-
// Same time constant as the 2D view animation and the camera's effective
|
|
6736
|
-
// smoothTime, so all three stay visually in step.
|
|
6644
|
+
// Align-north while the panel is hidden publishes one 2D pose through the
|
|
6645
|
+
// generic camera bridge. Camera application suppresses its own pose stream,
|
|
6646
|
+
// so the needle animates itself with the same time constant.
|
|
6737
6647
|
const animateHiddenCompassNeedle = useCallback(
|
|
6738
6648
|
(targetDeg: number) => {
|
|
6739
6649
|
cancelHiddenCompassAnimation()
|
|
@@ -6752,9 +6662,7 @@ export function FloorplanPanel({
|
|
|
6752
6662
|
nextDeg = targetDeg
|
|
6753
6663
|
}
|
|
6754
6664
|
latestFloorplanUserRotationDegRef.current = nextDeg
|
|
6755
|
-
|
|
6756
|
-
compassNeedleRef.current.style.transform = `rotate(${nextDeg}deg)`
|
|
6757
|
-
}
|
|
6665
|
+
setFloorplanCompassRotation(compassNeedleRef.current, nextDeg)
|
|
6758
6666
|
if (nextDeg !== targetDeg) {
|
|
6759
6667
|
hiddenCompassAnimationRef.current = requestAnimationFrame(tick)
|
|
6760
6668
|
}
|
|
@@ -6764,10 +6672,10 @@ export function FloorplanPanel({
|
|
|
6764
6672
|
[cancelHiddenCompassAnimation],
|
|
6765
6673
|
)
|
|
6766
6674
|
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
const
|
|
6770
|
-
if (
|
|
6675
|
+
const receiveFloorplanNavigationPose = useCallback(
|
|
6676
|
+
(pose: NavigationSyncPose) => {
|
|
6677
|
+
const previousPose = latestNavigationSyncPoseRef.current
|
|
6678
|
+
if (previousPose?.source === pose.source && previousPose.revision === pose.revision) {
|
|
6771
6679
|
return
|
|
6772
6680
|
}
|
|
6773
6681
|
|
|
@@ -6785,9 +6693,7 @@ export function FloorplanPanel({
|
|
|
6785
6693
|
// owns the needle, so any local animation yields to it.
|
|
6786
6694
|
cancelHiddenCompassAnimation()
|
|
6787
6695
|
latestFloorplanUserRotationDegRef.current = nextDeg
|
|
6788
|
-
|
|
6789
|
-
compassNeedleRef.current.style.transform = `rotate(${nextDeg}deg)`
|
|
6790
|
-
}
|
|
6696
|
+
setFloorplanCompassRotation(compassNeedleRef.current, nextDeg)
|
|
6791
6697
|
} else {
|
|
6792
6698
|
animateHiddenCompassNeedle(nextDeg)
|
|
6793
6699
|
}
|
|
@@ -6797,24 +6703,45 @@ export function FloorplanPanel({
|
|
|
6797
6703
|
if (pose.source === '3d') {
|
|
6798
6704
|
syncFloorplanViewportToNavigationPose(pose)
|
|
6799
6705
|
}
|
|
6800
|
-
}
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
cancelHiddenCompassAnimation
|
|
6706
|
+
},
|
|
6707
|
+
[
|
|
6708
|
+
animateHiddenCompassNeedle,
|
|
6709
|
+
cancelHiddenCompassAnimation,
|
|
6710
|
+
syncFloorplanViewportToNavigationPose,
|
|
6711
|
+
],
|
|
6712
|
+
)
|
|
6713
|
+
|
|
6714
|
+
useEffect(
|
|
6715
|
+
() => subscribeFloorplanCameraNavigation(receiveFloorplanNavigationPose),
|
|
6716
|
+
[receiveFloorplanNavigationPose],
|
|
6717
|
+
)
|
|
6718
|
+
|
|
6719
|
+
useEffect(() => {
|
|
6720
|
+
const receiveStoredNavigationPose = (pose: NavigationSyncPose | null) => {
|
|
6721
|
+
if (pose?.source === '2d') {
|
|
6722
|
+
receiveFloorplanNavigationPose(pose)
|
|
6723
|
+
}
|
|
6804
6724
|
}
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6725
|
+
return subscribeNavigationSyncPose(receiveStoredNavigationPose)
|
|
6726
|
+
}, [receiveFloorplanNavigationPose])
|
|
6727
|
+
|
|
6728
|
+
useEffect(
|
|
6729
|
+
() => () => {
|
|
6730
|
+
cancelHiddenCompassAnimation()
|
|
6731
|
+
},
|
|
6732
|
+
[cancelHiddenCompassAnimation],
|
|
6733
|
+
)
|
|
6810
6734
|
|
|
6811
6735
|
// When the panel is hidden the imperative path owns the compass needle.
|
|
6812
6736
|
// React re-renders can overwrite the needle's inline transform with stale
|
|
6813
6737
|
// state; this layout effect restores the authoritative ref value before
|
|
6814
6738
|
// the browser paints so the needle never visibly snaps to a stale angle.
|
|
6815
6739
|
useLayoutEffect(() => {
|
|
6816
|
-
if (!isFloorplanOpen
|
|
6817
|
-
|
|
6740
|
+
if (!isFloorplanOpen) {
|
|
6741
|
+
setFloorplanCompassRotation(
|
|
6742
|
+
compassNeedleRef.current,
|
|
6743
|
+
latestFloorplanUserRotationDegRef.current,
|
|
6744
|
+
)
|
|
6818
6745
|
}
|
|
6819
6746
|
})
|
|
6820
6747
|
|
|
@@ -6865,35 +6792,6 @@ export function FloorplanPanel({
|
|
|
6865
6792
|
}
|
|
6866
6793
|
}, [])
|
|
6867
6794
|
|
|
6868
|
-
// Reset to auto-fit each time the 2D editor re-opens. The panel stays
|
|
6869
|
-
// mounted across close/open (hidden via `display: none`), so without
|
|
6870
|
-
// this the user's last pan/zoom — and any stale `measuredSceneBBox`
|
|
6871
|
-
// captured before they closed it — would survive and the reopened
|
|
6872
|
-
// editor would show the same off-screen viewport instead of fitting
|
|
6873
|
-
// to the current scene.
|
|
6874
|
-
useEffect(() => {
|
|
6875
|
-
if (!isFloorplanOpen) {
|
|
6876
|
-
stopFloorplanViewAnimation()
|
|
6877
|
-
floorplanSpacePanPressedRef.current = false
|
|
6878
|
-
panStateRef.current = null
|
|
6879
|
-
floorplanRotationStateRef.current = null
|
|
6880
|
-
setIsSpacePanPressed(false)
|
|
6881
|
-
setIsPanning(false)
|
|
6882
|
-
setIsRotatingFloorplan(false)
|
|
6883
|
-
return
|
|
6884
|
-
}
|
|
6885
|
-
setMeasuredSceneBBox(null)
|
|
6886
|
-
|
|
6887
|
-
if (!latestNavigationSyncPoseRef.current) {
|
|
6888
|
-
stopFloorplanViewAnimation()
|
|
6889
|
-
hasUserAdjustedViewportRef.current = false
|
|
6890
|
-
latestFloorplanUserRotationDegRef.current = 0
|
|
6891
|
-
latestViewportRef.current = null
|
|
6892
|
-
setFloorplanUserRotationDeg(0)
|
|
6893
|
-
setViewport(null)
|
|
6894
|
-
}
|
|
6895
|
-
}, [isFloorplanOpen, stopFloorplanViewAnimation])
|
|
6896
|
-
|
|
6897
6795
|
useEffect(() => {
|
|
6898
6796
|
const levelChanged = previousLevelIdRef.current !== (levelId ?? null)
|
|
6899
6797
|
|
|
@@ -6952,19 +6850,32 @@ export function FloorplanPanel({
|
|
|
6952
6850
|
height,
|
|
6953
6851
|
}
|
|
6954
6852
|
}, [fittedViewport, svgAspectRatio, viewport])
|
|
6853
|
+
const presentationViewBox = resolveFloorplanPresentationViewBox(
|
|
6854
|
+
viewBox,
|
|
6855
|
+
floorplanImperativeViewBoxRef.current,
|
|
6856
|
+
floorplanViewportInteractionInProgressRef.current,
|
|
6857
|
+
)
|
|
6858
|
+
const presentationPaintViewBox = getFloorplanRotationOverscanViewBox(presentationViewBox)
|
|
6955
6859
|
const floorplanWorldUnitsPerPixel = useMemo(() => {
|
|
6956
6860
|
const widthUnitsPerPixel = viewBox.width / Math.max(surfaceSize.width, 1)
|
|
6957
6861
|
const heightUnitsPerPixel = viewBox.height / Math.max(surfaceSize.height, 1)
|
|
6958
6862
|
|
|
6959
6863
|
return (widthUnitsPerPixel + heightUnitsPerPixel) / 2
|
|
6960
6864
|
}, [surfaceSize.height, surfaceSize.width, viewBox.height, viewBox.width])
|
|
6961
|
-
const
|
|
6962
|
-
|
|
6963
|
-
|
|
6865
|
+
const getLiveFloorplanWorldUnitsPerPixel = useCallback(() => {
|
|
6866
|
+
const width = latestViewportRef.current?.width ?? viewBox.width
|
|
6867
|
+
const height = width / svgAspectRatio
|
|
6868
|
+
const widthUnitsPerPixel = width / Math.max(surfaceSize.width, 1)
|
|
6869
|
+
const heightUnitsPerPixel = height / Math.max(surfaceSize.height, 1)
|
|
6870
|
+
return (widthUnitsPerPixel + heightUnitsPerPixel) / 2
|
|
6871
|
+
}, [surfaceSize.height, surfaceSize.width, svgAspectRatio, viewBox.width])
|
|
6872
|
+
const getFloorplanWallHitTolerance = useCallback(
|
|
6873
|
+
() => getLiveFloorplanWorldUnitsPerPixel() * (FLOORPLAN_WALL_HIT_STROKE_WIDTH / 2),
|
|
6874
|
+
[getLiveFloorplanWorldUnitsPerPixel],
|
|
6964
6875
|
)
|
|
6965
|
-
const
|
|
6966
|
-
() =>
|
|
6967
|
-
[
|
|
6876
|
+
const getFloorplanOpeningHitTolerance = useCallback(
|
|
6877
|
+
() => getLiveFloorplanWorldUnitsPerPixel() * (FLOORPLAN_OPENING_HIT_STROKE_WIDTH / 2),
|
|
6878
|
+
[getLiveFloorplanWorldUnitsPerPixel],
|
|
6968
6879
|
)
|
|
6969
6880
|
const wallSelectionHatchSpacing = useMemo(
|
|
6970
6881
|
() => Math.max(floorplanWorldUnitsPerPixel * 12, 0.0001),
|
|
@@ -7220,7 +7131,11 @@ export function FloorplanPanel({
|
|
|
7220
7131
|
[surfaceSize.width, viewBox.width],
|
|
7221
7132
|
)
|
|
7222
7133
|
const gridBounds = useMemo(
|
|
7223
|
-
() =>
|
|
7134
|
+
() =>
|
|
7135
|
+
getRotatedViewBoxBounds(
|
|
7136
|
+
getFloorplanRotationOverscanViewBox(viewBox),
|
|
7137
|
+
floorplanSceneRotationDeg,
|
|
7138
|
+
),
|
|
7224
7139
|
[floorplanSceneRotationDeg, viewBox],
|
|
7225
7140
|
)
|
|
7226
7141
|
|
|
@@ -7249,7 +7164,9 @@ export function FloorplanPanel({
|
|
|
7249
7164
|
),
|
|
7250
7165
|
[gridBounds, gridSteps.majorStep],
|
|
7251
7166
|
)
|
|
7252
|
-
const
|
|
7167
|
+
const liveFloorplanUnitsPerPixel = viewBox.width / Math.max(surfaceSize.width, 1)
|
|
7168
|
+
const floorplanUnitsPerPixel = floorplanRenderUnitsPerPixel ?? liveFloorplanUnitsPerPixel
|
|
7169
|
+
latestFloorplanRenderUnitsPerPixelRef.current = floorplanUnitsPerPixel
|
|
7253
7170
|
|
|
7254
7171
|
useEffect(() => {
|
|
7255
7172
|
setReferenceScaleUnit(unit === 'imperial' ? 'feet' : 'meters')
|
|
@@ -7435,120 +7352,6 @@ export function FloorplanPanel({
|
|
|
7435
7352
|
[getSvgPointFromClientPoint, buildingRotationY],
|
|
7436
7353
|
)
|
|
7437
7354
|
|
|
7438
|
-
const previewElevatorResize = useCallback(
|
|
7439
|
-
(dragState: ElevatorResizeDragState, planPoint: WallPlanPoint) => {
|
|
7440
|
-
const localDeltaX = planPoint[0] - dragState.center.x
|
|
7441
|
-
const localDeltaY = planPoint[1] - dragState.center.y
|
|
7442
|
-
const [localX, localY] = rotatePlanVector(localDeltaX, localDeltaY, -dragState.rotation)
|
|
7443
|
-
const axis = getElevatorResizeAxis(dragState.handle)
|
|
7444
|
-
const sign = getElevatorResizeSign(dragState.handle)
|
|
7445
|
-
const localDistance = sign * (axis === 'width' ? localX : localY)
|
|
7446
|
-
const nextOuterSize = Math.max(0.1, localDistance) * 2
|
|
7447
|
-
|
|
7448
|
-
if (axis === 'width') {
|
|
7449
|
-
const nextShaftWidth = roundPlanMeters(
|
|
7450
|
-
Math.max(0.8, nextOuterSize - dragState.shaftWallThickness * 2),
|
|
7451
|
-
)
|
|
7452
|
-
const nextCabWidth = nextShaftWidth
|
|
7453
|
-
useLiveNodeOverrides
|
|
7454
|
-
.getState()
|
|
7455
|
-
.set(dragState.elevatorId, { shaftWidth: nextShaftWidth, width: nextCabWidth })
|
|
7456
|
-
setCursorPoint(planPoint)
|
|
7457
|
-
return { shaftWidth: nextShaftWidth, width: nextCabWidth } satisfies Partial<ElevatorNode>
|
|
7458
|
-
}
|
|
7459
|
-
|
|
7460
|
-
const nextShaftDepth = roundPlanMeters(
|
|
7461
|
-
Math.max(0.8, nextOuterSize - dragState.shaftWallThickness * 2),
|
|
7462
|
-
)
|
|
7463
|
-
const nextCabDepth = nextShaftDepth
|
|
7464
|
-
useLiveNodeOverrides
|
|
7465
|
-
.getState()
|
|
7466
|
-
.set(dragState.elevatorId, { depth: nextCabDepth, shaftDepth: nextShaftDepth })
|
|
7467
|
-
setCursorPoint(planPoint)
|
|
7468
|
-
return { depth: nextCabDepth, shaftDepth: nextShaftDepth } satisfies Partial<ElevatorNode>
|
|
7469
|
-
},
|
|
7470
|
-
[setCursorPoint],
|
|
7471
|
-
)
|
|
7472
|
-
|
|
7473
|
-
const handleElevatorResizePointerDown = useCallback(
|
|
7474
|
-
(
|
|
7475
|
-
entry: FloorplanElevatorEntry,
|
|
7476
|
-
handle: ElevatorResizeHandle,
|
|
7477
|
-
event: ReactPointerEvent<SVGCircleElement>,
|
|
7478
|
-
) => {
|
|
7479
|
-
if (event.button !== 0 || mode !== 'select') {
|
|
7480
|
-
return
|
|
7481
|
-
}
|
|
7482
|
-
|
|
7483
|
-
event.preventDefault()
|
|
7484
|
-
event.stopPropagation()
|
|
7485
|
-
event.currentTarget.setPointerCapture(event.pointerId)
|
|
7486
|
-
setHoveredElevatorId(null)
|
|
7487
|
-
setSelection({ selectedIds: [entry.elevator.id] })
|
|
7488
|
-
|
|
7489
|
-
setElevatorResizeDragState({
|
|
7490
|
-
center: entry.center,
|
|
7491
|
-
elevatorId: entry.elevator.id,
|
|
7492
|
-
handle,
|
|
7493
|
-
pointerId: event.pointerId,
|
|
7494
|
-
rotation: entry.rotation,
|
|
7495
|
-
shaftWallThickness: entry.shaftWallThickness,
|
|
7496
|
-
})
|
|
7497
|
-
},
|
|
7498
|
-
[mode, setSelection],
|
|
7499
|
-
)
|
|
7500
|
-
|
|
7501
|
-
const handleElevatorResizePointerMove = useCallback(
|
|
7502
|
-
(event: ReactPointerEvent<SVGCircleElement>) => {
|
|
7503
|
-
const dragState = elevatorResizeDragState
|
|
7504
|
-
if (!dragState || dragState.pointerId !== event.pointerId) {
|
|
7505
|
-
return
|
|
7506
|
-
}
|
|
7507
|
-
|
|
7508
|
-
const planPoint = getPlanPointFromClientPoint(event.clientX, event.clientY)
|
|
7509
|
-
if (!planPoint) {
|
|
7510
|
-
return
|
|
7511
|
-
}
|
|
7512
|
-
|
|
7513
|
-
event.preventDefault()
|
|
7514
|
-
event.stopPropagation()
|
|
7515
|
-
previewElevatorResize(dragState, planPoint)
|
|
7516
|
-
},
|
|
7517
|
-
[elevatorResizeDragState, getPlanPointFromClientPoint, previewElevatorResize],
|
|
7518
|
-
)
|
|
7519
|
-
|
|
7520
|
-
const handleElevatorResizePointerUp = useCallback(
|
|
7521
|
-
(event: ReactPointerEvent<SVGCircleElement>) => {
|
|
7522
|
-
const dragState = elevatorResizeDragState
|
|
7523
|
-
if (!dragState || dragState.pointerId !== event.pointerId) {
|
|
7524
|
-
return
|
|
7525
|
-
}
|
|
7526
|
-
|
|
7527
|
-
const planPoint = getPlanPointFromClientPoint(event.clientX, event.clientY)
|
|
7528
|
-
const updates = planPoint ? previewElevatorResize(dragState, planPoint) : {}
|
|
7529
|
-
|
|
7530
|
-
event.preventDefault()
|
|
7531
|
-
event.stopPropagation()
|
|
7532
|
-
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
7533
|
-
event.currentTarget.releasePointerCapture(event.pointerId)
|
|
7534
|
-
}
|
|
7535
|
-
|
|
7536
|
-
useLiveNodeOverrides.getState().clear(dragState.elevatorId)
|
|
7537
|
-
if (Object.keys(updates).length > 0) {
|
|
7538
|
-
updateNode(dragState.elevatorId as AnyNodeId, updates)
|
|
7539
|
-
}
|
|
7540
|
-
setElevatorResizeDragState(null)
|
|
7541
|
-
setCursorPoint(null)
|
|
7542
|
-
},
|
|
7543
|
-
[
|
|
7544
|
-
elevatorResizeDragState,
|
|
7545
|
-
getPlanPointFromClientPoint,
|
|
7546
|
-
previewElevatorResize,
|
|
7547
|
-
updateNode,
|
|
7548
|
-
setCursorPoint,
|
|
7549
|
-
],
|
|
7550
|
-
)
|
|
7551
|
-
|
|
7552
7355
|
useEffect(() => {
|
|
7553
7356
|
siteBoundaryDraftRef.current = siteBoundaryDraft
|
|
7554
7357
|
}, [siteBoundaryDraft])
|
|
@@ -7777,8 +7580,77 @@ export function FloorplanPanel({
|
|
|
7777
7580
|
[beginPanelInteraction, panelRect],
|
|
7778
7581
|
)
|
|
7779
7582
|
|
|
7583
|
+
const applyFloorplanRotationImperatively = useCallback(
|
|
7584
|
+
(rotationState: FloorplanRotationState, nextUserRotationDeg: number) => {
|
|
7585
|
+
const currentViewport = latestViewportRef.current ?? rotationState.latestViewport
|
|
7586
|
+
const nextSceneRotationDeg =
|
|
7587
|
+
FLOORPLAN_VIEW_ROTATION_DEG + nextUserRotationDeg - buildingRotationDeg
|
|
7588
|
+
const nextCenterSvg = rotateSvgPoint(rotationState.viewportCenterLocal, nextSceneRotationDeg)
|
|
7589
|
+
const nextViewport = {
|
|
7590
|
+
centerX: nextCenterSvg.x,
|
|
7591
|
+
centerY: nextCenterSvg.y,
|
|
7592
|
+
width: currentViewport.width,
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7595
|
+
hasUserAdjustedViewportRef.current = true
|
|
7596
|
+
latestFloorplanUserRotationDegRef.current = nextUserRotationDeg
|
|
7597
|
+
latestViewportRef.current = nextViewport
|
|
7598
|
+
setFloorplanCompassRotation(compassNeedleRef.current, nextUserRotationDeg)
|
|
7599
|
+
// Transform the already-painted SVG as one compositor layer. Mutating the
|
|
7600
|
+
// scene rotation/viewBox here forces the heavy vector plan to rerasterize.
|
|
7601
|
+
rotationState.svg.style.transform = `rotate(${nextUserRotationDeg - rotationState.initialUserRotationDeg}deg)`
|
|
7602
|
+
|
|
7603
|
+
rotationState.latestUserRotationDeg = nextUserRotationDeg
|
|
7604
|
+
rotationState.latestViewport = nextViewport
|
|
7605
|
+
},
|
|
7606
|
+
[buildingRotationDeg],
|
|
7607
|
+
)
|
|
7608
|
+
|
|
7609
|
+
const commitFloorplanZoom = useCallback(() => {
|
|
7610
|
+
if (floorplanZoomCommitTimerRef.current !== null) {
|
|
7611
|
+
window.clearTimeout(floorplanZoomCommitTimerRef.current)
|
|
7612
|
+
floorplanZoomCommitTimerRef.current = null
|
|
7613
|
+
}
|
|
7614
|
+
const nextViewport = latestViewportRef.current
|
|
7615
|
+
const pendingPose = floorplanZoomPoseRef.current
|
|
7616
|
+
floorplanZoomPoseRef.current = null
|
|
7617
|
+
floorplanViewportInteractionInProgressRef.current = false
|
|
7618
|
+
floorplanImperativeViewBoxRef.current = null
|
|
7619
|
+
if (!nextViewport) return
|
|
7620
|
+
setFloorplanRenderUnitsPerPixel(
|
|
7621
|
+
(current) => current ?? latestFloorplanRenderUnitsPerPixelRef.current,
|
|
7622
|
+
)
|
|
7623
|
+
setViewport((current) =>
|
|
7624
|
+
floorplanViewportEquals(current, nextViewport) ? current : nextViewport,
|
|
7625
|
+
)
|
|
7626
|
+
if (floorplanRenderScaleCommitTimerRef.current !== null) {
|
|
7627
|
+
window.clearTimeout(floorplanRenderScaleCommitTimerRef.current)
|
|
7628
|
+
}
|
|
7629
|
+
floorplanRenderScaleCommitTimerRef.current = window.setTimeout(() => {
|
|
7630
|
+
floorplanRenderScaleCommitTimerRef.current = null
|
|
7631
|
+
setFloorplanRenderUnitsPerPixel(null)
|
|
7632
|
+
}, 350)
|
|
7633
|
+
if (pendingPose) {
|
|
7634
|
+
publishFloorplanNavigationPose(
|
|
7635
|
+
pendingPose.localCenter,
|
|
7636
|
+
pendingPose.userRotationDeg,
|
|
7637
|
+
pendingPose.viewWidth,
|
|
7638
|
+
)
|
|
7639
|
+
}
|
|
7640
|
+
}, [publishFloorplanNavigationPose])
|
|
7641
|
+
|
|
7642
|
+
const scheduleFloorplanZoomCommit = useCallback(() => {
|
|
7643
|
+
if (floorplanZoomCommitTimerRef.current !== null) {
|
|
7644
|
+
window.clearTimeout(floorplanZoomCommitTimerRef.current)
|
|
7645
|
+
}
|
|
7646
|
+
floorplanZoomCommitTimerRef.current = window.setTimeout(commitFloorplanZoom, 300)
|
|
7647
|
+
}, [commitFloorplanZoom])
|
|
7648
|
+
|
|
7780
7649
|
const zoomViewportAtClientPoint = useCallback(
|
|
7781
7650
|
(clientX: number, clientY: number, widthFactor: number) => {
|
|
7651
|
+
if (!canZoomFloorplanDuringNavigation(floorplanRotationStateRef.current !== null)) {
|
|
7652
|
+
return
|
|
7653
|
+
}
|
|
7782
7654
|
if (!Number.isFinite(widthFactor) || widthFactor <= 0) {
|
|
7783
7655
|
return
|
|
7784
7656
|
}
|
|
@@ -7792,14 +7664,27 @@ export function FloorplanPanel({
|
|
|
7792
7664
|
if (!localPoint) {
|
|
7793
7665
|
return
|
|
7794
7666
|
}
|
|
7795
|
-
const
|
|
7667
|
+
const currentSceneRotationDeg =
|
|
7668
|
+
FLOORPLAN_VIEW_ROTATION_DEG +
|
|
7669
|
+
latestFloorplanUserRotationDegRef.current -
|
|
7670
|
+
buildingRotationDeg
|
|
7671
|
+
const svgPoint = rotateSvgPoint(localPoint, currentSceneRotationDeg)
|
|
7796
7672
|
|
|
7797
|
-
const currentViewport =
|
|
7798
|
-
|
|
7673
|
+
const currentViewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
7674
|
+
if (!currentViewport) {
|
|
7675
|
+
return
|
|
7676
|
+
}
|
|
7677
|
+
const currentViewBox = {
|
|
7678
|
+
minX: currentViewport.centerX - currentViewport.width / 2,
|
|
7679
|
+
minY: currentViewport.centerY - currentViewport.width / svgAspectRatio / 2,
|
|
7680
|
+
width: currentViewport.width,
|
|
7681
|
+
height: currentViewport.width / svgAspectRatio,
|
|
7682
|
+
}
|
|
7683
|
+
const fitted = latestFittedViewportRef.current
|
|
7799
7684
|
const nextWidth = resolveFloorplanViewWidth(
|
|
7800
7685
|
currentViewport.width * widthFactor,
|
|
7801
7686
|
currentViewport.width,
|
|
7802
|
-
|
|
7687
|
+
fitted,
|
|
7803
7688
|
true,
|
|
7804
7689
|
)
|
|
7805
7690
|
const nextHeight = nextWidth / svgAspectRatio
|
|
@@ -7812,34 +7697,149 @@ export function FloorplanPanel({
|
|
|
7812
7697
|
x: nextMinX + nextWidth / 2,
|
|
7813
7698
|
y: nextMinY + nextHeight / 2,
|
|
7814
7699
|
}
|
|
7815
|
-
const localCenter = rotateSvgPoint(nextCenterSvg, -
|
|
7700
|
+
const localCenter = rotateSvgPoint(nextCenterSvg, -currentSceneRotationDeg)
|
|
7701
|
+
const nextViewport = {
|
|
7702
|
+
centerX: nextCenterSvg.x,
|
|
7703
|
+
centerY: nextCenterSvg.y,
|
|
7704
|
+
width: nextWidth,
|
|
7705
|
+
}
|
|
7816
7706
|
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7707
|
+
stopFloorplanViewAnimation()
|
|
7708
|
+
if (floorplanRenderScaleCommitTimerRef.current !== null) {
|
|
7709
|
+
window.clearTimeout(floorplanRenderScaleCommitTimerRef.current)
|
|
7710
|
+
floorplanRenderScaleCommitTimerRef.current = null
|
|
7711
|
+
}
|
|
7712
|
+
floorplanViewportInteractionInProgressRef.current = true
|
|
7713
|
+
applyFloorplanViewportImperatively(nextViewport)
|
|
7714
|
+
scheduleFloorplanZoomCommit()
|
|
7715
|
+
const userRotationDeg = latestFloorplanUserRotationDegRef.current
|
|
7716
|
+
floorplanZoomPoseRef.current = { localCenter, userRotationDeg, viewWidth: nextWidth }
|
|
7717
|
+
if (useEditor.getState().viewMode === 'split') {
|
|
7718
|
+
publishFloorplanNavigationPose(localCenter, userRotationDeg, nextWidth)
|
|
7719
|
+
}
|
|
7827
7720
|
},
|
|
7828
7721
|
[
|
|
7829
|
-
|
|
7830
|
-
|
|
7722
|
+
applyFloorplanViewportImperatively,
|
|
7723
|
+
buildingRotationDeg,
|
|
7831
7724
|
getSvgPointFromClientPoint,
|
|
7832
7725
|
publishFloorplanNavigationPose,
|
|
7833
|
-
|
|
7726
|
+
scheduleFloorplanZoomCommit,
|
|
7727
|
+
stopFloorplanViewAnimation,
|
|
7834
7728
|
svgAspectRatio,
|
|
7835
|
-
viewBox,
|
|
7836
|
-
viewport,
|
|
7837
7729
|
],
|
|
7838
7730
|
)
|
|
7839
7731
|
|
|
7732
|
+
useEffect(
|
|
7733
|
+
() => () => {
|
|
7734
|
+
if (floorplanZoomCommitTimerRef.current !== null) {
|
|
7735
|
+
window.clearTimeout(floorplanZoomCommitTimerRef.current)
|
|
7736
|
+
}
|
|
7737
|
+
if (floorplanRenderScaleCommitTimerRef.current !== null) {
|
|
7738
|
+
window.clearTimeout(floorplanRenderScaleCommitTimerRef.current)
|
|
7739
|
+
}
|
|
7740
|
+
floorplanViewportInteractionInProgressRef.current = false
|
|
7741
|
+
floorplanImperativeViewBoxRef.current = null
|
|
7742
|
+
},
|
|
7743
|
+
[],
|
|
7744
|
+
)
|
|
7745
|
+
|
|
7746
|
+
const commitFloorplanPan = useCallback(() => {
|
|
7747
|
+
const nextViewport = latestViewportRef.current
|
|
7748
|
+
const pendingPose = floorplanPanPoseRef.current
|
|
7749
|
+
floorplanPanPoseRef.current = null
|
|
7750
|
+
floorplanViewportInteractionInProgressRef.current = false
|
|
7751
|
+
floorplanImperativeViewBoxRef.current = null
|
|
7752
|
+
if (nextViewport) {
|
|
7753
|
+
setViewport((current) =>
|
|
7754
|
+
floorplanViewportEquals(current, nextViewport) ? current : nextViewport,
|
|
7755
|
+
)
|
|
7756
|
+
}
|
|
7757
|
+
if (pendingPose) {
|
|
7758
|
+
publishFloorplanNavigationPose(
|
|
7759
|
+
pendingPose.localCenter,
|
|
7760
|
+
pendingPose.userRotationDeg,
|
|
7761
|
+
pendingPose.viewWidth,
|
|
7762
|
+
)
|
|
7763
|
+
}
|
|
7764
|
+
}, [publishFloorplanNavigationPose])
|
|
7765
|
+
|
|
7766
|
+
const commitFloorplanRotation = useCallback(
|
|
7767
|
+
(rotationState: FloorplanRotationState) => {
|
|
7768
|
+
floorplanViewportInteractionInProgressRef.current = false
|
|
7769
|
+
floorplanImperativeViewBoxRef.current = null
|
|
7770
|
+
queueFloorplanRotationPresentationRestore(pendingFloorplanRotationRestoreRef, rotationState)
|
|
7771
|
+
setFloorplanUserRotationDeg((current) =>
|
|
7772
|
+
current === rotationState.latestUserRotationDeg
|
|
7773
|
+
? current
|
|
7774
|
+
: rotationState.latestUserRotationDeg,
|
|
7775
|
+
)
|
|
7776
|
+
setViewport((current) =>
|
|
7777
|
+
floorplanViewportEquals(current, rotationState.latestViewport)
|
|
7778
|
+
? current
|
|
7779
|
+
: rotationState.latestViewport,
|
|
7780
|
+
)
|
|
7781
|
+
publishFloorplanNavigationPose(
|
|
7782
|
+
rotationState.viewportCenterLocal,
|
|
7783
|
+
rotationState.latestUserRotationDeg,
|
|
7784
|
+
rotationState.latestViewport.width,
|
|
7785
|
+
)
|
|
7786
|
+
},
|
|
7787
|
+
[publishFloorplanNavigationPose],
|
|
7788
|
+
)
|
|
7789
|
+
|
|
7790
|
+
useLayoutEffect(() => {
|
|
7791
|
+
flushFloorplanRotationPresentationRestore(pendingFloorplanRotationRestoreRef)
|
|
7792
|
+
})
|
|
7793
|
+
|
|
7794
|
+
// Finalize imperative navigation when the floorplan closes so reopening
|
|
7795
|
+
// restores the last visible pose instead of stale React state.
|
|
7796
|
+
useEffect(() => {
|
|
7797
|
+
if (isFloorplanOpen) return
|
|
7798
|
+
stopFloorplanViewAnimation()
|
|
7799
|
+
const rotationState = floorplanRotationStateRef.current
|
|
7800
|
+
finalizeFloorplanNavigation({
|
|
7801
|
+
zoomPending:
|
|
7802
|
+
floorplanZoomCommitTimerRef.current !== null || floorplanZoomPoseRef.current !== null,
|
|
7803
|
+
panActive: panStateRef.current !== null,
|
|
7804
|
+
rotationState,
|
|
7805
|
+
commitZoom: commitFloorplanZoom,
|
|
7806
|
+
commitPan: commitFloorplanPan,
|
|
7807
|
+
commitRotation: commitFloorplanRotation,
|
|
7808
|
+
})
|
|
7809
|
+
floorplanSpacePanPressedRef.current = false
|
|
7810
|
+
panStateRef.current = null
|
|
7811
|
+
floorplanRotationStateRef.current = null
|
|
7812
|
+
floorplanViewportInteractionInProgressRef.current = false
|
|
7813
|
+
floorplanImperativeViewBoxRef.current = null
|
|
7814
|
+
setIsSpacePanPressed(false)
|
|
7815
|
+
setIsPanning(false)
|
|
7816
|
+
setIsRotatingFloorplan(false)
|
|
7817
|
+
}, [
|
|
7818
|
+
commitFloorplanPan,
|
|
7819
|
+
commitFloorplanRotation,
|
|
7820
|
+
commitFloorplanZoom,
|
|
7821
|
+
isFloorplanOpen,
|
|
7822
|
+
stopFloorplanViewAnimation,
|
|
7823
|
+
])
|
|
7824
|
+
|
|
7825
|
+
useEffect(() => {
|
|
7826
|
+
if (!isFloorplanOpen) return
|
|
7827
|
+
setMeasuredSceneBBox(null)
|
|
7828
|
+
|
|
7829
|
+
if (!latestNavigationSyncPoseRef.current) {
|
|
7830
|
+
stopFloorplanViewAnimation()
|
|
7831
|
+
hasUserAdjustedViewportRef.current = false
|
|
7832
|
+
latestFloorplanUserRotationDegRef.current = 0
|
|
7833
|
+
latestViewportRef.current = null
|
|
7834
|
+
setFloorplanUserRotationDeg(0)
|
|
7835
|
+
setViewport(null)
|
|
7836
|
+
}
|
|
7837
|
+
}, [isFloorplanOpen, stopFloorplanViewAnimation])
|
|
7838
|
+
|
|
7840
7839
|
const clearWallPlacementDraft = useCallback(() => {
|
|
7841
7840
|
setDraftStart(null)
|
|
7842
7841
|
setWallChainFirstVertex(null)
|
|
7842
|
+
wallConstructionOptionsRef.current = undefined
|
|
7843
7843
|
wallChainWallIdsRef.current = []
|
|
7844
7844
|
setDraftEnd(null)
|
|
7845
7845
|
useSegmentDraftChain.getState().clear('wall')
|
|
@@ -7857,6 +7857,7 @@ export function FloorplanPanel({
|
|
|
7857
7857
|
}, [])
|
|
7858
7858
|
const clearSlabPlacementDraft = useCallback(() => {
|
|
7859
7859
|
setSlabDraftPoints([])
|
|
7860
|
+
slabPlacementBaseRef.current = null
|
|
7860
7861
|
}, [])
|
|
7861
7862
|
const clearZonePlacementDraft = useCallback(() => {
|
|
7862
7863
|
setZoneDraftPoints([])
|
|
@@ -7865,13 +7866,11 @@ export function FloorplanPanel({
|
|
|
7865
7866
|
const clearWallEndpointDrag = useCallback(() => {
|
|
7866
7867
|
wallEndpointDragRef.current = null
|
|
7867
7868
|
setWallEndpointDraft(null)
|
|
7868
|
-
setHoveredEndpointId(null)
|
|
7869
7869
|
useWallSnapIndicator.getState().clear()
|
|
7870
7870
|
}, [])
|
|
7871
7871
|
const clearWallCurveDrag = useCallback(() => {
|
|
7872
7872
|
wallCurveDragRef.current = null
|
|
7873
7873
|
setWallCurveDraft(null)
|
|
7874
|
-
setHoveredWallCurveHandleId(null)
|
|
7875
7874
|
}, [])
|
|
7876
7875
|
const clearSiteBoundaryInteraction = useCallback(() => {
|
|
7877
7876
|
const draft = siteBoundaryDraftRef.current
|
|
@@ -7981,18 +7980,22 @@ export function FloorplanPanel({
|
|
|
7981
7980
|
// the tools' own `commitSlab/CeilingDrawing`) are called ONLY in 2D-only view
|
|
7982
7981
|
// so split / 3D keep their single-owner tool commit (no double-create).
|
|
7983
7982
|
const createSlabOnCurrentLevel = useCallback(
|
|
7984
|
-
(points: WallPlanPoint[]) => {
|
|
7983
|
+
(points: WallPlanPoint[], baseElevation: number | null) => {
|
|
7985
7984
|
if (!levelId) {
|
|
7986
7985
|
return null
|
|
7987
7986
|
}
|
|
7988
7987
|
const { createNode, nodes } = useScene.getState()
|
|
7989
7988
|
const slabCount = Object.values(nodes).filter((node) => node.type === 'slab').length
|
|
7990
7989
|
const defaults = useEditor.getState().toolDefaults.slab ?? {}
|
|
7991
|
-
const
|
|
7990
|
+
const authored = SlabNodeSchema.parse({
|
|
7992
7991
|
...defaults,
|
|
7993
7992
|
name: `Slab ${slabCount + 1}`,
|
|
7994
7993
|
polygon: points.map(([x, z]) => [x, z] as [number, number]),
|
|
7995
7994
|
})
|
|
7995
|
+
const slab = {
|
|
7996
|
+
...authored,
|
|
7997
|
+
elevation: resolveSlabPlacementElevation(authored, baseElevation),
|
|
7998
|
+
}
|
|
7996
7999
|
createNode(slab, levelId)
|
|
7997
8000
|
sfxEmitter.emit('sfx:structure-build')
|
|
7998
8001
|
setSelection({ selectedIds: [slab.id] })
|
|
@@ -8079,14 +8082,6 @@ export function FloorplanPanel({
|
|
|
8079
8082
|
}
|
|
8080
8083
|
}, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh])
|
|
8081
8084
|
|
|
8082
|
-
useEffect(() => {
|
|
8083
|
-
if (!hasPendingItemMeshFootprints) {
|
|
8084
|
-
return
|
|
8085
|
-
}
|
|
8086
|
-
|
|
8087
|
-
scheduleMovingFloorplanNodeRefresh()
|
|
8088
|
-
}, [scheduleMovingFloorplanNodeRefresh])
|
|
8089
|
-
|
|
8090
8085
|
// Subscribe to the live-transforms store so rotation/position changes that
|
|
8091
8086
|
// *don't* go through pointer events still refresh the floorplan — e.g. R/T
|
|
8092
8087
|
// keyboard rotation during placement updates `useLiveTransforms` but emits
|
|
@@ -8240,9 +8235,17 @@ export function FloorplanPanel({
|
|
|
8240
8235
|
setShiftPressed(true)
|
|
8241
8236
|
}
|
|
8242
8237
|
|
|
8243
|
-
if (
|
|
8238
|
+
if (
|
|
8239
|
+
isStairBuildActive &&
|
|
8240
|
+
useEditor.getState().viewMode === '2d' &&
|
|
8241
|
+
(event.key === 'r' || event.key === 'R')
|
|
8242
|
+
) {
|
|
8244
8243
|
useStairBuildPreview.getState().rotateBy(Math.PI / 4)
|
|
8245
|
-
} else if (
|
|
8244
|
+
} else if (
|
|
8245
|
+
isStairBuildActive &&
|
|
8246
|
+
useEditor.getState().viewMode === '2d' &&
|
|
8247
|
+
(event.key === 't' || event.key === 'T')
|
|
8248
|
+
) {
|
|
8246
8249
|
useStairBuildPreview.getState().rotateBy(-Math.PI / 4)
|
|
8247
8250
|
}
|
|
8248
8251
|
|
|
@@ -8786,8 +8789,13 @@ export function FloorplanPanel({
|
|
|
8786
8789
|
event.preventDefault()
|
|
8787
8790
|
event.stopPropagation()
|
|
8788
8791
|
|
|
8792
|
+
floorplanNavigationSyncScheduler.flush()
|
|
8793
|
+
if (floorplanZoomCommitTimerRef.current !== null) commitFloorplanZoom()
|
|
8794
|
+
stopFloorplanViewAnimation()
|
|
8789
8795
|
floorplanNavigationClickSuppressedRef.current = true
|
|
8790
|
-
const currentViewport =
|
|
8796
|
+
const currentViewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
8797
|
+
if (!currentViewport) return
|
|
8798
|
+
floorplanViewportInteractionInProgressRef.current = true
|
|
8791
8799
|
panStateRef.current = {
|
|
8792
8800
|
pointerId: event.pointerId,
|
|
8793
8801
|
clientX: event.clientX,
|
|
@@ -8812,17 +8820,36 @@ export function FloorplanPanel({
|
|
|
8812
8820
|
event.preventDefault()
|
|
8813
8821
|
event.stopPropagation()
|
|
8814
8822
|
|
|
8815
|
-
|
|
8823
|
+
floorplanNavigationSyncScheduler.flush()
|
|
8824
|
+
if (floorplanZoomCommitTimerRef.current !== null) commitFloorplanZoom()
|
|
8825
|
+
stopFloorplanViewAnimation()
|
|
8826
|
+
const currentViewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
8827
|
+
const svg = svgRef.current
|
|
8828
|
+
if (!(currentViewport && svg)) return
|
|
8829
|
+
const currentUserRotationDeg = latestFloorplanUserRotationDegRef.current
|
|
8830
|
+
const currentSceneRotationDeg =
|
|
8831
|
+
FLOORPLAN_VIEW_ROTATION_DEG + currentUserRotationDeg - buildingRotationDeg
|
|
8816
8832
|
const viewportCenterLocal = rotateSvgPoint(
|
|
8817
8833
|
{ x: currentViewport.centerX, y: currentViewport.centerY },
|
|
8818
|
-
-
|
|
8834
|
+
-currentSceneRotationDeg,
|
|
8819
8835
|
)
|
|
8820
|
-
|
|
8836
|
+
const svgStyle = {
|
|
8837
|
+
transform: svg.style.transform,
|
|
8838
|
+
transformOrigin: svg.style.transformOrigin,
|
|
8839
|
+
willChange: svg.style.willChange,
|
|
8840
|
+
}
|
|
8841
|
+
floorplanViewportInteractionInProgressRef.current = true
|
|
8842
|
+
svg.style.transformOrigin = 'center'
|
|
8843
|
+
svg.style.willChange = 'transform'
|
|
8821
8844
|
floorplanRotationStateRef.current = {
|
|
8822
8845
|
pointerId: event.pointerId,
|
|
8823
8846
|
startClientX: event.clientX,
|
|
8824
|
-
initialUserRotationDeg:
|
|
8847
|
+
initialUserRotationDeg: currentUserRotationDeg,
|
|
8825
8848
|
viewportCenterLocal,
|
|
8849
|
+
svg,
|
|
8850
|
+
svgStyle,
|
|
8851
|
+
latestUserRotationDeg: currentUserRotationDeg,
|
|
8852
|
+
latestViewport: currentViewport,
|
|
8826
8853
|
}
|
|
8827
8854
|
setIsRotatingFloorplan(true)
|
|
8828
8855
|
setCursorPoint(null)
|
|
@@ -8831,12 +8858,12 @@ export function FloorplanPanel({
|
|
|
8831
8858
|
event.currentTarget.setPointerCapture(event.pointerId)
|
|
8832
8859
|
},
|
|
8833
8860
|
[
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
viewport,
|
|
8861
|
+
commitFloorplanZoom,
|
|
8862
|
+
buildingRotationDeg,
|
|
8863
|
+
floorplanNavigationSyncScheduler,
|
|
8838
8864
|
setFloorplanCursorPosition,
|
|
8839
8865
|
setCursorPoint,
|
|
8866
|
+
stopFloorplanViewAnimation,
|
|
8840
8867
|
],
|
|
8841
8868
|
)
|
|
8842
8869
|
|
|
@@ -8876,24 +8903,31 @@ export function FloorplanPanel({
|
|
|
8876
8903
|
[isScreenSelectionToolActive, setPreviewSelectedIds],
|
|
8877
8904
|
)
|
|
8878
8905
|
|
|
8879
|
-
const endFloorplanNavigation = useCallback(
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8906
|
+
const endFloorplanNavigation = useCallback(
|
|
8907
|
+
(event?: ReactPointerEvent<SVGSVGElement>) => {
|
|
8908
|
+
const wasPanning = panStateRef.current !== null
|
|
8909
|
+
const rotationState = floorplanRotationStateRef.current
|
|
8910
|
+
if (
|
|
8911
|
+
event &&
|
|
8912
|
+
(panStateRef.current || floorplanRotationStateRef.current) &&
|
|
8913
|
+
event.currentTarget.hasPointerCapture(event.pointerId)
|
|
8914
|
+
) {
|
|
8915
|
+
event.currentTarget.releasePointerCapture(event.pointerId)
|
|
8916
|
+
}
|
|
8887
8917
|
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8918
|
+
panStateRef.current = null
|
|
8919
|
+
floorplanRotationStateRef.current = null
|
|
8920
|
+
if (wasPanning) commitFloorplanPan()
|
|
8921
|
+
if (rotationState) commitFloorplanRotation(rotationState)
|
|
8922
|
+
setIsPanning(false)
|
|
8923
|
+
setIsRotatingFloorplan(false)
|
|
8892
8924
|
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8925
|
+
window.setTimeout(() => {
|
|
8926
|
+
floorplanNavigationClickSuppressedRef.current = false
|
|
8927
|
+
}, 0)
|
|
8928
|
+
},
|
|
8929
|
+
[commitFloorplanPan, commitFloorplanRotation],
|
|
8930
|
+
)
|
|
8897
8931
|
|
|
8898
8932
|
const hoveredWallIdRef = useRef<string | null>(null)
|
|
8899
8933
|
const hoveredCeilingIdRef = useRef<string | null>(null)
|
|
@@ -8929,10 +8963,20 @@ export function FloorplanPanel({
|
|
|
8929
8963
|
const worldX = buildingPosition[0] + planPoint[0] * cos + planPoint[1] * sin
|
|
8930
8964
|
const worldZ = buildingPosition[2] - planPoint[0] * sin + planPoint[1] * cos
|
|
8931
8965
|
|
|
8966
|
+
// On sculpted ground the grid Y is the terrain height under the plan point,
|
|
8967
|
+
// not the flat storey plane. There is no XZ correction to make here — the
|
|
8968
|
+
// plan view's ray is vertical by construction, so the skew the 3D path has
|
|
8969
|
+
// to march for cannot happen — but the Y still has to be the ground's, or a
|
|
8970
|
+
// wall drafted in 2D lands at the datum while the same wall drafted in 3D
|
|
8971
|
+
// lands on the hillside.
|
|
8972
|
+
const groundY = groundHeightAt(worldX, worldZ, floorplanGridWorldY)
|
|
8973
|
+
const worldY = groundY ?? floorplanGridWorldY
|
|
8974
|
+
const localY = groundY === null ? floorplanGridLocalY : groundY - buildingPosition[1]
|
|
8975
|
+
|
|
8932
8976
|
emitter.emit(`grid:${eventType}` as any, {
|
|
8933
8977
|
nativeEvent: nativeEvent.nativeEvent as any,
|
|
8934
|
-
position: [worldX,
|
|
8935
|
-
localPosition: [planPoint[0],
|
|
8978
|
+
position: [worldX, worldY, worldZ],
|
|
8979
|
+
localPosition: [planPoint[0], localY, planPoint[1]],
|
|
8936
8980
|
})
|
|
8937
8981
|
},
|
|
8938
8982
|
[buildingPosition, buildingRotationY, floorplanGridLocalY, floorplanGridWorldY],
|
|
@@ -9093,9 +9137,14 @@ export function FloorplanPanel({
|
|
|
9093
9137
|
(rotationState.startClientX - event.clientX) * FLOORPLAN_ROTATION_DEGREES_PER_PIXEL
|
|
9094
9138
|
const nextUserRotationDeg = rotationState.initialUserRotationDeg + angleDeltaDeg
|
|
9095
9139
|
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9140
|
+
applyFloorplanRotationImperatively(rotationState, nextUserRotationDeg)
|
|
9141
|
+
if (useEditor.getState().viewMode === 'split') {
|
|
9142
|
+
publishFloorplanNavigationPose(
|
|
9143
|
+
rotationState.viewportCenterLocal,
|
|
9144
|
+
nextUserRotationDeg,
|
|
9145
|
+
rotationState.latestViewport.width,
|
|
9146
|
+
)
|
|
9147
|
+
}
|
|
9099
9148
|
return
|
|
9100
9149
|
}
|
|
9101
9150
|
|
|
@@ -9105,8 +9154,11 @@ export function FloorplanPanel({
|
|
|
9105
9154
|
|
|
9106
9155
|
const deltaX = event.clientX - panStateRef.current.clientX
|
|
9107
9156
|
const deltaY = event.clientY - panStateRef.current.clientY
|
|
9108
|
-
const
|
|
9109
|
-
|
|
9157
|
+
const currentViewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
9158
|
+
if (!currentViewport) return
|
|
9159
|
+
const currentHeight = currentViewport.width / svgAspectRatio
|
|
9160
|
+
const worldPerPixelX = currentViewport.width / surfaceSize.width
|
|
9161
|
+
const worldPerPixelY = currentHeight / surfaceSize.height
|
|
9110
9162
|
|
|
9111
9163
|
const nextCenterSvg = {
|
|
9112
9164
|
x: panStateRef.current.centerSvg.x - deltaX * worldPerPixelX,
|
|
@@ -9117,8 +9169,20 @@ export function FloorplanPanel({
|
|
|
9117
9169
|
FLOORPLAN_VIEW_ROTATION_DEG + currentUserRotationDeg - buildingRotationDeg
|
|
9118
9170
|
const localCenter = rotateSvgPoint(nextCenterSvg, -currentSceneRotationDeg)
|
|
9119
9171
|
|
|
9120
|
-
|
|
9121
|
-
|
|
9172
|
+
const nextViewport = {
|
|
9173
|
+
centerX: nextCenterSvg.x,
|
|
9174
|
+
centerY: nextCenterSvg.y,
|
|
9175
|
+
width: currentViewport.width,
|
|
9176
|
+
}
|
|
9177
|
+
applyFloorplanViewportImperatively(nextViewport)
|
|
9178
|
+
floorplanPanPoseRef.current = {
|
|
9179
|
+
localCenter,
|
|
9180
|
+
userRotationDeg: currentUserRotationDeg,
|
|
9181
|
+
viewWidth: currentViewport.width,
|
|
9182
|
+
}
|
|
9183
|
+
if (useEditor.getState().viewMode === 'split') {
|
|
9184
|
+
publishFloorplanNavigationPose(localCenter, currentUserRotationDeg, currentViewport.width)
|
|
9185
|
+
}
|
|
9122
9186
|
|
|
9123
9187
|
panStateRef.current = {
|
|
9124
9188
|
pointerId: event.pointerId,
|
|
@@ -9134,10 +9198,6 @@ export function FloorplanPanel({
|
|
|
9134
9198
|
return
|
|
9135
9199
|
}
|
|
9136
9200
|
|
|
9137
|
-
if (elevatorResizeDragState?.pointerId === event.pointerId) {
|
|
9138
|
-
return
|
|
9139
|
-
}
|
|
9140
|
-
|
|
9141
9201
|
if (wallEndpointDragRef.current?.pointerId === event.pointerId) {
|
|
9142
9202
|
return
|
|
9143
9203
|
}
|
|
@@ -9458,6 +9518,8 @@ export function FloorplanPanel({
|
|
|
9458
9518
|
},
|
|
9459
9519
|
[
|
|
9460
9520
|
buildingRotationDeg,
|
|
9521
|
+
applyFloorplanViewportImperatively,
|
|
9522
|
+
applyFloorplanRotationImperatively,
|
|
9461
9523
|
draftStart,
|
|
9462
9524
|
ceilingDraftPoints,
|
|
9463
9525
|
emitFloorplanWallLeave,
|
|
@@ -9486,15 +9548,12 @@ export function FloorplanPanel({
|
|
|
9486
9548
|
isWallBuildActive,
|
|
9487
9549
|
levelId,
|
|
9488
9550
|
publishFloorplanNavigationPose,
|
|
9489
|
-
smoothFloorplanNavigationView,
|
|
9490
9551
|
referenceScaleDraft,
|
|
9491
9552
|
roofDraftStart,
|
|
9492
|
-
elevatorResizeDragState,
|
|
9493
9553
|
siteVertexDragState,
|
|
9494
9554
|
surfaceSize.height,
|
|
9495
9555
|
surfaceSize.width,
|
|
9496
|
-
|
|
9497
|
-
viewBox.width,
|
|
9556
|
+
svgAspectRatio,
|
|
9498
9557
|
walls,
|
|
9499
9558
|
setCursorPoint,
|
|
9500
9559
|
setDraftEnd,
|
|
@@ -9519,16 +9578,32 @@ export function FloorplanPanel({
|
|
|
9519
9578
|
if (firstPoint && slabDraftPoints.length >= 3 && isPointNearPlanPoint(point, firstPoint)) {
|
|
9520
9579
|
// 2D-only view: the 3D tool can't commit, so close the polygon here.
|
|
9521
9580
|
if (useEditor.getState().viewMode === '2d') {
|
|
9522
|
-
createSlabOnCurrentLevel(slabDraftPoints)
|
|
9581
|
+
createSlabOnCurrentLevel(slabDraftPoints, slabPlacementBaseRef.current)
|
|
9523
9582
|
}
|
|
9524
9583
|
clearDraft()
|
|
9525
9584
|
return
|
|
9526
9585
|
}
|
|
9527
9586
|
|
|
9587
|
+
if (slabDraftPoints.length === 0) {
|
|
9588
|
+
const recessed = useEditor.getState().toolDefaults.slab?.recessed === true
|
|
9589
|
+
if (!levelId || recessed) {
|
|
9590
|
+
slabPlacementBaseRef.current = null
|
|
9591
|
+
} else {
|
|
9592
|
+
const support = spatialGridManager.getPointedSupportSurface(
|
|
9593
|
+
levelId,
|
|
9594
|
+
[point[0], 1_000_000, point[1]],
|
|
9595
|
+
[0, -1, 0],
|
|
9596
|
+
)
|
|
9597
|
+
slabPlacementBaseRef.current =
|
|
9598
|
+
support.slabId != null
|
|
9599
|
+
? support.elevation
|
|
9600
|
+
: terrainSupportLift(useScene.getState().nodes, levelId, point[0], point[1])
|
|
9601
|
+
}
|
|
9602
|
+
}
|
|
9528
9603
|
setSlabDraftPoints((currentPoints) => [...currentPoints, point])
|
|
9529
9604
|
setCursorPoint(point)
|
|
9530
9605
|
},
|
|
9531
|
-
[clearDraft, createSlabOnCurrentLevel, slabDraftPoints, setCursorPoint],
|
|
9606
|
+
[clearDraft, createSlabOnCurrentLevel, levelId, slabDraftPoints, setCursorPoint],
|
|
9532
9607
|
)
|
|
9533
9608
|
const handleSlabPlacementConfirm = useCallback(
|
|
9534
9609
|
(point?: WallPlanPoint) => {
|
|
@@ -9553,7 +9628,7 @@ export function FloorplanPanel({
|
|
|
9553
9628
|
|
|
9554
9629
|
// 2D-only view: the 3D tool can't commit, so create the slab here.
|
|
9555
9630
|
if (useEditor.getState().viewMode === '2d') {
|
|
9556
|
-
createSlabOnCurrentLevel(nextPoints)
|
|
9631
|
+
createSlabOnCurrentLevel(nextPoints, slabPlacementBaseRef.current)
|
|
9557
9632
|
}
|
|
9558
9633
|
clearDraft()
|
|
9559
9634
|
},
|
|
@@ -9660,6 +9735,14 @@ export function FloorplanPanel({
|
|
|
9660
9735
|
const handleWallPlacementPoint = useCallback(
|
|
9661
9736
|
(point: WallPlanPoint) => {
|
|
9662
9737
|
if (!draftStart) {
|
|
9738
|
+
wallConstructionOptionsRef.current = levelId
|
|
9739
|
+
? resolveTerrainWallConstructionOptions(
|
|
9740
|
+
useScene.getState().nodes,
|
|
9741
|
+
levelId,
|
|
9742
|
+
point,
|
|
9743
|
+
useEditor.getState().toolDefaults.wall,
|
|
9744
|
+
)
|
|
9745
|
+
: undefined
|
|
9663
9746
|
setDraftStart(point)
|
|
9664
9747
|
setWallChainFirstVertex(point)
|
|
9665
9748
|
setDraftEnd(point)
|
|
@@ -9686,7 +9769,14 @@ export function FloorplanPanel({
|
|
|
9686
9769
|
// ceiling 2D-only committers: create locally here, gated on the
|
|
9687
9770
|
// view, so split / 3D keep their single-owner tool commit.
|
|
9688
9771
|
const viewIs2DOnly = useEditor.getState().viewMode === '2d'
|
|
9689
|
-
|
|
9772
|
+
let createdWall: WallNode | null = null
|
|
9773
|
+
if (viewIs2DOnly) {
|
|
9774
|
+
createdWall = createWallOnCurrentLevel(
|
|
9775
|
+
draftStart,
|
|
9776
|
+
point,
|
|
9777
|
+
wallConstructionOptionsRef.current,
|
|
9778
|
+
)
|
|
9779
|
+
}
|
|
9690
9780
|
if (createdWall) {
|
|
9691
9781
|
wallChainWallIdsRef.current.push(createdWall.id)
|
|
9692
9782
|
}
|
|
@@ -9753,21 +9843,7 @@ export function FloorplanPanel({
|
|
|
9753
9843
|
],
|
|
9754
9844
|
)
|
|
9755
9845
|
const { getFloorplanHitIdAtPoint, getFloorplanSelectionIdsInBounds } = useFloorplanHitTesting({
|
|
9756
|
-
|
|
9757
|
-
columnPolygons: floorplanColumnEntries,
|
|
9758
|
-
displaySlabPolygons,
|
|
9759
|
-
displayWallPolygons,
|
|
9760
|
-
floorplanElevatorEntries,
|
|
9761
|
-
floorplanItemEntries,
|
|
9762
|
-
floorplanOpeningHitTolerance,
|
|
9763
|
-
floorplanRoofEntries,
|
|
9764
|
-
floorplanStairEntries,
|
|
9765
|
-
floorplanWallHitTolerance,
|
|
9766
|
-
getOpeningCenterLine,
|
|
9767
|
-
isFloorplanItemContextActive,
|
|
9768
|
-
openingsPolygons,
|
|
9769
|
-
phase,
|
|
9770
|
-
toPoint2D,
|
|
9846
|
+
sceneRef: floorplanSceneRef,
|
|
9771
9847
|
})
|
|
9772
9848
|
// Wall-commit snap for the placement hook. Mirrors the move-preview branch:
|
|
9773
9849
|
// it honours the Magnetic snap toggle so a click never snaps to geometry the
|
|
@@ -9907,8 +9983,6 @@ export function FloorplanPanel({
|
|
|
9907
9983
|
modifierKeys,
|
|
9908
9984
|
planPoint,
|
|
9909
9985
|
structureLayer,
|
|
9910
|
-
toPoint2D,
|
|
9911
|
-
visibleZonePolygons,
|
|
9912
9986
|
})
|
|
9913
9987
|
|
|
9914
9988
|
if (backgroundSelection.handled) {
|
|
@@ -9973,7 +10047,6 @@ export function FloorplanPanel({
|
|
|
9973
10047
|
structureLayer,
|
|
9974
10048
|
getFloorplanHitIdAtPoint,
|
|
9975
10049
|
unit,
|
|
9976
|
-
visibleZonePolygons,
|
|
9977
10050
|
emitFloorplanGridEvent,
|
|
9978
10051
|
setCursorPoint,
|
|
9979
10052
|
],
|
|
@@ -10623,7 +10696,6 @@ export function FloorplanPanel({
|
|
|
10623
10696
|
!panStateRef.current &&
|
|
10624
10697
|
!floorplanRotationStateRef.current &&
|
|
10625
10698
|
!guideInteractionRef.current &&
|
|
10626
|
-
!elevatorResizeDragState &&
|
|
10627
10699
|
!wallEndpointDragRef.current &&
|
|
10628
10700
|
!siteVertexDragState
|
|
10629
10701
|
) {
|
|
@@ -10651,7 +10723,6 @@ export function FloorplanPanel({
|
|
|
10651
10723
|
handlePointerMove,
|
|
10652
10724
|
hasFloorplanCursorIndicator,
|
|
10653
10725
|
isSpacePanPressed,
|
|
10654
|
-
elevatorResizeDragState,
|
|
10655
10726
|
siteVertexDragState,
|
|
10656
10727
|
setFloorplanCursorPosition,
|
|
10657
10728
|
],
|
|
@@ -10879,11 +10950,13 @@ export function FloorplanPanel({
|
|
|
10879
10950
|
event.preventDefault()
|
|
10880
10951
|
event.stopPropagation()
|
|
10881
10952
|
|
|
10953
|
+
floorplanNavigationSyncScheduler.flush()
|
|
10882
10954
|
const widthFactor = Math.exp(event.deltaY * (event.ctrlKey ? 0.003 : 0.0015))
|
|
10883
10955
|
zoomViewportAtClientPoint(event.clientX, event.clientY, widthFactor)
|
|
10884
10956
|
}
|
|
10885
10957
|
|
|
10886
10958
|
const handleGestureStart = (event: Event) => {
|
|
10959
|
+
floorplanNavigationSyncScheduler.flush()
|
|
10887
10960
|
const gestureEvent = event as GestureLikeEvent
|
|
10888
10961
|
gestureScaleRef.current = gestureEvent.scale ?? 1
|
|
10889
10962
|
event.preventDefault()
|
|
@@ -10910,6 +10983,7 @@ export function FloorplanPanel({
|
|
|
10910
10983
|
|
|
10911
10984
|
const handleGestureEnd = (event: Event) => {
|
|
10912
10985
|
gestureScaleRef.current = 1
|
|
10986
|
+
commitFloorplanZoom()
|
|
10913
10987
|
event.preventDefault()
|
|
10914
10988
|
event.stopPropagation()
|
|
10915
10989
|
}
|
|
@@ -10929,7 +11003,7 @@ export function FloorplanPanel({
|
|
|
10929
11003
|
svg.removeEventListener('gesturechange', handleGestureChange)
|
|
10930
11004
|
svg.removeEventListener('gestureend', handleGestureEnd)
|
|
10931
11005
|
}
|
|
10932
|
-
}, [zoomViewportAtClientPoint])
|
|
11006
|
+
}, [commitFloorplanZoom, floorplanNavigationSyncScheduler, zoomViewportAtClientPoint])
|
|
10933
11007
|
|
|
10934
11008
|
const restoreGroundLevelStructureSelection = useCallback(() => {
|
|
10935
11009
|
const sceneNodes = useScene.getState().nodes
|
|
@@ -11047,7 +11121,7 @@ export function FloorplanPanel({
|
|
|
11047
11121
|
ref={containerRef}
|
|
11048
11122
|
>
|
|
11049
11123
|
<FloorplanSiteKeyHandler onRestoreGroundLevel={restoreGroundLevelStructureSelection} />
|
|
11050
|
-
<div className="relative min-h-0 flex-1" ref={viewportHostRef}>
|
|
11124
|
+
<div className="relative min-h-0 flex-1 bg-white" ref={viewportHostRef}>
|
|
11051
11125
|
<FloorplanCursorIndicator
|
|
11052
11126
|
cursorColor={floorplanCursorColor}
|
|
11053
11127
|
floorplanSelectionTool={floorplanSelectionTool}
|
|
@@ -11134,7 +11208,12 @@ export function FloorplanPanel({
|
|
|
11134
11208
|
Drawn line
|
|
11135
11209
|
</div>
|
|
11136
11210
|
<div className="mt-1 font-medium text-sm">
|
|
11137
|
-
{formatMeasurement(
|
|
11211
|
+
{formatMeasurement(
|
|
11212
|
+
pendingReferenceScale.measuredLengthUnits,
|
|
11213
|
+
unit,
|
|
11214
|
+
null,
|
|
11215
|
+
metricNotation,
|
|
11216
|
+
)}
|
|
11138
11217
|
</div>
|
|
11139
11218
|
</div>
|
|
11140
11219
|
|
|
@@ -11220,6 +11299,7 @@ export function FloorplanPanel({
|
|
|
11220
11299
|
// panel, so pan/zoom is preserved across the toggle.
|
|
11221
11300
|
<svg
|
|
11222
11301
|
className="h-full w-full touch-none"
|
|
11302
|
+
data-pascal-floorplan-2d
|
|
11223
11303
|
onClick={isMarqueeSelectionToolActive ? undefined : handleSvgClick}
|
|
11224
11304
|
onContextMenu={(event) => event.preventDefault()}
|
|
11225
11305
|
onDoubleClick={isMarqueeSelectionToolActive ? undefined : handleBackgroundDoubleClick}
|
|
@@ -11233,8 +11313,9 @@ export function FloorplanPanel({
|
|
|
11233
11313
|
style={{
|
|
11234
11314
|
cursor:
|
|
11235
11315
|
floorplanNavigationCursor ?? (referenceScaleDraft ? 'crosshair' : EDITOR_CURSOR),
|
|
11316
|
+
overflow: 'visible',
|
|
11236
11317
|
}}
|
|
11237
|
-
viewBox={`${
|
|
11318
|
+
viewBox={`${presentationViewBox.minX} ${presentationViewBox.minY} ${presentationViewBox.width} ${presentationViewBox.height}`}
|
|
11238
11319
|
>
|
|
11239
11320
|
<defs>
|
|
11240
11321
|
<pattern
|
|
@@ -11272,10 +11353,11 @@ export function FloorplanPanel({
|
|
|
11272
11353
|
</defs>
|
|
11273
11354
|
<rect
|
|
11274
11355
|
fill={palette.surface}
|
|
11275
|
-
height={
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
|
|
11356
|
+
height={presentationPaintViewBox.height}
|
|
11357
|
+
ref={floorplanBackgroundRef}
|
|
11358
|
+
width={presentationPaintViewBox.width}
|
|
11359
|
+
x={presentationPaintViewBox.minX}
|
|
11360
|
+
y={presentationPaintViewBox.minY}
|
|
11279
11361
|
/>
|
|
11280
11362
|
|
|
11281
11363
|
<g
|
|
@@ -11337,7 +11419,7 @@ export function FloorplanPanel({
|
|
|
11337
11419
|
{isMarqueeSelectionToolActive && (
|
|
11338
11420
|
<rect
|
|
11339
11421
|
fill="transparent"
|
|
11340
|
-
height={
|
|
11422
|
+
height={presentationViewBox.height}
|
|
11341
11423
|
onClick={(event) => {
|
|
11342
11424
|
event.preventDefault()
|
|
11343
11425
|
event.stopPropagation()
|
|
@@ -11351,25 +11433,18 @@ export function FloorplanPanel({
|
|
|
11351
11433
|
onPointerMove={handleMarqueePointerMove}
|
|
11352
11434
|
onPointerUp={handleMarqueePointerUp}
|
|
11353
11435
|
style={{ cursor: EDITOR_CURSOR }}
|
|
11354
|
-
width={
|
|
11355
|
-
x={
|
|
11356
|
-
y={
|
|
11436
|
+
width={presentationViewBox.width}
|
|
11437
|
+
x={presentationViewBox.minX}
|
|
11438
|
+
y={presentationViewBox.minY}
|
|
11357
11439
|
/>
|
|
11358
11440
|
)}
|
|
11359
11441
|
|
|
11360
|
-
{/* Registry-driven floor-plan layer.
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
today) overlay on top until their inline equivalent is
|
|
11365
|
-
removed in their Phase 5 migration PR.
|
|
11366
|
-
|
|
11367
|
-
Wrapped in <FloorplanRenderProvider> so registry-driven
|
|
11368
|
-
kinds receive the same themed palette / units-per-pixel
|
|
11369
|
-
the legacy layers compute. The hatch pattern id is the
|
|
11370
|
-
legacy wall hatch — kinds that opt into selection hatch
|
|
11371
|
-
fills reuse this <defs> pattern via fill="url(...)". */}
|
|
11442
|
+
{/* Registry-driven floor-plan layer. Every committed node kind
|
|
11443
|
+
supplies its geometry through `NodeDefinition.floorplan`.
|
|
11444
|
+
The provider carries editor presentation data—palette,
|
|
11445
|
+
scale, rotation, and the shared selection-hatch pattern. */}
|
|
11372
11446
|
<FloorplanRenderProvider
|
|
11447
|
+
getSceneRotationDeg={getFloorplanSceneRotationDeg}
|
|
11373
11448
|
hatchPatternId={wallSelectionHatchId}
|
|
11374
11449
|
palette={floorplanRegistryPalette}
|
|
11375
11450
|
sceneRotationDeg={floorplanSceneRotationDeg}
|
|
@@ -11396,6 +11471,8 @@ export function FloorplanPanel({
|
|
|
11396
11471
|
<FloorplanWallMoveGhostLayer />
|
|
11397
11472
|
</g>
|
|
11398
11473
|
<FloorplanMeasurementToolLayer />
|
|
11474
|
+
<FloorplanRegisteredToolLayer />
|
|
11475
|
+
{floorplanSceneSlot}
|
|
11399
11476
|
</FloorplanRenderProvider>
|
|
11400
11477
|
{/* Cursor-driven placement ghost for movingNode when the
|
|
11401
11478
|
active kind is registry-driven. Renders via a portal
|
|
@@ -11506,13 +11583,6 @@ export function FloorplanPanel({
|
|
|
11506
11583
|
walls={walls}
|
|
11507
11584
|
/>
|
|
11508
11585
|
|
|
11509
|
-
{/* Wall / fence endpoint, wall curve, slab / ceiling /
|
|
11510
|
-
zone vertex+midpoint+edge handles are all driven by the
|
|
11511
|
-
registry's `def.floorplanAffordances` and rendered as
|
|
11512
|
-
part of `FloorplanRegistryLayer`. The legacy handle
|
|
11513
|
-
layers that lived here received empty handle arrays
|
|
11514
|
-
post-migration and rendered nothing. */}
|
|
11515
|
-
|
|
11516
11586
|
{selectedGuide && showGuides && (
|
|
11517
11587
|
<FloorplanGuideSelectionOverlay
|
|
11518
11588
|
guide={selectedGuide}
|
|
@@ -11564,12 +11634,12 @@ export function FloorplanPanel({
|
|
|
11564
11634
|
{isFloorplanNavigationOverlayVisible && (
|
|
11565
11635
|
<rect
|
|
11566
11636
|
fill="transparent"
|
|
11567
|
-
height={
|
|
11637
|
+
height={presentationViewBox.height}
|
|
11568
11638
|
pointerEvents="all"
|
|
11569
11639
|
style={{ cursor: floorplanNavigationCursor ?? 'grab' }}
|
|
11570
|
-
width={
|
|
11571
|
-
x={
|
|
11572
|
-
y={
|
|
11640
|
+
width={presentationViewBox.width}
|
|
11641
|
+
x={presentationViewBox.minX}
|
|
11642
|
+
y={presentationViewBox.minY}
|
|
11573
11643
|
/>
|
|
11574
11644
|
)}
|
|
11575
11645
|
</svg>
|