@pascal-app/editor 1.0.3 → 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 +11 -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 +202 -57
- 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 +36 -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 +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -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 +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- 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 +80 -1
- 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 +43 -142
- 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-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-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- 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-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- 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 +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- 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/move-registry-node-tool.tsx +3 -1
- 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/cursor-sphere.tsx +36 -12
- 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 +46 -20
- 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 +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- 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/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 +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- 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-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 +4 -4
- 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/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/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- 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.tsx +5 -6
- 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 +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- 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 +246 -39
- 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/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 +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -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 +59 -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 +485 -0
- package/src/lib/glb-export.ts +237 -119
- 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 +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- 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 +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- 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/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 +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- 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 +24 -36
- package/src/lib/scene.ts +33 -57
- 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 +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -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/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/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 +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- 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
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
resolveBuildingForLevel,
|
|
24
24
|
resolveSelectionProxyId,
|
|
25
25
|
resumeSceneHistory,
|
|
26
|
+
type SlabNode,
|
|
26
27
|
useInteractive,
|
|
27
28
|
useLiveNodeOverrides,
|
|
28
29
|
useLiveTransforms,
|
|
@@ -43,6 +44,14 @@ import {
|
|
|
43
44
|
} from 'react'
|
|
44
45
|
import { useShallow } from 'zustand/react/shallow'
|
|
45
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'
|
|
46
55
|
import { ROTATE_HANDLE_DRAG_LABEL } from '../../../lib/contextual-help'
|
|
47
56
|
import {
|
|
48
57
|
canDirectRotateNode,
|
|
@@ -53,6 +62,7 @@ import {
|
|
|
53
62
|
snapDirectRotationDelta,
|
|
54
63
|
} from '../../../lib/direct-manipulation'
|
|
55
64
|
import { createEditorApi } from '../../../lib/editor-api'
|
|
65
|
+
import { isRoomOwnedPlate, roomOwnedPlateDrillTarget } from '../../../lib/floor-footprints'
|
|
56
66
|
import {
|
|
57
67
|
type FloorplanAnnotationVisibility,
|
|
58
68
|
filterFloorplanAnnotationGeometry,
|
|
@@ -86,7 +96,15 @@ import {
|
|
|
86
96
|
isIdle,
|
|
87
97
|
tangentReshapeScope,
|
|
88
98
|
} from '../../../lib/interaction/scope'
|
|
89
|
-
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'
|
|
90
108
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
91
109
|
import { clearSurfacePlanSnapFeedback } from '../../../lib/surface-plan-snap'
|
|
92
110
|
import { paintZoneMembership } from '../../../lib/units'
|
|
@@ -101,8 +119,13 @@ import useInteractionScope, {
|
|
|
101
119
|
useMovingNode,
|
|
102
120
|
} from '../../../store/use-interaction-scope'
|
|
103
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'
|
|
104
124
|
import { startGroupPickUp } from '../../editor/group-actions'
|
|
105
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'
|
|
106
129
|
import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
|
|
107
130
|
import {
|
|
108
131
|
FloorplanGroupSelectionBox,
|
|
@@ -120,6 +143,7 @@ import {
|
|
|
120
143
|
resolveSvgAnnotationCollisions,
|
|
121
144
|
svgAnnotationLabelId,
|
|
122
145
|
} from './floorplan-annotation-layout'
|
|
146
|
+
import { FloorplanDimensionEditOverlay } from './floorplan-dimension-edit-overlay'
|
|
123
147
|
import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
|
|
124
148
|
import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
|
|
125
149
|
import {
|
|
@@ -179,22 +203,19 @@ const ScaleAwareFloorplanGroupSelectionBox = memo(function ScaleAwareFloorplanGr
|
|
|
179
203
|
return <FloorplanGroupSelectionBox {...props} unitsPerPixel={unitsPerPixel} />
|
|
180
204
|
})
|
|
181
205
|
|
|
182
|
-
/**
|
|
183
|
-
* Snapshot of node fields captured at drag-start, used by the single-undo
|
|
184
|
-
* dance to revert untracked before re-applying as a single tracked
|
|
185
|
-
* change. The dispatcher only knows about the `affectedIds` the
|
|
186
|
-
* affordance declares; it captures whatever fields exist on each node by
|
|
187
|
-
* cloning the full record minus the registry-managed `id` / `type`.
|
|
188
|
-
*/
|
|
189
|
-
type NodeSnapshot = { id: AnyNodeId; data: Record<string, unknown> }
|
|
190
|
-
|
|
191
206
|
type ActiveDrag = {
|
|
192
207
|
pointerId: number
|
|
193
208
|
captureTarget: Element
|
|
194
209
|
/** Key for the visual `active` flag — e.g. `${nodeId}:${endpoint}`. */
|
|
195
210
|
handleId: string
|
|
196
211
|
session: FloorplanAffordanceSession
|
|
197
|
-
|
|
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
|
|
198
219
|
historyPaused: boolean
|
|
199
220
|
/**
|
|
200
221
|
* Last plan point handed to `session.apply` (the grab point until the first
|
|
@@ -218,7 +239,6 @@ type ActiveDrag = {
|
|
|
218
239
|
}
|
|
219
240
|
|
|
220
241
|
type FloorplanAffordanceCancelEffects = {
|
|
221
|
-
restoreSnapshots: (snapshots: NodeSnapshot[]) => void
|
|
222
242
|
resumeHistory: () => void
|
|
223
243
|
clearPreview: (id: AnyNodeId) => void
|
|
224
244
|
clearSnapFeedback: () => void
|
|
@@ -242,7 +262,7 @@ export function cancelFloorplanAffordanceDrag(
|
|
|
242
262
|
drag.captureTarget.releasePointerCapture?.(drag.pointerId)
|
|
243
263
|
}
|
|
244
264
|
|
|
245
|
-
|
|
265
|
+
drag.writes.revert()
|
|
246
266
|
if (drag.historyPaused) {
|
|
247
267
|
effects.resumeHistory()
|
|
248
268
|
drag.historyPaused = false
|
|
@@ -378,6 +398,7 @@ type CacheEntry = {
|
|
|
378
398
|
deps: NodeDeps
|
|
379
399
|
base: FloorplanGeometry | null
|
|
380
400
|
overlay: FloorplanGeometry | null
|
|
401
|
+
handles: FloorplanGeometry | null
|
|
381
402
|
node: AnyNode
|
|
382
403
|
}
|
|
383
404
|
|
|
@@ -433,7 +454,7 @@ type FloorplanLevelDataHook = (args: {
|
|
|
433
454
|
nodes: Record<string, AnyNode>
|
|
434
455
|
}) => unknown
|
|
435
456
|
|
|
436
|
-
type FloorplanRenderPass = 'base' | 'overlay'
|
|
457
|
+
type FloorplanRenderPass = 'base' | 'overlay' | 'handles'
|
|
437
458
|
|
|
438
459
|
const POINTER_CURSOR_STYLE = { cursor: 'pointer' } as const
|
|
439
460
|
// Group members advertise the drag-to-move-the-selection gesture.
|
|
@@ -441,18 +462,16 @@ const MOVE_CURSOR_STYLE = { cursor: 'move' } as const
|
|
|
441
462
|
const NO_POINTER_EVENTS_STYLE = { pointerEvents: 'none' } as const
|
|
442
463
|
|
|
443
464
|
export function isFloorplanOpeningPlacementState({
|
|
444
|
-
phase,
|
|
445
465
|
mode,
|
|
446
466
|
tool,
|
|
447
467
|
movingNodeHasWallOpeningPlacement,
|
|
448
468
|
}: {
|
|
449
|
-
phase: string
|
|
450
469
|
mode: string
|
|
451
470
|
tool: string | null
|
|
452
471
|
movingNodeHasWallOpeningPlacement: boolean
|
|
453
472
|
}): boolean {
|
|
454
473
|
return (
|
|
455
|
-
(
|
|
474
|
+
(mode === 'build' && (tool === 'door' || tool === 'window')) ||
|
|
456
475
|
movingNodeHasWallOpeningPlacement
|
|
457
476
|
)
|
|
458
477
|
}
|
|
@@ -471,12 +490,11 @@ export function floorplanEntryYieldsToTool(state: {
|
|
|
471
490
|
}
|
|
472
491
|
|
|
473
492
|
function floorplanEntryYieldsToToolNow(): boolean {
|
|
474
|
-
const {
|
|
493
|
+
const { mode, tool } = useEditor.getState()
|
|
475
494
|
const movingNode = getMovingNode()
|
|
476
495
|
return floorplanEntryYieldsToTool({
|
|
477
496
|
mode,
|
|
478
497
|
openingPlacement: isFloorplanOpeningPlacementState({
|
|
479
|
-
phase,
|
|
480
498
|
mode,
|
|
481
499
|
tool,
|
|
482
500
|
movingNodeHasWallOpeningPlacement:
|
|
@@ -486,21 +504,6 @@ function floorplanEntryYieldsToToolNow(): boolean {
|
|
|
486
504
|
})
|
|
487
505
|
}
|
|
488
506
|
|
|
489
|
-
function snapshotNode(node: AnyNode): NodeSnapshot {
|
|
490
|
-
// Shallow-clone every non-id, non-type field. Arrays / vec tuples are
|
|
491
|
-
// deep-cloned to detach from the live store reference.
|
|
492
|
-
const data: Record<string, unknown> = {}
|
|
493
|
-
for (const [key, value] of Object.entries(node)) {
|
|
494
|
-
if (key === 'id' || key === 'type' || key === 'object' || key === 'parentId') continue
|
|
495
|
-
data[key] = Array.isArray(value) ? [...(value as unknown[])] : value
|
|
496
|
-
}
|
|
497
|
-
return { id: node.id, data }
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function snapshotsToUpdates(snapshots: NodeSnapshot[]) {
|
|
501
|
-
return snapshots.map((s) => ({ id: s.id, data: s.data }))
|
|
502
|
-
}
|
|
503
|
-
|
|
504
507
|
// Stable empty sentinel used by per-entry builders while the floor plan is
|
|
505
508
|
// hidden; committed scene edits still flow through `useScene`.
|
|
506
509
|
const EMPTY_LIVE_OVERRIDES: Map<string, LiveNodeOverrides> = new Map()
|
|
@@ -564,6 +567,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
564
567
|
}, [selectedLevelId, ambientBuildingSourceId, nodes])
|
|
565
568
|
|
|
566
569
|
const levelId = selectedLevelId ?? ambientLevelId
|
|
570
|
+
useRoomRecords(levelId)
|
|
567
571
|
const isAmbient = !selectedLevelId && !!ambientLevelId
|
|
568
572
|
const activeBuildingId = useMemo(
|
|
569
573
|
() => (levelId ? resolveBuildingForLevel(levelId as AnyNodeId, nodes) : null),
|
|
@@ -584,19 +588,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
584
588
|
// `handleClickStop` / `handleSelect`, so the tool never sees it. Pass
|
|
585
589
|
// clicks through in that case (`floorplanEntryYieldsToToolNow`).
|
|
586
590
|
const editorMode = useEditor((s) => s.mode)
|
|
587
|
-
const structureLayer = useEditor((s) => s.structureLayer)
|
|
588
591
|
const floorplanSelectionTool = useEditor((s) => s.floorplanSelectionTool)
|
|
589
592
|
const endpointReshape = useEndpointReshape()
|
|
590
593
|
const isMarqueeSelectionActive =
|
|
591
594
|
editorMode === 'select' &&
|
|
592
595
|
floorplanSelectionTool === 'marquee' &&
|
|
593
|
-
structureLayer !== 'zones' &&
|
|
594
596
|
!movingNode &&
|
|
595
597
|
!endpointReshape
|
|
596
598
|
// While the floor plan is not on screen (pure 3D view), per-entry live
|
|
597
599
|
// selectors freeze to `undefined` so drag publishes do not re-render the
|
|
598
600
|
// hidden floor-plan tree.
|
|
599
|
-
const floorplanVisible = useEditor((s) => s
|
|
601
|
+
const floorplanVisible = useEditor((s) => isViewVisible(s, VIEW_2D))
|
|
600
602
|
const drawingType = useDrawingView((s) => s.drawingType)
|
|
601
603
|
const annotationVisibility = useFloorplanAnnotationVisibility((s) => s.visibility)
|
|
602
604
|
const wallDimensionReference = useFloorplanAnnotationVisibility((s) => s.wallDimensionReference)
|
|
@@ -697,6 +699,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
697
699
|
swallowNextClick(200)
|
|
698
700
|
return
|
|
699
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
|
+
}
|
|
700
713
|
const currentSelectedIds = useViewer.getState().selection.selectedIds
|
|
701
714
|
let nextSelectedIds: string[]
|
|
702
715
|
if (options.shouldToggle) {
|
|
@@ -754,13 +767,13 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
754
767
|
)
|
|
755
768
|
|
|
756
769
|
const handleClickStop = useCallback((event: React.MouseEvent<SVGGElement>) => {
|
|
757
|
-
if (floorplanEntryYieldsToToolNow()) return
|
|
770
|
+
if (floorplanEntryYieldsToToolNow() || !isIdle(useInteractionScope.getState().scope)) return
|
|
758
771
|
event.stopPropagation()
|
|
759
772
|
}, [])
|
|
760
773
|
|
|
761
774
|
const startDirectMoveDrag = useCallback(
|
|
762
775
|
(id: AnyNodeId, event: ReactPointerEvent<SVGGElement>): boolean => {
|
|
763
|
-
if (event.button !== 0) return false
|
|
776
|
+
if (event.button !== 0 || event.altKey) return false
|
|
764
777
|
|
|
765
778
|
const node = useScene.getState().nodes[id]
|
|
766
779
|
if (!node || !isRegistryMovable(node.type)) return false
|
|
@@ -1015,7 +1028,35 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1015
1028
|
// the stores at event time. Commit clears the interaction scope before
|
|
1016
1029
|
// React paints the next frame; a render-time `undefined` handler leaves
|
|
1017
1030
|
// a short dead zone where the first post-placement selection is lost.
|
|
1018
|
-
if (floorplanEntryYieldsToToolNow()) return
|
|
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
|
+
}
|
|
1019
1060
|
if (startDirectMoveDrag(id, event)) return
|
|
1020
1061
|
if (startDirectRotateDrag(id, event)) return
|
|
1021
1062
|
if (startGroupMoveDrag(id, event)) return
|
|
@@ -1241,6 +1282,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1241
1282
|
if (event.button !== 0) return
|
|
1242
1283
|
if (movingNode) return
|
|
1243
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
|
+
|
|
1244
1295
|
const sceneNodes = useScene.getState().nodes
|
|
1245
1296
|
const node = sceneNodes[nodeId]
|
|
1246
1297
|
if (!node) return
|
|
@@ -1266,13 +1317,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1266
1317
|
sceneApi: createSceneApi(useScene),
|
|
1267
1318
|
})
|
|
1268
1319
|
|
|
1269
|
-
const snapshots: NodeSnapshot[] = []
|
|
1270
|
-
for (const id of session.affectedIds) {
|
|
1271
|
-
const n = sceneNodes[id]
|
|
1272
|
-
if (n) snapshots.push(snapshotNode(n))
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
1320
|
pauseSceneHistory(useScene)
|
|
1321
|
+
const writes = createSessionWrites()
|
|
1322
|
+
writes.watch(session.affectedIds)
|
|
1276
1323
|
|
|
1277
1324
|
// Rotation readout setup. The wedge radius tracks the grab distance
|
|
1278
1325
|
// from the pivot (≈ the handle's orbit), nudged inward so the swept
|
|
@@ -1304,7 +1351,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1304
1351
|
captureTarget,
|
|
1305
1352
|
handleId,
|
|
1306
1353
|
session,
|
|
1307
|
-
|
|
1354
|
+
writes,
|
|
1308
1355
|
historyPaused: true,
|
|
1309
1356
|
lastPlanPoint: initialPlanPoint,
|
|
1310
1357
|
rotation,
|
|
@@ -1337,8 +1384,6 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1337
1384
|
cancelFloorplanAffordanceDrag(
|
|
1338
1385
|
dragRef,
|
|
1339
1386
|
{
|
|
1340
|
-
restoreSnapshots: (snapshots) =>
|
|
1341
|
-
useScene.getState().updateNodes(snapshotsToUpdates(snapshots)),
|
|
1342
1387
|
resumeHistory: () => resumeSceneHistory(useScene),
|
|
1343
1388
|
clearPreview: (id) => {
|
|
1344
1389
|
useLiveNodeOverrides.getState().clear(id)
|
|
@@ -1364,15 +1409,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1364
1409
|
if (!planPoint) return
|
|
1365
1410
|
|
|
1366
1411
|
drag.lastPlanPoint = planPoint
|
|
1367
|
-
drag.
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
+
)
|
|
1376
1423
|
|
|
1377
1424
|
// Live rotation readout. Sweep from the bearing at grab to the
|
|
1378
1425
|
// current pointer bearing around the pivot — the same measurement
|
|
@@ -1409,11 +1456,11 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1409
1456
|
// Sessions with a `commit` hook own their atomic write (e.g.
|
|
1410
1457
|
// affordances that publish to `useLiveNodeOverrides` during
|
|
1411
1458
|
// `apply()` and never touch scene mid-drag). Mirrors the move
|
|
1412
|
-
// overlay's `session.commit` path —
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
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.
|
|
1415
1462
|
if (commitValid && drag.session.commit) {
|
|
1416
|
-
|
|
1463
|
+
drag.writes.revert()
|
|
1417
1464
|
if (drag.historyPaused) {
|
|
1418
1465
|
resumeSceneHistory(useScene)
|
|
1419
1466
|
drag.historyPaused = false
|
|
@@ -1430,37 +1477,23 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1430
1477
|
}
|
|
1431
1478
|
|
|
1432
1479
|
// Legacy compatibility for sessions that still wrote preview state into
|
|
1433
|
-
// `useScene` during `apply()`:
|
|
1434
|
-
//
|
|
1435
|
-
// `commit()` hook and preview through live overrides/transforms
|
|
1436
|
-
const
|
|
1437
|
-
const
|
|
1438
|
-
for (const snap of drag.snapshots) {
|
|
1439
|
-
const current = sceneNodes[snap.id]
|
|
1440
|
-
if (!current) continue
|
|
1441
|
-
const data: Record<string, unknown> = {}
|
|
1442
|
-
let changed = false
|
|
1443
|
-
for (const [key, before] of Object.entries(snap.data)) {
|
|
1444
|
-
const after = (current as unknown as Record<string, unknown>)[key]
|
|
1445
|
-
if (!deepEqual(before, after)) {
|
|
1446
|
-
data[key] = Array.isArray(after) ? [...(after as unknown[])] : after
|
|
1447
|
-
changed = true
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
if (changed) finalUpdates.push({ id: snap.id, data })
|
|
1451
|
-
}
|
|
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
|
|
1452
1485
|
|
|
1453
|
-
if (commitValid &&
|
|
1486
|
+
if (commitValid && changed) {
|
|
1454
1487
|
// Legacy single-undo dance (mirrors the old 3D move-endpoint-tool):
|
|
1455
|
-
// 1.
|
|
1488
|
+
// 1. Take back the session's writes while history is still paused.
|
|
1456
1489
|
// 2. Resume history.
|
|
1457
|
-
// 3. Re-apply
|
|
1458
|
-
|
|
1490
|
+
// 3. Re-apply them — recorded as one tracked change.
|
|
1491
|
+
drag.writes.revert()
|
|
1459
1492
|
if (drag.historyPaused) {
|
|
1460
1493
|
resumeSceneHistory(useScene)
|
|
1461
1494
|
drag.historyPaused = false
|
|
1462
1495
|
}
|
|
1463
|
-
useScene.getState().
|
|
1496
|
+
useScene.getState().applyNodeChanges(changes)
|
|
1464
1497
|
commitPerfAction()
|
|
1465
1498
|
sfxEmitter.emit('sfx:structure-build')
|
|
1466
1499
|
} else {
|
|
@@ -1468,7 +1501,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1468
1501
|
// resume without committing. Also clear any live overrides
|
|
1469
1502
|
// the session published (no-op when the session writes to
|
|
1470
1503
|
// scene directly).
|
|
1471
|
-
|
|
1504
|
+
drag.writes.revert()
|
|
1472
1505
|
if (drag.historyPaused) {
|
|
1473
1506
|
resumeSceneHistory(useScene)
|
|
1474
1507
|
drag.historyPaused = false
|
|
@@ -1504,15 +1537,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1504
1537
|
) {
|
|
1505
1538
|
return
|
|
1506
1539
|
}
|
|
1507
|
-
drag.
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
+
)
|
|
1516
1551
|
}
|
|
1517
1552
|
|
|
1518
1553
|
window.addEventListener('pointermove', onPointerMove)
|
|
@@ -1542,6 +1577,48 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1542
1577
|
|
|
1543
1578
|
const palette = renderCtx?.palette
|
|
1544
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
|
+
|
|
1545
1622
|
return (
|
|
1546
1623
|
// The outer wrapper stops `click` events that escape an entry's
|
|
1547
1624
|
// `onClick={handleClickStop}`. The base+overlay split means
|
|
@@ -1567,48 +1644,10 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1567
1644
|
strokes, hatches). Lower-rank kinds (zones) paint first so
|
|
1568
1645
|
higher-rank kinds (slabs, then walls / items / shelves) layer
|
|
1569
1646
|
on top in the expected document-order z-stack. */}
|
|
1570
|
-
<g className="floorplan-registry-base">
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
annotationVisibility={annotationVisibility}
|
|
1575
|
-
floorplanMode={floorplanMode}
|
|
1576
|
-
floorplanVisible={floorplanVisible}
|
|
1577
|
-
geometryCacheRef={geometryCacheRef}
|
|
1578
|
-
hatchPatternId={renderCtx?.hatchPatternId}
|
|
1579
|
-
hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
|
|
1580
|
-
interactiveElevators={interactiveElevators}
|
|
1581
|
-
isMarqueeSelectionActive={isMarqueeSelectionActive}
|
|
1582
|
-
key={`base-${entry.id}`}
|
|
1583
|
-
levelDataCacheRef={levelDataCacheRef}
|
|
1584
|
-
levelNodeIdsByType={floorplanData.levelNodeIdsByType}
|
|
1585
|
-
moving={movingNode?.id === entry.id}
|
|
1586
|
-
node={entry.node}
|
|
1587
|
-
nodeId={entry.id}
|
|
1588
|
-
nodes={nodes}
|
|
1589
|
-
onClickStop={handleClickStop}
|
|
1590
|
-
onEntryPointerDown={handleEntryPointerDown}
|
|
1591
|
-
onGroupMovePointerDown={handleGroupMoveHandlePointerDown}
|
|
1592
|
-
onHandleHoverChange={setHoveredHandleId}
|
|
1593
|
-
onHandleDoubleClick={commitAffordanceAction}
|
|
1594
|
-
onHandlePointerDown={startAffordanceDrag}
|
|
1595
|
-
palette={palette}
|
|
1596
|
-
unitFocus={unitFocus}
|
|
1597
|
-
pass="base"
|
|
1598
|
-
sceneRotationDeg={sceneRotationDeg}
|
|
1599
|
-
setMovingNode={setMovingNode}
|
|
1600
|
-
setMovingNodeOrigin={setMovingNodeOrigin}
|
|
1601
|
-
siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
|
|
1602
|
-
unit={unit}
|
|
1603
|
-
metricNotation={metricNotation}
|
|
1604
|
-
wallDimensionReference={effectiveWallDimensionReference}
|
|
1605
|
-
visibilityRootId={
|
|
1606
|
-
entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
|
|
1607
|
-
}
|
|
1608
|
-
ctxOverrides={entry.ctxOverrides}
|
|
1609
|
-
/>
|
|
1610
|
-
))}
|
|
1611
|
-
</g>
|
|
1647
|
+
<g className="floorplan-registry-base">{renderEntries('base')}</g>
|
|
1648
|
+
<RoomHighlight2D levelId={levelId} />
|
|
1649
|
+
<RoomLabels2D levelId={levelId} />
|
|
1650
|
+
<FloorRegionControls2D levelId={levelId} />
|
|
1612
1651
|
{/* Overlay pass — interactive handles (vertex / midpoint / edge /
|
|
1613
1652
|
move) and labels (text / dimensions). Painted after every base
|
|
1614
1653
|
entry so polygon-editor chrome on a selected slab stays above
|
|
@@ -1616,49 +1655,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1616
1655
|
slab + wall geometry sitting on top of the zone. Each overlay
|
|
1617
1656
|
still routes through the same selection-handling `<g>` so a
|
|
1618
1657
|
click on a zone's name selects the zone. */}
|
|
1619
|
-
<g className="floorplan-registry-overlay">
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
floorplanVisible={floorplanVisible}
|
|
1626
|
-
geometryCacheRef={geometryCacheRef}
|
|
1627
|
-
hatchPatternId={renderCtx?.hatchPatternId}
|
|
1628
|
-
hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
|
|
1629
|
-
interactiveElevators={interactiveElevators}
|
|
1630
|
-
isMarqueeSelectionActive={isMarqueeSelectionActive}
|
|
1631
|
-
key={`overlay-${entry.id}`}
|
|
1632
|
-
levelDataCacheRef={levelDataCacheRef}
|
|
1633
|
-
levelNodeIdsByType={floorplanData.levelNodeIdsByType}
|
|
1634
|
-
moving={movingNode?.id === entry.id}
|
|
1635
|
-
node={entry.node}
|
|
1636
|
-
nodeId={entry.id}
|
|
1637
|
-
nodes={nodes}
|
|
1638
|
-
onClickStop={handleClickStop}
|
|
1639
|
-
onEntryPointerDown={handleEntryPointerDown}
|
|
1640
|
-
onGroupMovePointerDown={handleGroupMoveHandlePointerDown}
|
|
1641
|
-
onHandleHoverChange={setHoveredHandleId}
|
|
1642
|
-
onHandleDoubleClick={commitAffordanceAction}
|
|
1643
|
-
onHandlePointerDown={startAffordanceDrag}
|
|
1644
|
-
palette={palette}
|
|
1645
|
-
unitFocus={unitFocus}
|
|
1646
|
-
pass="overlay"
|
|
1647
|
-
sceneRotationDeg={sceneRotationDeg}
|
|
1648
|
-
setMovingNode={setMovingNode}
|
|
1649
|
-
setMovingNodeOrigin={setMovingNodeOrigin}
|
|
1650
|
-
siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
|
|
1651
|
-
unit={unit}
|
|
1652
|
-
metricNotation={metricNotation}
|
|
1653
|
-
wallDimensionReference={effectiveWallDimensionReference}
|
|
1654
|
-
visibilityRootId={
|
|
1655
|
-
entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
|
|
1656
|
-
}
|
|
1657
|
-
ctxOverrides={entry.ctxOverrides}
|
|
1658
|
-
/>
|
|
1659
|
-
))}
|
|
1660
|
-
</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} />
|
|
1661
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 />
|
|
1662
1668
|
{/* Dashed group bbox — shows what a group drag carries along while a
|
|
1663
1669
|
multi-selection exists, rides the live delta mid-drag, and doubles
|
|
1664
1670
|
as the group's whole-area drag handle. */}
|
|
@@ -2023,16 +2029,24 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2023
2029
|
}: FloorplanRegistryEntryProps): React.ReactElement | null {
|
|
2024
2030
|
const selected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
2025
2031
|
const highlighted = useViewer((state) => state.previewSelectedIds.includes(nodeId))
|
|
2026
|
-
const
|
|
2032
|
+
const multiSelected = useViewer(
|
|
2027
2033
|
(state) =>
|
|
2028
2034
|
state.selection.selectedIds.length > 1 && state.selection.selectedIds.includes(nodeId),
|
|
2029
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
|
|
2030
2041
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
2031
2042
|
const selectionProxyId = resolveSelectionProxyId(
|
|
2032
2043
|
node,
|
|
2033
2044
|
nodes as Record<string, AnyNode | undefined>,
|
|
2034
2045
|
)
|
|
2035
|
-
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)
|
|
2036
2050
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
2037
2051
|
const referencedAnnotationRole = useViewer((state) =>
|
|
2038
2052
|
floorplanEntryReferencedAnnotationRole(node, nodes, new Set(state.selection.selectedIds)),
|
|
@@ -2041,7 +2055,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2041
2055
|
state.activeRotateNodeId === nodeId ? nodeId : null,
|
|
2042
2056
|
)
|
|
2043
2057
|
const groupMoveCursor =
|
|
2044
|
-
|
|
2058
|
+
multiSelected && classifyParticipant(node, selectedLevelId, nodes) !== null
|
|
2045
2059
|
const live = useLiveTransforms((s) => (floorplanVisible ? s.transforms.get(nodeId) : undefined))
|
|
2046
2060
|
const liveOverride = useLiveNodeOverrides((s) =>
|
|
2047
2061
|
floorplanVisible ? s.overrides.get(nodeId) : undefined,
|
|
@@ -2066,25 +2080,55 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2066
2080
|
|
|
2067
2081
|
// Mirror the sidebar tree nodes' hover wiring — `useViewer.hoveredId` drives
|
|
2068
2082
|
// the highlight halo in 3D as well as registry floor-plan hover strokes.
|
|
2069
|
-
const handlePointerEnter = useCallback(
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
currentNode
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
)
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
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
|
+
)
|
|
2080
2120
|
|
|
2081
2121
|
const handlePointerLeave = useCallback(() => {
|
|
2122
|
+
useEditor.getState().setHoveredRoom(null)
|
|
2082
2123
|
const node = useScene.getState().nodes[nodeId]
|
|
2083
2124
|
const targetId = node
|
|
2084
|
-
?
|
|
2085
|
-
|
|
2086
|
-
useScene.getState().nodes
|
|
2087
|
-
|
|
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
|
+
)
|
|
2088
2132
|
: nodeId
|
|
2089
2133
|
if (useViewer.getState().hoveredId === targetId) setHoveredId(null)
|
|
2090
2134
|
}, [nodeId, setHoveredId])
|
|
@@ -2160,7 +2204,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2160
2204
|
wallDimensionReference,
|
|
2161
2205
|
visibilityRootId,
|
|
2162
2206
|
})
|
|
2163
|
-
const rawGeometry = cacheEntry ?
|
|
2207
|
+
const rawGeometry = cacheEntry ? cacheEntry[pass] : null
|
|
2164
2208
|
const visibleGeometry = rawGeometry
|
|
2165
2209
|
? filterFloorplanAnnotationGeometry(rawGeometry, presentationVisibility)
|
|
2166
2210
|
: null
|
|
@@ -2168,7 +2212,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2168
2212
|
// dimension chrome (all of which live in the overlay pass) while keeping
|
|
2169
2213
|
// its highlighted body geometry.
|
|
2170
2214
|
const geometry =
|
|
2171
|
-
visibleGeometry && suppressHandles && pass
|
|
2215
|
+
visibleGeometry && suppressHandles && pass !== 'base'
|
|
2172
2216
|
? stripHandleChrome(visibleGeometry)
|
|
2173
2217
|
: visibleGeometry
|
|
2174
2218
|
if (!geometry) return null
|
|
@@ -2183,6 +2227,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2183
2227
|
onClick={entryClick}
|
|
2184
2228
|
onPointerDown={entryPointerDown}
|
|
2185
2229
|
onPointerEnter={handlePointerEnter}
|
|
2230
|
+
onPointerMove={handlePointerEnter}
|
|
2186
2231
|
onPointerLeave={handlePointerLeave}
|
|
2187
2232
|
style={groupMoveCursor ? MOVE_CURSOR_STYLE : POINTER_CURSOR_STYLE}
|
|
2188
2233
|
>
|
|
@@ -2401,6 +2446,7 @@ export function buildFloorplanEntryGeometry({
|
|
|
2401
2446
|
selected,
|
|
2402
2447
|
unit,
|
|
2403
2448
|
metricNotation,
|
|
2449
|
+
roomLabelOverlay: true,
|
|
2404
2450
|
wallDimensionReference,
|
|
2405
2451
|
highlighted,
|
|
2406
2452
|
hovered,
|
|
@@ -2426,6 +2472,7 @@ export function buildFloorplanEntryGeometry({
|
|
|
2426
2472
|
automaticDimensions,
|
|
2427
2473
|
metricNotation,
|
|
2428
2474
|
purpose: 'edit',
|
|
2475
|
+
roomLabelOverlay: true,
|
|
2429
2476
|
wallDimensionReference,
|
|
2430
2477
|
}),
|
|
2431
2478
|
viewState: palette
|
|
@@ -2467,10 +2514,13 @@ export function buildFloorplanEntryGeometry({
|
|
|
2467
2514
|
transform: ctxOverrides.outputTransform,
|
|
2468
2515
|
}
|
|
2469
2516
|
: unprojectedGeometry
|
|
2470
|
-
const { base, overlay } = geometry
|
|
2517
|
+
const { base, overlay: overlayWithHandles } = geometry
|
|
2471
2518
|
? splitFloorplanOverlay(geometry)
|
|
2472
2519
|
: { base: null, overlay: null }
|
|
2473
|
-
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 }
|
|
2474
2524
|
geometryCache.set(nodeId, entry)
|
|
2475
2525
|
return entry
|
|
2476
2526
|
}
|
|
@@ -2923,12 +2973,13 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
|
|
|
2923
2973
|
// body (matches the 3D `HANDLE_OFFSET`).
|
|
2924
2974
|
const bi = 0.03 // base inset
|
|
2925
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`
|
|
2926
|
-
// Indigo palette to match the 3D
|
|
2976
|
+
// Indigo palette to match the 3D wall push arrows
|
|
2927
2977
|
// (`ARROW_COLOR` / `ARROW_HOVER_COLOR`) and the corner-sphere
|
|
2928
2978
|
// accent in `floating-action-menu.tsx`.
|
|
2929
2979
|
const fill = isHovered ? '#a5b4fc' : '#8381ed'
|
|
2930
2980
|
const angleDeg = (g.angle * 180) / Math.PI
|
|
2931
|
-
const cursor =
|
|
2981
|
+
const cursor =
|
|
2982
|
+
g.affordance === WALL_PUSH_AFFORDANCE ? 'grab' : g.affordance ? 'ew-resize' : 'move'
|
|
2932
2983
|
const affordance = g.affordance
|
|
2933
2984
|
const payload = g.payload
|
|
2934
2985
|
// No hover-grow: a scaling transform would enlarge the hit area
|
|
@@ -3515,6 +3566,42 @@ export function splitFloorplanOverlay(g: FloorplanGeometry): {
|
|
|
3515
3566
|
return { base: g, overlay: null }
|
|
3516
3567
|
}
|
|
3517
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
|
+
|
|
3518
3605
|
/**
|
|
3519
3606
|
* Per-node edit chrome hidden while a multi-selection is active: the group is
|
|
3520
3607
|
* manipulated as one rigid piece (drag to move, R/T to rotate), so individual
|
|
@@ -3703,29 +3790,6 @@ function depsValueEqual(a: unknown, b: unknown): boolean {
|
|
|
3703
3790
|
*/
|
|
3704
3791
|
export { floorplanLayerRank }
|
|
3705
3792
|
|
|
3706
|
-
function deepEqual(a: unknown, b: unknown): boolean {
|
|
3707
|
-
if (a === b) return true
|
|
3708
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
3709
|
-
if (a.length !== b.length) return false
|
|
3710
|
-
for (let i = 0; i < a.length; i++) {
|
|
3711
|
-
if (!deepEqual(a[i], b[i])) return false
|
|
3712
|
-
}
|
|
3713
|
-
return true
|
|
3714
|
-
}
|
|
3715
|
-
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
|
3716
|
-
const aKeys = Object.keys(a as Record<string, unknown>)
|
|
3717
|
-
const bKeys = Object.keys(b as Record<string, unknown>)
|
|
3718
|
-
if (aKeys.length !== bKeys.length) return false
|
|
3719
|
-
for (const key of aKeys) {
|
|
3720
|
-
if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
|
|
3721
|
-
return false
|
|
3722
|
-
}
|
|
3723
|
-
}
|
|
3724
|
-
return true
|
|
3725
|
-
}
|
|
3726
|
-
return false
|
|
3727
|
-
}
|
|
3728
|
-
|
|
3729
3793
|
const ROTATION_WEDGE_COLOR = '#8381ed'
|
|
3730
3794
|
const ROTATION_WEDGE_SEGMENTS = 48
|
|
3731
3795
|
|