@pascal-app/editor 1.0.1 → 1.0.4
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 +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +132 -0
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type FloorplanPoint,
|
|
13
13
|
type FloorplanScope,
|
|
14
14
|
type GeometryContext,
|
|
15
|
+
hidesDescendants,
|
|
15
16
|
isNodeKindEnabled,
|
|
16
17
|
isRegistryMovable,
|
|
17
18
|
kindsWithFloorplanScope,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
resolveBuildingForLevel,
|
|
23
24
|
resolveSelectionProxyId,
|
|
24
25
|
resumeSceneHistory,
|
|
26
|
+
type SlabNode,
|
|
25
27
|
useInteractive,
|
|
26
28
|
useLiveNodeOverrides,
|
|
27
29
|
useLiveTransforms,
|
|
@@ -42,6 +44,14 @@ import {
|
|
|
42
44
|
} from 'react'
|
|
43
45
|
import { useShallow } from 'zustand/react/shallow'
|
|
44
46
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
47
|
+
import {
|
|
48
|
+
hoverRoomFromHit,
|
|
49
|
+
resolvePlanRoomHit,
|
|
50
|
+
roomPickingEnabled,
|
|
51
|
+
selectionModifiersFromEvent,
|
|
52
|
+
useRoomRecords,
|
|
53
|
+
} from '../../../hooks/use-selected-room'
|
|
54
|
+
import { useCeilingEditSession } from '../../../lib/ceiling-edit-session'
|
|
45
55
|
import { ROTATE_HANDLE_DRAG_LABEL } from '../../../lib/contextual-help'
|
|
46
56
|
import {
|
|
47
57
|
canDirectRotateNode,
|
|
@@ -52,6 +62,7 @@ import {
|
|
|
52
62
|
snapDirectRotationDelta,
|
|
53
63
|
} from '../../../lib/direct-manipulation'
|
|
54
64
|
import { createEditorApi } from '../../../lib/editor-api'
|
|
65
|
+
import { isRoomOwnedPlate, roomOwnedPlateDrillTarget } from '../../../lib/floor-footprints'
|
|
55
66
|
import {
|
|
56
67
|
type FloorplanAnnotationVisibility,
|
|
57
68
|
filterFloorplanAnnotationGeometry,
|
|
@@ -85,13 +96,21 @@ import {
|
|
|
85
96
|
isIdle,
|
|
86
97
|
tangentReshapeScope,
|
|
87
98
|
} from '../../../lib/interaction/scope'
|
|
88
|
-
import {
|
|
99
|
+
import { runFloorplanWallPush, WALL_PUSH_AFFORDANCE } from '../../../lib/room-handle-drag'
|
|
100
|
+
import { selectRoom, selectRoomFromHit } from '../../../lib/room-selection-commands'
|
|
101
|
+
import { clickZoneArea, hoverZoneArea } from '../../../lib/room-zone-routing'
|
|
102
|
+
import {
|
|
103
|
+
emitCanvasNodeSelection,
|
|
104
|
+
enterBuildingFromPlanHit,
|
|
105
|
+
resolveCanvasBuildingId,
|
|
106
|
+
} from '../../../lib/selection-routing'
|
|
107
|
+
import { createSessionWrites, type SessionWrites } from '../../../lib/session-writes'
|
|
89
108
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
90
109
|
import { clearSurfacePlanSnapFeedback } from '../../../lib/surface-plan-snap'
|
|
91
110
|
import { paintZoneMembership } from '../../../lib/units'
|
|
92
111
|
import useDirectManipulationFeedback from '../../../store/use-direct-manipulation-feedback'
|
|
93
112
|
import useDrawingView from '../../../store/use-drawing-view'
|
|
94
|
-
import useEditor, { isAngleSnapActive } from '../../../store/use-editor'
|
|
113
|
+
import useEditor, { isAngleSnapActive, type Mode } from '../../../store/use-editor'
|
|
95
114
|
import useFloorplanAnnotationVisibility from '../../../store/use-floorplan-annotation-visibility'
|
|
96
115
|
import useFloorplanMode from '../../../store/use-floorplan-mode'
|
|
97
116
|
import useInteractionScope, {
|
|
@@ -100,8 +119,13 @@ import useInteractionScope, {
|
|
|
100
119
|
useMovingNode,
|
|
101
120
|
} from '../../../store/use-interaction-scope'
|
|
102
121
|
import { expandSessionSelectionForNode } from '../../../store/use-session-groups'
|
|
122
|
+
import { isViewVisible, VIEW_2D } from '../../../store/view-layout'
|
|
123
|
+
import { FloorRegionControls2D } from '../../editor/floor-region-controls'
|
|
103
124
|
import { startGroupPickUp } from '../../editor/group-actions'
|
|
104
125
|
import { classifyParticipant } from '../../editor/group-transform-shared'
|
|
126
|
+
import { RoomHandleDragPreview2D, RoomHandles2D } from '../../editor/room-handles-2d'
|
|
127
|
+
import { RoomHighlight2D } from '../../editor/room-highlight'
|
|
128
|
+
import { RoomLabels2D } from '../../editor/room-labels'
|
|
105
129
|
import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
|
|
106
130
|
import {
|
|
107
131
|
FloorplanGroupSelectionBox,
|
|
@@ -119,6 +143,7 @@ import {
|
|
|
119
143
|
resolveSvgAnnotationCollisions,
|
|
120
144
|
svgAnnotationLabelId,
|
|
121
145
|
} from './floorplan-annotation-layout'
|
|
146
|
+
import { FloorplanDimensionEditOverlay } from './floorplan-dimension-edit-overlay'
|
|
122
147
|
import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
|
|
123
148
|
import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
|
|
124
149
|
import {
|
|
@@ -178,22 +203,19 @@ const ScaleAwareFloorplanGroupSelectionBox = memo(function ScaleAwareFloorplanGr
|
|
|
178
203
|
return <FloorplanGroupSelectionBox {...props} unitsPerPixel={unitsPerPixel} />
|
|
179
204
|
})
|
|
180
205
|
|
|
181
|
-
/**
|
|
182
|
-
* Snapshot of node fields captured at drag-start, used by the single-undo
|
|
183
|
-
* dance to revert untracked before re-applying as a single tracked
|
|
184
|
-
* change. The dispatcher only knows about the `affectedIds` the
|
|
185
|
-
* affordance declares; it captures whatever fields exist on each node by
|
|
186
|
-
* cloning the full record minus the registry-managed `id` / `type`.
|
|
187
|
-
*/
|
|
188
|
-
type NodeSnapshot = { id: AnyNodeId; data: Record<string, unknown> }
|
|
189
|
-
|
|
190
206
|
type ActiveDrag = {
|
|
191
207
|
pointerId: number
|
|
192
208
|
captureTarget: Element
|
|
193
209
|
/** Key for the visual `active` flag — e.g. `${nodeId}:${endpoint}`. */
|
|
194
210
|
handleId: string
|
|
195
211
|
session: FloorplanAffordanceSession
|
|
196
|
-
|
|
212
|
+
/**
|
|
213
|
+
* What the session itself wrote to the scene during `apply()` (legacy
|
|
214
|
+
* sessions only; override-only sessions write nothing). Cancel and commit
|
|
215
|
+
* take back exactly these writes, so a collaborator's edit to the same node
|
|
216
|
+
* mid-drag survives.
|
|
217
|
+
*/
|
|
218
|
+
writes: SessionWrites
|
|
197
219
|
historyPaused: boolean
|
|
198
220
|
/**
|
|
199
221
|
* Last plan point handed to `session.apply` (the grab point until the first
|
|
@@ -217,7 +239,6 @@ type ActiveDrag = {
|
|
|
217
239
|
}
|
|
218
240
|
|
|
219
241
|
type FloorplanAffordanceCancelEffects = {
|
|
220
|
-
restoreSnapshots: (snapshots: NodeSnapshot[]) => void
|
|
221
242
|
resumeHistory: () => void
|
|
222
243
|
clearPreview: (id: AnyNodeId) => void
|
|
223
244
|
clearSnapFeedback: () => void
|
|
@@ -241,7 +262,7 @@ export function cancelFloorplanAffordanceDrag(
|
|
|
241
262
|
drag.captureTarget.releasePointerCapture?.(drag.pointerId)
|
|
242
263
|
}
|
|
243
264
|
|
|
244
|
-
|
|
265
|
+
drag.writes.revert()
|
|
245
266
|
if (drag.historyPaused) {
|
|
246
267
|
effects.resumeHistory()
|
|
247
268
|
drag.historyPaused = false
|
|
@@ -377,6 +398,7 @@ type CacheEntry = {
|
|
|
377
398
|
deps: NodeDeps
|
|
378
399
|
base: FloorplanGeometry | null
|
|
379
400
|
overlay: FloorplanGeometry | null
|
|
401
|
+
handles: FloorplanGeometry | null
|
|
380
402
|
node: AnyNode
|
|
381
403
|
}
|
|
382
404
|
|
|
@@ -432,7 +454,7 @@ type FloorplanLevelDataHook = (args: {
|
|
|
432
454
|
nodes: Record<string, AnyNode>
|
|
433
455
|
}) => unknown
|
|
434
456
|
|
|
435
|
-
type FloorplanRenderPass = 'base' | 'overlay'
|
|
457
|
+
type FloorplanRenderPass = 'base' | 'overlay' | 'handles'
|
|
436
458
|
|
|
437
459
|
const POINTER_CURSOR_STYLE = { cursor: 'pointer' } as const
|
|
438
460
|
// Group members advertise the drag-to-move-the-selection gesture.
|
|
@@ -440,49 +462,48 @@ const MOVE_CURSOR_STYLE = { cursor: 'move' } as const
|
|
|
440
462
|
const NO_POINTER_EVENTS_STYLE = { pointerEvents: 'none' } as const
|
|
441
463
|
|
|
442
464
|
export function isFloorplanOpeningPlacementState({
|
|
443
|
-
phase,
|
|
444
465
|
mode,
|
|
445
466
|
tool,
|
|
446
467
|
movingNodeHasWallOpeningPlacement,
|
|
447
468
|
}: {
|
|
448
|
-
phase: string
|
|
449
469
|
mode: string
|
|
450
470
|
tool: string | null
|
|
451
471
|
movingNodeHasWallOpeningPlacement: boolean
|
|
452
472
|
}): boolean {
|
|
453
473
|
return (
|
|
454
|
-
(
|
|
474
|
+
(mode === 'build' && (tool === 'door' || tool === 'window')) ||
|
|
455
475
|
movingNodeHasWallOpeningPlacement
|
|
456
476
|
)
|
|
457
477
|
}
|
|
458
478
|
|
|
459
|
-
|
|
460
|
-
|
|
479
|
+
/**
|
|
480
|
+
* Whether a press on an entry belongs to the active tool instead of selecting
|
|
481
|
+
* the entry. Build tools own the plan the way their 3D tools own the canvas,
|
|
482
|
+
* where selection only runs in select mode: a wall drawn onto another wall
|
|
483
|
+
* must place its point (T-junction), not select the wall under the cursor.
|
|
484
|
+
*/
|
|
485
|
+
export function floorplanEntryYieldsToTool(state: {
|
|
486
|
+
mode: Mode
|
|
487
|
+
openingPlacement: boolean
|
|
488
|
+
}): boolean {
|
|
489
|
+
return state.mode === 'build' || state.openingPlacement
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function floorplanEntryYieldsToToolNow(): boolean {
|
|
493
|
+
const { mode, tool } = useEditor.getState()
|
|
461
494
|
const movingNode = getMovingNode()
|
|
462
|
-
return
|
|
463
|
-
phase,
|
|
495
|
+
return floorplanEntryYieldsToTool({
|
|
464
496
|
mode,
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
497
|
+
openingPlacement: isFloorplanOpeningPlacementState({
|
|
498
|
+
mode,
|
|
499
|
+
tool,
|
|
500
|
+
movingNodeHasWallOpeningPlacement:
|
|
501
|
+
movingNode != null &&
|
|
502
|
+
!!nodeRegistry.get(movingNode.type)?.capabilities?.wallOpeningPlacement,
|
|
503
|
+
}),
|
|
468
504
|
})
|
|
469
505
|
}
|
|
470
506
|
|
|
471
|
-
function snapshotNode(node: AnyNode): NodeSnapshot {
|
|
472
|
-
// Shallow-clone every non-id, non-type field. Arrays / vec tuples are
|
|
473
|
-
// deep-cloned to detach from the live store reference.
|
|
474
|
-
const data: Record<string, unknown> = {}
|
|
475
|
-
for (const [key, value] of Object.entries(node)) {
|
|
476
|
-
if (key === 'id' || key === 'type' || key === 'object' || key === 'parentId') continue
|
|
477
|
-
data[key] = Array.isArray(value) ? [...(value as unknown[])] : value
|
|
478
|
-
}
|
|
479
|
-
return { id: node.id, data }
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function snapshotsToUpdates(snapshots: NodeSnapshot[]) {
|
|
483
|
-
return snapshots.map((s) => ({ id: s.id, data: s.data }))
|
|
484
|
-
}
|
|
485
|
-
|
|
486
507
|
// Stable empty sentinel used by per-entry builders while the floor plan is
|
|
487
508
|
// hidden; committed scene edits still flow through `useScene`.
|
|
488
509
|
const EMPTY_LIVE_OVERRIDES: Map<string, LiveNodeOverrides> = new Map()
|
|
@@ -546,6 +567,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
546
567
|
}, [selectedLevelId, ambientBuildingSourceId, nodes])
|
|
547
568
|
|
|
548
569
|
const levelId = selectedLevelId ?? ambientLevelId
|
|
570
|
+
useRoomRecords(levelId)
|
|
549
571
|
const isAmbient = !selectedLevelId && !!ambientLevelId
|
|
550
572
|
const activeBuildingId = useMemo(
|
|
551
573
|
() => (levelId ? resolveBuildingForLevel(levelId as AnyNodeId, nodes) : null),
|
|
@@ -558,27 +580,25 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
558
580
|
const sceneRotationDeg = renderCtx?.getSceneRotationDeg() ?? 0
|
|
559
581
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
560
582
|
const setMovingNodeOrigin = useEditor((s) => s.setMovingNodeOrigin)
|
|
561
|
-
//
|
|
562
|
-
// background
|
|
563
|
-
//
|
|
564
|
-
//
|
|
583
|
+
// Build tools and door / window placement (both build and move) need the
|
|
584
|
+
// SVG's background handlers to run — wall drafting places its point there,
|
|
585
|
+
// and opening placement finds the closest wall via `findClosestWallPoint`
|
|
586
|
+
// and emits `wall:click`. When the user clicks *on top of* a wall, the
|
|
565
587
|
// wall's registry entry would otherwise swallow the click via
|
|
566
|
-
// `handleClickStop` / `handleSelect`, so the
|
|
567
|
-
//
|
|
588
|
+
// `handleClickStop` / `handleSelect`, so the tool never sees it. Pass
|
|
589
|
+
// clicks through in that case (`floorplanEntryYieldsToToolNow`).
|
|
568
590
|
const editorMode = useEditor((s) => s.mode)
|
|
569
|
-
const structureLayer = useEditor((s) => s.structureLayer)
|
|
570
591
|
const floorplanSelectionTool = useEditor((s) => s.floorplanSelectionTool)
|
|
571
592
|
const endpointReshape = useEndpointReshape()
|
|
572
593
|
const isMarqueeSelectionActive =
|
|
573
594
|
editorMode === 'select' &&
|
|
574
595
|
floorplanSelectionTool === 'marquee' &&
|
|
575
|
-
structureLayer !== 'zones' &&
|
|
576
596
|
!movingNode &&
|
|
577
597
|
!endpointReshape
|
|
578
598
|
// While the floor plan is not on screen (pure 3D view), per-entry live
|
|
579
599
|
// selectors freeze to `undefined` so drag publishes do not re-render the
|
|
580
600
|
// hidden floor-plan tree.
|
|
581
|
-
const floorplanVisible = useEditor((s) => s
|
|
601
|
+
const floorplanVisible = useEditor((s) => isViewVisible(s, VIEW_2D))
|
|
582
602
|
const drawingType = useDrawingView((s) => s.drawingType)
|
|
583
603
|
const annotationVisibility = useFloorplanAnnotationVisibility((s) => s.visibility)
|
|
584
604
|
const wallDimensionReference = useFloorplanAnnotationVisibility((s) => s.wallDimensionReference)
|
|
@@ -679,12 +699,37 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
679
699
|
swallowNextClick(200)
|
|
680
700
|
return
|
|
681
701
|
}
|
|
702
|
+
// A zone's fill or label takes the same rule as its pill (`clickZoneArea`).
|
|
703
|
+
if (clickedNode?.type === 'zone') {
|
|
704
|
+
clickZoneArea(clickedNode.id, {
|
|
705
|
+
meta: false,
|
|
706
|
+
ctrl: false,
|
|
707
|
+
shift: options.shouldToggle,
|
|
708
|
+
alt: options.isolateMember,
|
|
709
|
+
})
|
|
710
|
+
swallowNextClick(200)
|
|
711
|
+
return
|
|
712
|
+
}
|
|
682
713
|
const currentSelectedIds = useViewer.getState().selection.selectedIds
|
|
683
714
|
let nextSelectedIds: string[]
|
|
684
715
|
if (options.shouldToggle) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
716
|
+
if (currentSelectedIds.includes(id)) {
|
|
717
|
+
// A room's slab and ceiling overlap in the plan and only the slab
|
|
718
|
+
// takes the click, so removing one removes the other too (a marquee
|
|
719
|
+
// picks both). 3D keeps single toggles: each is clickable there.
|
|
720
|
+
const nodes = useScene.getState().nodes
|
|
721
|
+
const node = nodes[id as AnyNodeId]
|
|
722
|
+
const counterparts = node
|
|
723
|
+
? (getFloorplanNodeExtension(nodeRegistry.get(node.type))?.selectionCounterparts?.({
|
|
724
|
+
node,
|
|
725
|
+
nodes,
|
|
726
|
+
}) ?? [])
|
|
727
|
+
: []
|
|
728
|
+
const removed = new Set<string>([id, ...counterparts])
|
|
729
|
+
nextSelectedIds = currentSelectedIds.filter((selectedId) => !removed.has(selectedId))
|
|
730
|
+
} else {
|
|
731
|
+
nextSelectedIds = [...currentSelectedIds, id]
|
|
732
|
+
}
|
|
688
733
|
} else if (options.isolateMember) {
|
|
689
734
|
nextSelectedIds = [id]
|
|
690
735
|
} else {
|
|
@@ -722,18 +767,21 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
722
767
|
)
|
|
723
768
|
|
|
724
769
|
const handleClickStop = useCallback((event: React.MouseEvent<SVGGElement>) => {
|
|
725
|
-
if (
|
|
770
|
+
if (floorplanEntryYieldsToToolNow() || !isIdle(useInteractionScope.getState().scope)) return
|
|
726
771
|
event.stopPropagation()
|
|
727
772
|
}, [])
|
|
728
773
|
|
|
729
774
|
const startDirectMoveDrag = useCallback(
|
|
730
775
|
(id: AnyNodeId, event: ReactPointerEvent<SVGGElement>): boolean => {
|
|
731
|
-
if (event.button !== 0) return false
|
|
776
|
+
if (event.button !== 0 || event.altKey) return false
|
|
732
777
|
|
|
733
778
|
const node = useScene.getState().nodes[id]
|
|
734
779
|
if (!node || !isRegistryMovable(node.type)) return false
|
|
735
780
|
const currentSelectedIds = useViewer.getState().selection.selectedIds
|
|
736
|
-
const
|
|
781
|
+
const definition = nodeRegistry.get(node.type)
|
|
782
|
+
const allowPlainDrag =
|
|
783
|
+
getFloorplanNodeExtension(definition)?.directDrag === true ||
|
|
784
|
+
definition?.capabilities?.movable?.directDrag === true
|
|
737
785
|
const commandModifier = event.metaKey || event.ctrlKey
|
|
738
786
|
if (
|
|
739
787
|
!shouldStartDirectMoveDrag({
|
|
@@ -980,7 +1028,35 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
980
1028
|
// the stores at event time. Commit clears the interaction scope before
|
|
981
1029
|
// React paints the next frame; a render-time `undefined` handler leaves
|
|
982
1030
|
// a short dead zone where the first post-placement selection is lost.
|
|
983
|
-
if (
|
|
1031
|
+
if (floorplanEntryYieldsToToolNow() || !isIdle(useInteractionScope.getState().scope)) return
|
|
1032
|
+
if (event.button === 0 && enterBuildingFromPlanHit(id)) {
|
|
1033
|
+
event.stopPropagation()
|
|
1034
|
+
swallowNextClick(200)
|
|
1035
|
+
return
|
|
1036
|
+
}
|
|
1037
|
+
const point = clientToPlan(event.clientX, event.clientY)
|
|
1038
|
+
if (
|
|
1039
|
+
event.button === 0 &&
|
|
1040
|
+
point &&
|
|
1041
|
+
roomPickingEnabled() &&
|
|
1042
|
+
selectRoomFromHit(resolvePlanRoomHit(id, point), selectionModifiersFromEvent(event), id)
|
|
1043
|
+
) {
|
|
1044
|
+
event.stopPropagation()
|
|
1045
|
+
swallowNextClick(200)
|
|
1046
|
+
return
|
|
1047
|
+
}
|
|
1048
|
+
// A room-owned plate past its room: the same routing as the 3D click.
|
|
1049
|
+
const plate = useScene.getState().nodes[id]
|
|
1050
|
+
if (event.button === 0 && point && roomPickingEnabled() && isRoomOwnedPlate(plate)) {
|
|
1051
|
+
const footprint = roomOwnedPlateDrillTarget(useScene.getState().nodes, plate as SlabNode)
|
|
1052
|
+
if (footprint) handleSelect(footprint.id as AnyNodeId, event)
|
|
1053
|
+
else {
|
|
1054
|
+
const hit = resolvePlanRoomHit(id, point)
|
|
1055
|
+
if (hit) selectRoom(hit)
|
|
1056
|
+
event.stopPropagation()
|
|
1057
|
+
}
|
|
1058
|
+
return
|
|
1059
|
+
}
|
|
984
1060
|
if (startDirectMoveDrag(id, event)) return
|
|
985
1061
|
if (startDirectRotateDrag(id, event)) return
|
|
986
1062
|
if (startGroupMoveDrag(id, event)) return
|
|
@@ -1206,6 +1282,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1206
1282
|
if (event.button !== 0) return
|
|
1207
1283
|
if (movingNode) return
|
|
1208
1284
|
|
|
1285
|
+
// A wall push arrow runs the same push drag as the 3D arrows (its own
|
|
1286
|
+
// gesture, preview and commit), not a registry affordance session.
|
|
1287
|
+
if (affordance === WALL_PUSH_AFFORDANCE) {
|
|
1288
|
+
event.preventDefault()
|
|
1289
|
+
event.stopPropagation()
|
|
1290
|
+
suppressBoxSelectForPointer(event)
|
|
1291
|
+
runFloorplanWallPush(payload, event.clientX, event.clientY)
|
|
1292
|
+
return
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1209
1295
|
const sceneNodes = useScene.getState().nodes
|
|
1210
1296
|
const node = sceneNodes[nodeId]
|
|
1211
1297
|
if (!node) return
|
|
@@ -1231,13 +1317,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1231
1317
|
sceneApi: createSceneApi(useScene),
|
|
1232
1318
|
})
|
|
1233
1319
|
|
|
1234
|
-
const snapshots: NodeSnapshot[] = []
|
|
1235
|
-
for (const id of session.affectedIds) {
|
|
1236
|
-
const n = sceneNodes[id]
|
|
1237
|
-
if (n) snapshots.push(snapshotNode(n))
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
1320
|
pauseSceneHistory(useScene)
|
|
1321
|
+
const writes = createSessionWrites()
|
|
1322
|
+
writes.watch(session.affectedIds)
|
|
1241
1323
|
|
|
1242
1324
|
// Rotation readout setup. The wedge radius tracks the grab distance
|
|
1243
1325
|
// from the pivot (≈ the handle's orbit), nudged inward so the swept
|
|
@@ -1269,7 +1351,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1269
1351
|
captureTarget,
|
|
1270
1352
|
handleId,
|
|
1271
1353
|
session,
|
|
1272
|
-
|
|
1354
|
+
writes,
|
|
1273
1355
|
historyPaused: true,
|
|
1274
1356
|
lastPlanPoint: initialPlanPoint,
|
|
1275
1357
|
rotation,
|
|
@@ -1302,8 +1384,6 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1302
1384
|
cancelFloorplanAffordanceDrag(
|
|
1303
1385
|
dragRef,
|
|
1304
1386
|
{
|
|
1305
|
-
restoreSnapshots: (snapshots) =>
|
|
1306
|
-
useScene.getState().updateNodes(snapshotsToUpdates(snapshots)),
|
|
1307
1387
|
resumeHistory: () => resumeSceneHistory(useScene),
|
|
1308
1388
|
clearPreview: (id) => {
|
|
1309
1389
|
useLiveNodeOverrides.getState().clear(id)
|
|
@@ -1329,15 +1409,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1329
1409
|
if (!planPoint) return
|
|
1330
1410
|
|
|
1331
1411
|
drag.lastPlanPoint = planPoint
|
|
1332
|
-
drag.
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1412
|
+
drag.writes.record(() =>
|
|
1413
|
+
drag.session.apply({
|
|
1414
|
+
planPoint,
|
|
1415
|
+
modifiers: {
|
|
1416
|
+
shiftKey: event.shiftKey,
|
|
1417
|
+
altKey: event.altKey,
|
|
1418
|
+
ctrlKey: event.ctrlKey,
|
|
1419
|
+
metaKey: event.metaKey,
|
|
1420
|
+
},
|
|
1421
|
+
}),
|
|
1422
|
+
)
|
|
1341
1423
|
|
|
1342
1424
|
// Live rotation readout. Sweep from the bearing at grab to the
|
|
1343
1425
|
// current pointer bearing around the pivot — the same measurement
|
|
@@ -1374,11 +1456,11 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1374
1456
|
// Sessions with a `commit` hook own their atomic write (e.g.
|
|
1375
1457
|
// affordances that publish to `useLiveNodeOverrides` during
|
|
1376
1458
|
// `apply()` and never touch scene mid-drag). Mirrors the move
|
|
1377
|
-
// overlay's `session.commit` path —
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
1459
|
+
// overlay's `session.commit` path — take back what the session wrote
|
|
1460
|
+
// (nothing, for those), resume history, then let the session do the
|
|
1461
|
+
// tracked write.
|
|
1380
1462
|
if (commitValid && drag.session.commit) {
|
|
1381
|
-
|
|
1463
|
+
drag.writes.revert()
|
|
1382
1464
|
if (drag.historyPaused) {
|
|
1383
1465
|
resumeSceneHistory(useScene)
|
|
1384
1466
|
drag.historyPaused = false
|
|
@@ -1395,37 +1477,23 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1395
1477
|
}
|
|
1396
1478
|
|
|
1397
1479
|
// Legacy compatibility for sessions that still wrote preview state into
|
|
1398
|
-
// `useScene` during `apply()`:
|
|
1399
|
-
//
|
|
1400
|
-
// `commit()` hook and preview through live overrides/transforms
|
|
1401
|
-
const
|
|
1402
|
-
const
|
|
1403
|
-
for (const snap of drag.snapshots) {
|
|
1404
|
-
const current = sceneNodes[snap.id]
|
|
1405
|
-
if (!current) continue
|
|
1406
|
-
const data: Record<string, unknown> = {}
|
|
1407
|
-
let changed = false
|
|
1408
|
-
for (const [key, before] of Object.entries(snap.data)) {
|
|
1409
|
-
const after = (current as unknown as Record<string, unknown>)[key]
|
|
1410
|
-
if (!deepEqual(before, after)) {
|
|
1411
|
-
data[key] = Array.isArray(after) ? [...(after as unknown[])] : after
|
|
1412
|
-
changed = true
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
if (changed) finalUpdates.push({ id: snap.id, data })
|
|
1416
|
-
}
|
|
1480
|
+
// `useScene` during `apply()`: the fields they wrote are re-applied as
|
|
1481
|
+
// one tracked change after the untracked revert. New sessions should
|
|
1482
|
+
// provide a `commit()` hook and preview through live overrides/transforms.
|
|
1483
|
+
const changes = drag.writes.changes()
|
|
1484
|
+
const changed = changes.create.length + changes.update.length + changes.delete.length > 0
|
|
1417
1485
|
|
|
1418
|
-
if (commitValid &&
|
|
1486
|
+
if (commitValid && changed) {
|
|
1419
1487
|
// Legacy single-undo dance (mirrors the old 3D move-endpoint-tool):
|
|
1420
|
-
// 1.
|
|
1488
|
+
// 1. Take back the session's writes while history is still paused.
|
|
1421
1489
|
// 2. Resume history.
|
|
1422
|
-
// 3. Re-apply
|
|
1423
|
-
|
|
1490
|
+
// 3. Re-apply them — recorded as one tracked change.
|
|
1491
|
+
drag.writes.revert()
|
|
1424
1492
|
if (drag.historyPaused) {
|
|
1425
1493
|
resumeSceneHistory(useScene)
|
|
1426
1494
|
drag.historyPaused = false
|
|
1427
1495
|
}
|
|
1428
|
-
useScene.getState().
|
|
1496
|
+
useScene.getState().applyNodeChanges(changes)
|
|
1429
1497
|
commitPerfAction()
|
|
1430
1498
|
sfxEmitter.emit('sfx:structure-build')
|
|
1431
1499
|
} else {
|
|
@@ -1433,7 +1501,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1433
1501
|
// resume without committing. Also clear any live overrides
|
|
1434
1502
|
// the session published (no-op when the session writes to
|
|
1435
1503
|
// scene directly).
|
|
1436
|
-
|
|
1504
|
+
drag.writes.revert()
|
|
1437
1505
|
if (drag.historyPaused) {
|
|
1438
1506
|
resumeSceneHistory(useScene)
|
|
1439
1507
|
drag.historyPaused = false
|
|
@@ -1469,15 +1537,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1469
1537
|
) {
|
|
1470
1538
|
return
|
|
1471
1539
|
}
|
|
1472
|
-
drag.
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1540
|
+
drag.writes.record(() =>
|
|
1541
|
+
drag.session.apply({
|
|
1542
|
+
planPoint: drag.lastPlanPoint,
|
|
1543
|
+
modifiers: {
|
|
1544
|
+
shiftKey: event.shiftKey,
|
|
1545
|
+
altKey: event.altKey,
|
|
1546
|
+
ctrlKey: event.ctrlKey,
|
|
1547
|
+
metaKey: event.metaKey,
|
|
1548
|
+
},
|
|
1549
|
+
}),
|
|
1550
|
+
)
|
|
1481
1551
|
}
|
|
1482
1552
|
|
|
1483
1553
|
window.addEventListener('pointermove', onPointerMove)
|
|
@@ -1507,6 +1577,48 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1507
1577
|
|
|
1508
1578
|
const palette = renderCtx?.palette
|
|
1509
1579
|
|
|
1580
|
+
const renderEntries = (pass: FloorplanRenderPass) =>
|
|
1581
|
+
entries.map((entry) => (
|
|
1582
|
+
<FloorplanRegistryEntry
|
|
1583
|
+
activeDragId={handleIdForNode(activeDragId, entry.id)}
|
|
1584
|
+
annotationVisibility={annotationVisibility}
|
|
1585
|
+
floorplanMode={floorplanMode}
|
|
1586
|
+
floorplanVisible={floorplanVisible}
|
|
1587
|
+
geometryCacheRef={geometryCacheRef}
|
|
1588
|
+
hatchPatternId={renderCtx?.hatchPatternId}
|
|
1589
|
+
hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
|
|
1590
|
+
interactiveElevators={interactiveElevators}
|
|
1591
|
+
isMarqueeSelectionActive={isMarqueeSelectionActive}
|
|
1592
|
+
key={`${pass}-${entry.id}`}
|
|
1593
|
+
levelDataCacheRef={levelDataCacheRef}
|
|
1594
|
+
levelNodeIdsByType={floorplanData.levelNodeIdsByType}
|
|
1595
|
+
moving={movingNode?.id === entry.id}
|
|
1596
|
+
node={entry.node}
|
|
1597
|
+
nodeId={entry.id}
|
|
1598
|
+
nodes={nodes}
|
|
1599
|
+
onClickStop={handleClickStop}
|
|
1600
|
+
onEntryPointerDown={handleEntryPointerDown}
|
|
1601
|
+
onGroupMovePointerDown={handleGroupMoveHandlePointerDown}
|
|
1602
|
+
onHandleHoverChange={setHoveredHandleId}
|
|
1603
|
+
onHandleDoubleClick={commitAffordanceAction}
|
|
1604
|
+
onHandlePointerDown={startAffordanceDrag}
|
|
1605
|
+
palette={palette}
|
|
1606
|
+
unitFocus={unitFocus}
|
|
1607
|
+
pass={pass}
|
|
1608
|
+
sceneRotationDeg={sceneRotationDeg}
|
|
1609
|
+
setMovingNode={setMovingNode}
|
|
1610
|
+
setMovingNodeOrigin={setMovingNodeOrigin}
|
|
1611
|
+
siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
|
|
1612
|
+
unit={unit}
|
|
1613
|
+
metricNotation={metricNotation}
|
|
1614
|
+
wallDimensionReference={effectiveWallDimensionReference}
|
|
1615
|
+
visibilityRootId={
|
|
1616
|
+
entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
|
|
1617
|
+
}
|
|
1618
|
+
ctxOverrides={entry.ctxOverrides}
|
|
1619
|
+
/>
|
|
1620
|
+
))
|
|
1621
|
+
|
|
1510
1622
|
return (
|
|
1511
1623
|
// The outer wrapper stops `click` events that escape an entry's
|
|
1512
1624
|
// `onClick={handleClickStop}`. The base+overlay split means
|
|
@@ -1532,48 +1644,10 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1532
1644
|
strokes, hatches). Lower-rank kinds (zones) paint first so
|
|
1533
1645
|
higher-rank kinds (slabs, then walls / items / shelves) layer
|
|
1534
1646
|
on top in the expected document-order z-stack. */}
|
|
1535
|
-
<g className="floorplan-registry-base">
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
annotationVisibility={annotationVisibility}
|
|
1540
|
-
floorplanMode={floorplanMode}
|
|
1541
|
-
floorplanVisible={floorplanVisible}
|
|
1542
|
-
geometryCacheRef={geometryCacheRef}
|
|
1543
|
-
hatchPatternId={renderCtx?.hatchPatternId}
|
|
1544
|
-
hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
|
|
1545
|
-
interactiveElevators={interactiveElevators}
|
|
1546
|
-
isMarqueeSelectionActive={isMarqueeSelectionActive}
|
|
1547
|
-
key={`base-${entry.id}`}
|
|
1548
|
-
levelDataCacheRef={levelDataCacheRef}
|
|
1549
|
-
levelNodeIdsByType={floorplanData.levelNodeIdsByType}
|
|
1550
|
-
moving={movingNode?.id === entry.id}
|
|
1551
|
-
node={entry.node}
|
|
1552
|
-
nodeId={entry.id}
|
|
1553
|
-
nodes={nodes}
|
|
1554
|
-
onClickStop={handleClickStop}
|
|
1555
|
-
onEntryPointerDown={handleEntryPointerDown}
|
|
1556
|
-
onGroupMovePointerDown={handleGroupMoveHandlePointerDown}
|
|
1557
|
-
onHandleHoverChange={setHoveredHandleId}
|
|
1558
|
-
onHandleDoubleClick={commitAffordanceAction}
|
|
1559
|
-
onHandlePointerDown={startAffordanceDrag}
|
|
1560
|
-
palette={palette}
|
|
1561
|
-
unitFocus={unitFocus}
|
|
1562
|
-
pass="base"
|
|
1563
|
-
sceneRotationDeg={sceneRotationDeg}
|
|
1564
|
-
setMovingNode={setMovingNode}
|
|
1565
|
-
setMovingNodeOrigin={setMovingNodeOrigin}
|
|
1566
|
-
siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
|
|
1567
|
-
unit={unit}
|
|
1568
|
-
metricNotation={metricNotation}
|
|
1569
|
-
wallDimensionReference={effectiveWallDimensionReference}
|
|
1570
|
-
visibilityRootId={
|
|
1571
|
-
entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
|
|
1572
|
-
}
|
|
1573
|
-
ctxOverrides={entry.ctxOverrides}
|
|
1574
|
-
/>
|
|
1575
|
-
))}
|
|
1576
|
-
</g>
|
|
1647
|
+
<g className="floorplan-registry-base">{renderEntries('base')}</g>
|
|
1648
|
+
<RoomHighlight2D levelId={levelId} />
|
|
1649
|
+
<RoomLabels2D levelId={levelId} />
|
|
1650
|
+
<FloorRegionControls2D levelId={levelId} />
|
|
1577
1651
|
{/* Overlay pass — interactive handles (vertex / midpoint / edge /
|
|
1578
1652
|
move) and labels (text / dimensions). Painted after every base
|
|
1579
1653
|
entry so polygon-editor chrome on a selected slab stays above
|
|
@@ -1581,49 +1655,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1581
1655
|
slab + wall geometry sitting on top of the zone. Each overlay
|
|
1582
1656
|
still routes through the same selection-handling `<g>` so a
|
|
1583
1657
|
click on a zone's name selects the zone. */}
|
|
1584
|
-
<g className="floorplan-registry-overlay">
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
floorplanVisible={floorplanVisible}
|
|
1591
|
-
geometryCacheRef={geometryCacheRef}
|
|
1592
|
-
hatchPatternId={renderCtx?.hatchPatternId}
|
|
1593
|
-
hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
|
|
1594
|
-
interactiveElevators={interactiveElevators}
|
|
1595
|
-
isMarqueeSelectionActive={isMarqueeSelectionActive}
|
|
1596
|
-
key={`overlay-${entry.id}`}
|
|
1597
|
-
levelDataCacheRef={levelDataCacheRef}
|
|
1598
|
-
levelNodeIdsByType={floorplanData.levelNodeIdsByType}
|
|
1599
|
-
moving={movingNode?.id === entry.id}
|
|
1600
|
-
node={entry.node}
|
|
1601
|
-
nodeId={entry.id}
|
|
1602
|
-
nodes={nodes}
|
|
1603
|
-
onClickStop={handleClickStop}
|
|
1604
|
-
onEntryPointerDown={handleEntryPointerDown}
|
|
1605
|
-
onGroupMovePointerDown={handleGroupMoveHandlePointerDown}
|
|
1606
|
-
onHandleHoverChange={setHoveredHandleId}
|
|
1607
|
-
onHandleDoubleClick={commitAffordanceAction}
|
|
1608
|
-
onHandlePointerDown={startAffordanceDrag}
|
|
1609
|
-
palette={palette}
|
|
1610
|
-
unitFocus={unitFocus}
|
|
1611
|
-
pass="overlay"
|
|
1612
|
-
sceneRotationDeg={sceneRotationDeg}
|
|
1613
|
-
setMovingNode={setMovingNode}
|
|
1614
|
-
setMovingNodeOrigin={setMovingNodeOrigin}
|
|
1615
|
-
siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
|
|
1616
|
-
unit={unit}
|
|
1617
|
-
metricNotation={metricNotation}
|
|
1618
|
-
wallDimensionReference={effectiveWallDimensionReference}
|
|
1619
|
-
visibilityRootId={
|
|
1620
|
-
entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
|
|
1621
|
-
}
|
|
1622
|
-
ctxOverrides={entry.ctxOverrides}
|
|
1623
|
-
/>
|
|
1624
|
-
))}
|
|
1625
|
-
</g>
|
|
1658
|
+
<g className="floorplan-registry-overlay">{renderEntries('overlay')}</g>
|
|
1659
|
+
{/* Handles pass — every grabbable handle above every node's overlay,
|
|
1660
|
+
so a neighbour's outline never covers a selected wall's corner. */}
|
|
1661
|
+
<g className="floorplan-registry-handles">{renderEntries('handles')}</g>
|
|
1662
|
+
<RoomHandles2D levelId={levelId} />
|
|
1663
|
+
<RoomHandleDragPreview2D levelId={levelId} />
|
|
1626
1664
|
<FloorplanAnnotationLayoutResolver active={floorplanVisible} />
|
|
1665
|
+
{/* Click-to-type dimension labels. Portals its HTML input to
|
|
1666
|
+
document.body, so mounting it inside the SVG tree is safe. */}
|
|
1667
|
+
<FloorplanDimensionEditOverlay />
|
|
1627
1668
|
{/* Dashed group bbox — shows what a group drag carries along while a
|
|
1628
1669
|
multi-selection exists, rides the live delta mid-drag, and doubles
|
|
1629
1670
|
as the group's whole-area drag handle. */}
|
|
@@ -1988,16 +2029,24 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
1988
2029
|
}: FloorplanRegistryEntryProps): React.ReactElement | null {
|
|
1989
2030
|
const selected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
1990
2031
|
const highlighted = useViewer((state) => state.previewSelectedIds.includes(nodeId))
|
|
1991
|
-
const
|
|
2032
|
+
const multiSelected = useViewer(
|
|
1992
2033
|
(state) =>
|
|
1993
2034
|
state.selection.selectedIds.length > 1 && state.selection.selectedIds.includes(nodeId),
|
|
1994
2035
|
)
|
|
2036
|
+
// A ceiling's vertex / edge handles belong to its Edit ceiling session.
|
|
2037
|
+
const ceilingWithoutSession = useCeilingEditSession(
|
|
2038
|
+
(state) => node.type === 'ceiling' && state.session?.ceilingId !== nodeId,
|
|
2039
|
+
)
|
|
2040
|
+
const suppressHandles = multiSelected || ceilingWithoutSession
|
|
1995
2041
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
1996
2042
|
const selectionProxyId = resolveSelectionProxyId(
|
|
1997
2043
|
node,
|
|
1998
2044
|
nodes as Record<string, AnyNode | undefined>,
|
|
1999
2045
|
)
|
|
2000
|
-
const
|
|
2046
|
+
const phase = useEditor((state) => state.phase)
|
|
2047
|
+
const hoverTargetId =
|
|
2048
|
+
phase === 'site' ? (resolveCanvasBuildingId(node, nodes) ?? selectionProxyId) : selectionProxyId
|
|
2049
|
+
const hovered = useViewer((state) => state.hoveredId === hoverTargetId)
|
|
2001
2050
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
2002
2051
|
const referencedAnnotationRole = useViewer((state) =>
|
|
2003
2052
|
floorplanEntryReferencedAnnotationRole(node, nodes, new Set(state.selection.selectedIds)),
|
|
@@ -2006,7 +2055,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2006
2055
|
state.activeRotateNodeId === nodeId ? nodeId : null,
|
|
2007
2056
|
)
|
|
2008
2057
|
const groupMoveCursor =
|
|
2009
|
-
|
|
2058
|
+
multiSelected && classifyParticipant(node, selectedLevelId, nodes) !== null
|
|
2010
2059
|
const live = useLiveTransforms((s) => (floorplanVisible ? s.transforms.get(nodeId) : undefined))
|
|
2011
2060
|
const liveOverride = useLiveNodeOverrides((s) =>
|
|
2012
2061
|
floorplanVisible ? s.overrides.get(nodeId) : undefined,
|
|
@@ -2031,25 +2080,55 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2031
2080
|
|
|
2032
2081
|
// Mirror the sidebar tree nodes' hover wiring — `useViewer.hoveredId` drives
|
|
2033
2082
|
// the highlight halo in 3D as well as registry floor-plan hover strokes.
|
|
2034
|
-
const handlePointerEnter = useCallback(
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
currentNode
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
)
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2083
|
+
const handlePointerEnter = useCallback(
|
|
2084
|
+
(event: ReactPointerEvent<SVGGElement>) => {
|
|
2085
|
+
if (floorplanEntryYieldsToToolNow() || !isIdle(useInteractionScope.getState().scope)) return
|
|
2086
|
+
const currentNode = useScene.getState().nodes[nodeId]
|
|
2087
|
+
if (currentNode && useEditor.getState().phase === 'site') {
|
|
2088
|
+
const buildingId = resolveCanvasBuildingId(currentNode, useScene.getState().nodes)
|
|
2089
|
+
if (buildingId) {
|
|
2090
|
+
setHoveredId(buildingId)
|
|
2091
|
+
return
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (currentNode?.type === 'zone' && roomPickingEnabled()) {
|
|
2095
|
+
hoverZoneArea(nodeId, selectionModifiersFromEvent(event))
|
|
2096
|
+
return
|
|
2097
|
+
}
|
|
2098
|
+
const point = clientToPlan(event.clientX, event.clientY)
|
|
2099
|
+
if (
|
|
2100
|
+
point &&
|
|
2101
|
+
roomPickingEnabled() &&
|
|
2102
|
+
hoverRoomFromHit(
|
|
2103
|
+
resolvePlanRoomHit(nodeId, point),
|
|
2104
|
+
selectionModifiersFromEvent(event),
|
|
2105
|
+
nodeId,
|
|
2106
|
+
)
|
|
2107
|
+
)
|
|
2108
|
+
return
|
|
2109
|
+
setHoveredId(
|
|
2110
|
+
currentNode
|
|
2111
|
+
? resolveSelectionProxyId(
|
|
2112
|
+
currentNode,
|
|
2113
|
+
useScene.getState().nodes as Record<string, AnyNode | undefined>,
|
|
2114
|
+
)
|
|
2115
|
+
: nodeId,
|
|
2116
|
+
)
|
|
2117
|
+
},
|
|
2118
|
+
[nodeId, setHoveredId],
|
|
2119
|
+
)
|
|
2045
2120
|
|
|
2046
2121
|
const handlePointerLeave = useCallback(() => {
|
|
2122
|
+
useEditor.getState().setHoveredRoom(null)
|
|
2047
2123
|
const node = useScene.getState().nodes[nodeId]
|
|
2048
2124
|
const targetId = node
|
|
2049
|
-
?
|
|
2050
|
-
|
|
2051
|
-
useScene.getState().nodes
|
|
2052
|
-
|
|
2125
|
+
? useEditor.getState().phase === 'site'
|
|
2126
|
+
? (resolveCanvasBuildingId(node, useScene.getState().nodes) ??
|
|
2127
|
+
resolveSelectionProxyId(node, useScene.getState().nodes))
|
|
2128
|
+
: resolveSelectionProxyId(
|
|
2129
|
+
node,
|
|
2130
|
+
useScene.getState().nodes as Record<string, AnyNode | undefined>,
|
|
2131
|
+
)
|
|
2053
2132
|
: nodeId
|
|
2054
2133
|
if (useViewer.getState().hoveredId === targetId) setHoveredId(null)
|
|
2055
2134
|
}, [nodeId, setHoveredId])
|
|
@@ -2125,7 +2204,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2125
2204
|
wallDimensionReference,
|
|
2126
2205
|
visibilityRootId,
|
|
2127
2206
|
})
|
|
2128
|
-
const rawGeometry = cacheEntry ?
|
|
2207
|
+
const rawGeometry = cacheEntry ? cacheEntry[pass] : null
|
|
2129
2208
|
const visibleGeometry = rawGeometry
|
|
2130
2209
|
? filterFloorplanAnnotationGeometry(rawGeometry, presentationVisibility)
|
|
2131
2210
|
: null
|
|
@@ -2133,7 +2212,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2133
2212
|
// dimension chrome (all of which live in the overlay pass) while keeping
|
|
2134
2213
|
// its highlighted body geometry.
|
|
2135
2214
|
const geometry =
|
|
2136
|
-
visibleGeometry && suppressHandles && pass
|
|
2215
|
+
visibleGeometry && suppressHandles && pass !== 'base'
|
|
2137
2216
|
? stripHandleChrome(visibleGeometry)
|
|
2138
2217
|
: visibleGeometry
|
|
2139
2218
|
if (!geometry) return null
|
|
@@ -2148,6 +2227,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2148
2227
|
onClick={entryClick}
|
|
2149
2228
|
onPointerDown={entryPointerDown}
|
|
2150
2229
|
onPointerEnter={handlePointerEnter}
|
|
2230
|
+
onPointerMove={handlePointerEnter}
|
|
2151
2231
|
onPointerLeave={handlePointerLeave}
|
|
2152
2232
|
style={groupMoveCursor ? MOVE_CURSOR_STYLE : POINTER_CURSOR_STYLE}
|
|
2153
2233
|
>
|
|
@@ -2366,6 +2446,7 @@ export function buildFloorplanEntryGeometry({
|
|
|
2366
2446
|
selected,
|
|
2367
2447
|
unit,
|
|
2368
2448
|
metricNotation,
|
|
2449
|
+
roomLabelOverlay: true,
|
|
2369
2450
|
wallDimensionReference,
|
|
2370
2451
|
highlighted,
|
|
2371
2452
|
hovered,
|
|
@@ -2391,6 +2472,7 @@ export function buildFloorplanEntryGeometry({
|
|
|
2391
2472
|
automaticDimensions,
|
|
2392
2473
|
metricNotation,
|
|
2393
2474
|
purpose: 'edit',
|
|
2475
|
+
roomLabelOverlay: true,
|
|
2394
2476
|
wallDimensionReference,
|
|
2395
2477
|
}),
|
|
2396
2478
|
viewState: palette
|
|
@@ -2432,10 +2514,13 @@ export function buildFloorplanEntryGeometry({
|
|
|
2432
2514
|
transform: ctxOverrides.outputTransform,
|
|
2433
2515
|
}
|
|
2434
2516
|
: unprojectedGeometry
|
|
2435
|
-
const { base, overlay } = geometry
|
|
2517
|
+
const { base, overlay: overlayWithHandles } = geometry
|
|
2436
2518
|
? splitFloorplanOverlay(geometry)
|
|
2437
2519
|
: { base: null, overlay: null }
|
|
2438
|
-
const
|
|
2520
|
+
const { rest: overlay, handles } = overlayWithHandles
|
|
2521
|
+
? splitFloorplanHandles(overlayWithHandles)
|
|
2522
|
+
: { rest: null, handles: null }
|
|
2523
|
+
const entry: CacheEntry = { deps, base, overlay, handles, node: effectiveNode }
|
|
2439
2524
|
geometryCache.set(nodeId, entry)
|
|
2440
2525
|
return entry
|
|
2441
2526
|
}
|
|
@@ -2888,12 +2973,13 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
|
|
|
2888
2973
|
// body (matches the 3D `HANDLE_OFFSET`).
|
|
2889
2974
|
const bi = 0.03 // base inset
|
|
2890
2975
|
const arrowD = `M ${bi},${-sh} L ${bi + sl},${-sh} L ${bi + sl},${-hh} L ${bi + sl + hl},0 L ${bi + sl},${hh} L ${bi + sl},${sh} L ${bi},${sh} Z`
|
|
2891
|
-
// Indigo palette to match the 3D
|
|
2976
|
+
// Indigo palette to match the 3D wall push arrows
|
|
2892
2977
|
// (`ARROW_COLOR` / `ARROW_HOVER_COLOR`) and the corner-sphere
|
|
2893
2978
|
// accent in `floating-action-menu.tsx`.
|
|
2894
2979
|
const fill = isHovered ? '#a5b4fc' : '#8381ed'
|
|
2895
2980
|
const angleDeg = (g.angle * 180) / Math.PI
|
|
2896
|
-
const cursor =
|
|
2981
|
+
const cursor =
|
|
2982
|
+
g.affordance === WALL_PUSH_AFFORDANCE ? 'grab' : g.affordance ? 'ew-resize' : 'move'
|
|
2897
2983
|
const affordance = g.affordance
|
|
2898
2984
|
const payload = g.payload
|
|
2899
2985
|
// No hover-grow: a scaling transform would enlarge the hit area
|
|
@@ -3334,15 +3420,26 @@ export function isFloorplanHierarchyVisible(
|
|
|
3334
3420
|
liveOverrides: Map<string, LiveNodeOverrides>,
|
|
3335
3421
|
rootId: AnyNodeId,
|
|
3336
3422
|
): boolean {
|
|
3423
|
+
// The root is checked on its own because a site-scoped node can be declared
|
|
3424
|
+
// on the Site without a `parentId` link. A Site's flag never reaches the
|
|
3425
|
+
// nodes on it; see `hidesDescendants`.
|
|
3337
3426
|
const root = nodes[rootId]
|
|
3338
|
-
if (
|
|
3427
|
+
if (
|
|
3428
|
+
root &&
|
|
3429
|
+
root.id !== node.id &&
|
|
3430
|
+
hidesDescendants(root) &&
|
|
3431
|
+
!isFloorplanNodeVisible(root, liveOverrides.get(root.id))
|
|
3432
|
+
) {
|
|
3433
|
+
return false
|
|
3434
|
+
}
|
|
3339
3435
|
|
|
3340
3436
|
let current: AnyNode | undefined = node
|
|
3341
3437
|
const seen = new Set<AnyNodeId>()
|
|
3342
3438
|
while (current) {
|
|
3343
3439
|
if (seen.has(current.id)) return true
|
|
3344
3440
|
seen.add(current.id)
|
|
3345
|
-
|
|
3441
|
+
const reaches = current.id === node.id || hidesDescendants(current)
|
|
3442
|
+
if (reaches && !isFloorplanNodeVisible(current, liveOverrides.get(current.id))) return false
|
|
3346
3443
|
if (current.id === rootId) return true
|
|
3347
3444
|
const parentId = current.parentId as AnyNodeId | null
|
|
3348
3445
|
if (!parentId) return true
|
|
@@ -3469,6 +3566,42 @@ export function splitFloorplanOverlay(g: FloorplanGeometry): {
|
|
|
3469
3566
|
return { base: g, overlay: null }
|
|
3470
3567
|
}
|
|
3471
3568
|
|
|
3569
|
+
/** The kinds a pointer grabs: they paint and hit-test above every node's outline. */
|
|
3570
|
+
const HANDLE_KINDS = new Set<FloorplanGeometry['kind']>([
|
|
3571
|
+
'endpoint-handle',
|
|
3572
|
+
'midpoint-handle',
|
|
3573
|
+
'edge-handle',
|
|
3574
|
+
'move-handle',
|
|
3575
|
+
'move-arrow',
|
|
3576
|
+
'rotate-arrow',
|
|
3577
|
+
])
|
|
3578
|
+
|
|
3579
|
+
/**
|
|
3580
|
+
* Splits an overlay tree into its grabbable handles and the rest (labels,
|
|
3581
|
+
* dimensions, outlines). The handles render in a pass of their own after
|
|
3582
|
+
* every node's overlay: an outline painted later (a neighbouring wall's body)
|
|
3583
|
+
* would otherwise cover a selected wall's corner handle and take its press —
|
|
3584
|
+
* the 2D twin of the 3D handle hit priority.
|
|
3585
|
+
*/
|
|
3586
|
+
export function splitFloorplanHandles(g: FloorplanGeometry): {
|
|
3587
|
+
rest: FloorplanGeometry | null
|
|
3588
|
+
handles: FloorplanGeometry | null
|
|
3589
|
+
} {
|
|
3590
|
+
if (HANDLE_KINDS.has(g.kind)) return { rest: null, handles: g }
|
|
3591
|
+
if (g.kind !== 'group') return { rest: g, handles: null }
|
|
3592
|
+
const rest: FloorplanGeometry[] = []
|
|
3593
|
+
const handles: FloorplanGeometry[] = []
|
|
3594
|
+
for (const child of g.children) {
|
|
3595
|
+
const split = splitFloorplanHandles(child)
|
|
3596
|
+
if (split.rest) rest.push(split.rest)
|
|
3597
|
+
if (split.handles) handles.push(split.handles)
|
|
3598
|
+
}
|
|
3599
|
+
return {
|
|
3600
|
+
rest: rest.length ? { ...g, children: rest } : null,
|
|
3601
|
+
handles: handles.length ? { ...g, children: handles } : null,
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3472
3605
|
/**
|
|
3473
3606
|
* Per-node edit chrome hidden while a multi-selection is active: the group is
|
|
3474
3607
|
* manipulated as one rigid piece (drag to move, R/T to rotate), so individual
|
|
@@ -3657,29 +3790,6 @@ function depsValueEqual(a: unknown, b: unknown): boolean {
|
|
|
3657
3790
|
*/
|
|
3658
3791
|
export { floorplanLayerRank }
|
|
3659
3792
|
|
|
3660
|
-
function deepEqual(a: unknown, b: unknown): boolean {
|
|
3661
|
-
if (a === b) return true
|
|
3662
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
3663
|
-
if (a.length !== b.length) return false
|
|
3664
|
-
for (let i = 0; i < a.length; i++) {
|
|
3665
|
-
if (!deepEqual(a[i], b[i])) return false
|
|
3666
|
-
}
|
|
3667
|
-
return true
|
|
3668
|
-
}
|
|
3669
|
-
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
|
3670
|
-
const aKeys = Object.keys(a as Record<string, unknown>)
|
|
3671
|
-
const bKeys = Object.keys(b as Record<string, unknown>)
|
|
3672
|
-
if (aKeys.length !== bKeys.length) return false
|
|
3673
|
-
for (const key of aKeys) {
|
|
3674
|
-
if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
|
|
3675
|
-
return false
|
|
3676
|
-
}
|
|
3677
|
-
}
|
|
3678
|
-
return true
|
|
3679
|
-
}
|
|
3680
|
-
return false
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
3793
|
const ROTATION_WEDGE_COLOR = '#8381ed'
|
|
3684
3794
|
const ROTATION_WEDGE_SEGMENTS = 48
|
|
3685
3795
|
|