@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
|
@@ -6,20 +6,28 @@ import {
|
|
|
6
6
|
type AnyNodeId,
|
|
7
7
|
bboxAnchors,
|
|
8
8
|
bboxCornerAnchors,
|
|
9
|
+
beginSceneHistoryDraft,
|
|
10
|
+
beginSceneHistoryPauseSession,
|
|
11
|
+
cascadeDirty,
|
|
12
|
+
collectDescendants,
|
|
9
13
|
createSceneApi,
|
|
10
14
|
emitter,
|
|
11
15
|
type FloorplanMoveTargetSession,
|
|
12
16
|
type GroupMoveSnapResult,
|
|
17
|
+
getEffectiveNode,
|
|
13
18
|
type MovableConfig,
|
|
14
19
|
nodeRegistry,
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
runAsSingleSceneHistoryStep,
|
|
21
|
+
runSceneHistoryDraftWrite,
|
|
22
|
+
sceneHistoryDraftRevertUpdates,
|
|
23
|
+
settleSceneHistoryDrafts,
|
|
17
24
|
useLiveNodeOverrides,
|
|
18
25
|
useLiveTransforms,
|
|
19
26
|
useScene,
|
|
20
27
|
} from '@pascal-app/core'
|
|
28
|
+
import { nodeLevelFrame } from '@pascal-app/core/procedural-items'
|
|
21
29
|
import { useViewer } from '@pascal-app/viewer'
|
|
22
|
-
import { useEffect } from 'react'
|
|
30
|
+
import { useEffect, useMemo } from 'react'
|
|
23
31
|
import { commitFreshPlacementSubtree } from '../../lib/fresh-planar-placement'
|
|
24
32
|
import { isHistoryShortcut } from '../../lib/history'
|
|
25
33
|
import { isFreshPlacementMetadata, stripPlacementMetadataFlags } from '../../lib/placement-metadata'
|
|
@@ -30,6 +38,7 @@ import {
|
|
|
30
38
|
} from '../../lib/rigid-plan-svg-transform'
|
|
31
39
|
import { movementSfxStepKey } from '../../lib/sfx/movement-tick'
|
|
32
40
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
41
|
+
import { surfaceAttachmentId } from '../../lib/surface-attachment'
|
|
33
42
|
import { resolveAlignmentForFloorplanView } from '../../lib/world-grid-snap'
|
|
34
43
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
35
44
|
import useEditor, {
|
|
@@ -37,7 +46,11 @@ import useEditor, {
|
|
|
37
46
|
isGridSnapActive,
|
|
38
47
|
isMagneticSnapActive,
|
|
39
48
|
} from '../../store/use-editor'
|
|
40
|
-
import {
|
|
49
|
+
import useInteractionScope, {
|
|
50
|
+
isInteractionSubtreeDraft,
|
|
51
|
+
useMovingNode,
|
|
52
|
+
} from '../../store/use-interaction-scope'
|
|
53
|
+
import usePlacementPreview from '../../store/use-placement-preview'
|
|
41
54
|
import { useWallMoveGhosts } from '../../store/use-wall-move-ghosts'
|
|
42
55
|
|
|
43
56
|
// Figma-style alignment snap threshold. Meters in world space; 8cm gives
|
|
@@ -67,7 +80,14 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
67
80
|
* cursor → meters accounts for pan / zoom / building rotation.
|
|
68
81
|
*/
|
|
69
82
|
export function FloorplanRegistryMoveOverlay() {
|
|
70
|
-
const
|
|
83
|
+
const source = useMovingNode()
|
|
84
|
+
const movingNode = useMemo(
|
|
85
|
+
() =>
|
|
86
|
+
source && isInteractionSubtreeDraft(source.id)
|
|
87
|
+
? (useScene.getState().nodes[source.id] ?? source)
|
|
88
|
+
: source,
|
|
89
|
+
[source],
|
|
90
|
+
)
|
|
71
91
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
72
92
|
const setMovingNodeOrigin = useEditor((s) => s.setMovingNodeOrigin)
|
|
73
93
|
|
|
@@ -93,7 +113,15 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
93
113
|
return [m.x, m.y]
|
|
94
114
|
}
|
|
95
115
|
|
|
96
|
-
const isPointerOverFloorplanScene = (
|
|
116
|
+
const isPointerOverFloorplanScene = (event: PointerEvent): boolean => {
|
|
117
|
+
// View controls overlap the SVG viewport; clicking one must not place the draft.
|
|
118
|
+
if (
|
|
119
|
+
(event.target as Element | null)?.closest?.(
|
|
120
|
+
'button, input, select, textarea, [role="button"]',
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return false
|
|
124
|
+
const { clientX, clientY } = event
|
|
97
125
|
// The scene's `<g>` only covers painted SVG elements, so hovers over
|
|
98
126
|
// empty grid background often target the parent SVG. Bounds keep the
|
|
99
127
|
// cursor active anywhere inside the floor-plan viewport.
|
|
@@ -108,6 +136,8 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
108
136
|
)
|
|
109
137
|
}
|
|
110
138
|
|
|
139
|
+
const ownsSubtree = useInteractionScope.getState().adoptSubtree(movingNode.id)
|
|
140
|
+
|
|
111
141
|
// ── Path 1 — kind-owned `floorplanMoveTarget` ───────────────────
|
|
112
142
|
if (hasMoveTarget && def?.floorplanMoveTarget) {
|
|
113
143
|
const sceneNodes = useScene.getState().nodes
|
|
@@ -120,15 +150,55 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
120
150
|
}) => FloorplanMoveTargetSession
|
|
121
151
|
)({ node: movingNode, nodes: sceneNodes, sceneApi })
|
|
122
152
|
|
|
123
|
-
|
|
124
|
-
// so the single-undo dance has a clean baseline to revert to.
|
|
125
|
-
const snapshots = session.affectedIds
|
|
126
|
-
.map((id) => sceneNodes[id])
|
|
127
|
-
.filter((n): n is AnyNode => !!n)
|
|
128
|
-
.map((n) => snapshotNode(n))
|
|
153
|
+
const initialAffectedIds = session.affectedIds.filter((id) => sceneNodes[id])
|
|
129
154
|
|
|
130
|
-
|
|
155
|
+
// History is paused only around this overlay's own writes (`ownWrite`): every affected
|
|
156
|
+
// node is a carried draft (core's history-drafts), so writes others make mid-move record
|
|
157
|
+
// as their own steps and space detection reconciles them. The drop writes through a
|
|
158
|
+
// session keyed by the moving node, which the 3D mover shares in split view.
|
|
159
|
+
const draftEnds = initialAffectedIds.map((id) =>
|
|
160
|
+
beginSceneHistoryDraft(
|
|
161
|
+
id,
|
|
162
|
+
isFreshPlacementMetadata(sceneNodes[id]!.metadata) ? null : sceneNodes[id]!,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
131
165
|
let historyPaused = true
|
|
166
|
+
const endDrafts = () => {
|
|
167
|
+
for (const end of draftEnds) end()
|
|
168
|
+
historyPaused = false
|
|
169
|
+
}
|
|
170
|
+
const ownWrite = runSceneHistoryDraftWrite
|
|
171
|
+
// Puts back only the fields this move wrote and still holds; a rename or any other write
|
|
172
|
+
// someone made mid-move stays.
|
|
173
|
+
// The drop ends the carry: a transient marker (the 3D mover's, in split view) goes even
|
|
174
|
+
// when someone else edited the metadata meanwhile and the move no longer holds it.
|
|
175
|
+
const endTransientMarkers = () => {
|
|
176
|
+
const nodes = useScene.getState().nodes
|
|
177
|
+
const updates = session.affectedIds.flatMap((id) => {
|
|
178
|
+
const metadata = nodes[id]?.metadata as Record<string, unknown> | undefined
|
|
179
|
+
if (!metadata?.isTransient) return []
|
|
180
|
+
const { isTransient: _transient, ...rest } = metadata
|
|
181
|
+
return [{ id, data: { metadata: rest } }]
|
|
182
|
+
})
|
|
183
|
+
if (updates.length > 0) useScene.getState().updateNodes(updates)
|
|
184
|
+
}
|
|
185
|
+
const revertOwnWrites = () =>
|
|
186
|
+
ownWrite(() => {
|
|
187
|
+
const updates = sceneHistoryDraftRevertUpdates(session.affectedIds)
|
|
188
|
+
if (updates.length > 0) useScene.getState().updateNodes(updates)
|
|
189
|
+
})
|
|
190
|
+
const recordDrop = (write: () => void) => {
|
|
191
|
+
const drop = beginSceneHistoryPauseSession(useScene, { gesture: movingNode.id })
|
|
192
|
+
try {
|
|
193
|
+
// The move's write and its transient cleanup are one undo entry and one commit.
|
|
194
|
+
drop.commitStep(() => runAsSingleSceneHistoryStep(useScene, write))
|
|
195
|
+
// Only a successful drop ends ownership; a rejected write can still be retried.
|
|
196
|
+
settleSceneHistoryDrafts(session.affectedIds)
|
|
197
|
+
endDrafts()
|
|
198
|
+
} finally {
|
|
199
|
+
drop.end()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
132
202
|
|
|
133
203
|
const clearLivePreviews = () => {
|
|
134
204
|
const liveTransforms = useLiveTransforms.getState()
|
|
@@ -137,7 +207,12 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
137
207
|
for (const id of session.affectedIds) {
|
|
138
208
|
liveTransforms.clear(id)
|
|
139
209
|
liveOverrides.clear(id)
|
|
140
|
-
|
|
210
|
+
const api = createSceneApi(useScene)
|
|
211
|
+
for (const dirtyId of new Set([
|
|
212
|
+
...cascadeDirty(id, { scene: api }),
|
|
213
|
+
...collectDescendants(id, { scene: api }),
|
|
214
|
+
]))
|
|
215
|
+
scene.markDirty(dirtyId)
|
|
141
216
|
}
|
|
142
217
|
}
|
|
143
218
|
|
|
@@ -161,9 +236,29 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
161
236
|
// the 2D pane the overlay claimed R forever and the 3D flip went dead.
|
|
162
237
|
let pointerOverFloorplan = false
|
|
163
238
|
const onPointerTrack = (event: PointerEvent) => {
|
|
164
|
-
pointerOverFloorplan = isPointerOverFloorplanScene(event
|
|
239
|
+
pointerOverFloorplan = isPointerOverFloorplanScene(event)
|
|
165
240
|
}
|
|
166
241
|
|
|
242
|
+
let rejectionIndicator: SVGCircleElement | null = null
|
|
243
|
+
let lastPlanPoint: [number, number] | null = null
|
|
244
|
+
const clearRejection = () => {
|
|
245
|
+
rejectionIndicator?.remove()
|
|
246
|
+
rejectionIndicator = null
|
|
247
|
+
}
|
|
248
|
+
const showRejection = () => {
|
|
249
|
+
if (!lastPlanPoint) return
|
|
250
|
+
clearRejection()
|
|
251
|
+
rejectionIndicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
|
|
252
|
+
rejectionIndicator.setAttribute('cx', String(lastPlanPoint[0]))
|
|
253
|
+
rejectionIndicator.setAttribute('cy', String(lastPlanPoint[1]))
|
|
254
|
+
rejectionIndicator.setAttribute('r', '0.15')
|
|
255
|
+
rejectionIndicator.setAttribute('fill', 'none')
|
|
256
|
+
rejectionIndicator.setAttribute('stroke', '#ef4444')
|
|
257
|
+
rejectionIndicator.setAttribute('stroke-width', '1.5')
|
|
258
|
+
rejectionIndicator.setAttribute('vector-effect', 'non-scaling-stroke')
|
|
259
|
+
rejectionIndicator.setAttribute('pointer-events', 'none')
|
|
260
|
+
scene.appendChild(rejectionIndicator)
|
|
261
|
+
}
|
|
167
262
|
const onMove = (event: PointerEvent) => {
|
|
168
263
|
// Skip 3D-canvas / other-UI cursor moves so the overlay only
|
|
169
264
|
// tracks pointer events that actually correspond to a floor-plan
|
|
@@ -172,25 +267,36 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
172
267
|
// hovers over empty grid background — without it, the cursor
|
|
173
268
|
// only updated the shelf when it happened to brush over an
|
|
174
269
|
// existing SVG entry, leaving the move feeling "stuck" elsewhere.
|
|
175
|
-
if (!isPointerOverFloorplanScene(event
|
|
270
|
+
if (!isPointerOverFloorplanScene(event)) return
|
|
176
271
|
const planPoint = toMeters(event.clientX, event.clientY)
|
|
177
272
|
if (!planPoint) return
|
|
178
273
|
hasMovedSinceStart = true
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
274
|
+
lastPlanPoint = planPoint
|
|
275
|
+
clearRejection()
|
|
276
|
+
ownWrite(() =>
|
|
277
|
+
session.apply({
|
|
278
|
+
planPoint,
|
|
279
|
+
modifiers: {
|
|
280
|
+
shiftKey: event.shiftKey,
|
|
281
|
+
altKey: event.altKey,
|
|
282
|
+
ctrlKey: event.ctrlKey,
|
|
283
|
+
metaKey: event.metaKey,
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
188
287
|
// Move "tick" — same feedback the 3D move gives, which fires whenever the
|
|
189
288
|
// resolved position changes (any snapping mode, not just grid), so it
|
|
190
289
|
// ticks as the item lands on each new snapped/free position.
|
|
191
290
|
const movedId = session.affectedIds[0]
|
|
192
291
|
const moved = movedId ? useScene.getState().nodes[movedId] : undefined
|
|
193
|
-
const
|
|
292
|
+
const effective = moved ? getEffectiveNode(moved) : undefined
|
|
293
|
+
const pos =
|
|
294
|
+
effective?.type === 'item' || effective?.type === 'procedural-item'
|
|
295
|
+
? nodeLevelFrame(effective.id, {
|
|
296
|
+
...useScene.getState().nodes,
|
|
297
|
+
[effective.id]: effective,
|
|
298
|
+
}).position
|
|
299
|
+
: (effective as { position?: [number, number, number] } | undefined)?.position
|
|
194
300
|
if (pos) {
|
|
195
301
|
const key = movementSfxStepKey({
|
|
196
302
|
coords: [pos[0], pos[2]],
|
|
@@ -205,18 +311,32 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
205
311
|
}
|
|
206
312
|
|
|
207
313
|
const commitFinalStateOrRevert = () => {
|
|
208
|
-
|
|
314
|
+
if (!useScene.getState().nodes[movingNode.id]) {
|
|
315
|
+
revertOwnWrites()
|
|
316
|
+
endDrafts()
|
|
317
|
+
clearLivePreviews()
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
// Some registered sessions stage their final polygon from canCommit.
|
|
321
|
+
const commitValid = ownWrite(() => session.canCommit())
|
|
209
322
|
const freshPlacement = isFreshPlacementMetadata(
|
|
210
323
|
(useScene.getState().nodes[movingNode.id] as { metadata?: unknown } | undefined)
|
|
211
324
|
?.metadata,
|
|
212
325
|
)
|
|
213
326
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
327
|
+
const atomicPreview =
|
|
328
|
+
freshPlacement &&
|
|
329
|
+
(ownsSubtree ||
|
|
330
|
+
('children' in movingNode && movingNode.children.length > 0) ||
|
|
331
|
+
surfaceAttachmentId(useScene.getState().nodes[movingNode.id] ?? movingNode) !== null)
|
|
332
|
+
if (atomicPreview && !commitValid) {
|
|
333
|
+
showRejection()
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Keep ordinary teardown order. Validated replacements can refuse, so they
|
|
338
|
+
// claim teardown only after success, before clearing the moving node.
|
|
339
|
+
if (!freshPlacement) setMovingNodeOrigin('2d')
|
|
220
340
|
|
|
221
341
|
// Sessions with a `commit` hook own their atomic write (e.g.
|
|
222
342
|
// wall move emits creates + deletes + updates via the junction
|
|
@@ -224,70 +344,85 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
224
344
|
// and Phase 2's resume — but Phase 2's write is delegated, and
|
|
225
345
|
// we skip the snapshot-diff finalUpdates path.
|
|
226
346
|
if (commitValid && freshPlacement) {
|
|
227
|
-
session
|
|
347
|
+
// Subtrees finalize from the preview. Staging a session commit would publish
|
|
348
|
+
// an unvalidated graph, including for presets created outside our factory.
|
|
349
|
+
if (!atomicPreview) ownWrite(() => session.commit?.())
|
|
228
350
|
const stagedNode = useScene.getState().nodes[movingNode.id]
|
|
351
|
+
const preview = usePlacementPreview.getState().node
|
|
352
|
+
const effective = stagedNode ? getEffectiveNode(stagedNode) : null
|
|
353
|
+
const candidate =
|
|
354
|
+
effective?.visible === false && preview?.id === movingNode.id ? preview : effective
|
|
229
355
|
const committedId = stagedNode
|
|
230
356
|
? commitFreshPlacementSubtree(
|
|
231
357
|
movingNode.id as AnyNodeId,
|
|
232
358
|
{
|
|
359
|
+
...(atomicPreview ? candidate : {}),
|
|
233
360
|
metadata: stripPlacementMetadataFlags(stagedNode.metadata),
|
|
234
361
|
visible: true,
|
|
235
362
|
} as Partial<AnyNode>,
|
|
363
|
+
showRejection,
|
|
236
364
|
)
|
|
237
365
|
: null
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
366
|
+
if (!committedId) return false
|
|
367
|
+
setMovingNodeOrigin('2d')
|
|
368
|
+
if (atomicPreview) {
|
|
369
|
+
for (const id of session.affectedIds) {
|
|
370
|
+
useLiveTransforms.getState().clear(id)
|
|
371
|
+
useLiveNodeOverrides.getState().clear(id)
|
|
372
|
+
}
|
|
241
373
|
}
|
|
242
374
|
if (committedId) {
|
|
375
|
+
settleSceneHistoryDrafts(session.affectedIds)
|
|
243
376
|
sfxEmitter.emit('sfx:item-place')
|
|
244
377
|
useViewer.getState().setSelection({ selectedIds: [committedId] })
|
|
245
378
|
}
|
|
379
|
+
endDrafts()
|
|
246
380
|
return
|
|
247
381
|
}
|
|
248
382
|
|
|
249
383
|
if (commitValid && session.commit) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
session.commit()
|
|
384
|
+
revertOwnWrites()
|
|
385
|
+
recordDrop(() => {
|
|
386
|
+
session.commit?.()
|
|
387
|
+
endTransientMarkers()
|
|
388
|
+
})
|
|
256
389
|
sfxEmitter.emit('sfx:item-place')
|
|
257
|
-
useViewer.getState().setSelection({ selectedIds:
|
|
390
|
+
useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
|
|
258
391
|
return
|
|
259
392
|
}
|
|
260
393
|
|
|
261
394
|
const sceneState = useScene.getState().nodes
|
|
262
395
|
const finalUpdates: Array<{ id: AnyNodeId; data: Record<string, unknown> }> = []
|
|
263
|
-
|
|
264
|
-
|
|
396
|
+
// Reapply everything the revert removes, including newly introduced fields and
|
|
397
|
+
// attachment entries held on hosts outside affectedIds. Foreign writes stay recorded.
|
|
398
|
+
for (const update of sceneHistoryDraftRevertUpdates(session.affectedIds)) {
|
|
399
|
+
const current = sceneState[update.id]
|
|
265
400
|
if (!current) continue
|
|
266
401
|
const data: Record<string, unknown> = {}
|
|
267
402
|
let changed = false
|
|
268
|
-
for (const [key, before] of Object.entries(
|
|
403
|
+
for (const [key, before] of Object.entries(update.data)) {
|
|
269
404
|
const after = (current as unknown as Record<string, unknown>)[key]
|
|
270
405
|
if (!deepEqual(before, after)) {
|
|
271
406
|
data[key] = Array.isArray(after) ? [...(after as unknown[])] : after
|
|
272
407
|
changed = true
|
|
273
408
|
}
|
|
274
409
|
}
|
|
275
|
-
if (changed) finalUpdates.push({ id:
|
|
410
|
+
if (changed) finalUpdates.push({ id: update.id, data })
|
|
276
411
|
}
|
|
277
412
|
|
|
278
|
-
for (const
|
|
279
|
-
const current = sceneState[
|
|
413
|
+
for (const id of initialAffectedIds) {
|
|
414
|
+
const current = sceneState[id]
|
|
280
415
|
if (!current || !isFreshPlacementMetadata((current as { metadata?: unknown }).metadata)) {
|
|
281
416
|
continue
|
|
282
417
|
}
|
|
283
|
-
const existing = finalUpdates.find((update) => update.id ===
|
|
418
|
+
const existing = finalUpdates.find((update) => update.id === id)
|
|
284
419
|
const metadata = stripPlacementMetadataFlags((current as { metadata?: unknown }).metadata)
|
|
285
420
|
if (existing) {
|
|
286
421
|
existing.data.metadata = metadata
|
|
287
422
|
existing.data.visible = true
|
|
288
423
|
} else {
|
|
289
424
|
finalUpdates.push({
|
|
290
|
-
id
|
|
425
|
+
id,
|
|
291
426
|
data: { metadata, visible: true },
|
|
292
427
|
})
|
|
293
428
|
}
|
|
@@ -298,25 +433,21 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
298
433
|
// 1. Revert to baseline while history is still paused.
|
|
299
434
|
// 2. Resume history.
|
|
300
435
|
// 3. Re-apply the final state — recorded as one tracked change.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
useScene.getState().updateNodes(finalUpdates)
|
|
436
|
+
revertOwnWrites()
|
|
437
|
+
recordDrop(() => {
|
|
438
|
+
useScene.getState().updateNodes(finalUpdates)
|
|
439
|
+
endTransientMarkers()
|
|
440
|
+
})
|
|
307
441
|
sfxEmitter.emit('sfx:item-place')
|
|
308
442
|
// Re-select the moved node(s) — mirrors the legacy 3D move
|
|
309
443
|
// tool. The action menu cleared selection on Move click so
|
|
310
444
|
// selection-gated affordances (slab/ceiling boundary editor,
|
|
311
445
|
// etc.) would unmount during the drag; restoring it here
|
|
312
446
|
// brings them back at the new position.
|
|
313
|
-
useViewer.getState().setSelection({ selectedIds:
|
|
447
|
+
useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
|
|
314
448
|
} else {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
resumeSceneHistory(useScene)
|
|
318
|
-
historyPaused = false
|
|
319
|
-
}
|
|
449
|
+
revertOwnWrites()
|
|
450
|
+
endDrafts()
|
|
320
451
|
}
|
|
321
452
|
}
|
|
322
453
|
|
|
@@ -325,7 +456,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
325
456
|
// Bounding-rect check (see `isPointerOverFloorplanScene`) — same
|
|
326
457
|
// reason as `onMove`: commits should land for any pointer-up
|
|
327
458
|
// inside the SVG viewport, including empty grid background.
|
|
328
|
-
if (!isPointerOverFloorplanScene(event
|
|
459
|
+
if (!isPointerOverFloorplanScene(event)) return
|
|
329
460
|
if (!hasMovedSinceStart) return
|
|
330
461
|
|
|
331
462
|
// Commit using the LAST pointermove's state — no re-apply at
|
|
@@ -344,7 +475,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
344
475
|
// pointermove right before pointerup, so the trade-off lands
|
|
345
476
|
// on the side of WYSIWYG.
|
|
346
477
|
|
|
347
|
-
commitFinalStateOrRevert()
|
|
478
|
+
if (commitFinalStateOrRevert() === false) return
|
|
348
479
|
setMovingNode(null)
|
|
349
480
|
|
|
350
481
|
// Swallow the click event that follows this pointer-up — the
|
|
@@ -391,7 +522,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
391
522
|
}
|
|
392
523
|
event.preventDefault()
|
|
393
524
|
event.stopImmediatePropagation()
|
|
394
|
-
session.flipSide()
|
|
525
|
+
ownWrite(() => session.flipSide?.())
|
|
395
526
|
sfxEmitter.emit('sfx:item-rotate')
|
|
396
527
|
return
|
|
397
528
|
}
|
|
@@ -408,22 +539,17 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
408
539
|
setMovingNodeOrigin('2d')
|
|
409
540
|
if (isFreshPlacementMetadata((movingNode as { metadata?: unknown }).metadata)) {
|
|
410
541
|
emitter.emit('tool:cancel')
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
historyPaused = false
|
|
415
|
-
}
|
|
542
|
+
if (!ownsSubtree && !isInteractionSubtreeDraft(movingNode.id))
|
|
543
|
+
ownWrite(() => useScene.getState().deleteNode(movingNode.id))
|
|
544
|
+
endDrafts()
|
|
416
545
|
clearLivePreviews()
|
|
417
546
|
useAlignmentGuides.getState().clear()
|
|
418
547
|
setMovingNode(null)
|
|
419
548
|
return
|
|
420
549
|
}
|
|
421
|
-
// Revert
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
resumeSceneHistory(useScene)
|
|
425
|
-
historyPaused = false
|
|
426
|
-
}
|
|
550
|
+
// Revert as the overlay's own write: no history entry.
|
|
551
|
+
revertOwnWrites()
|
|
552
|
+
endDrafts()
|
|
427
553
|
// Clear any live previews the session wrote. Slab / ceiling
|
|
428
554
|
// 2D move stages a translation delta in `useLiveTransforms`;
|
|
429
555
|
// wall move publishes `{ start, end, ... }` to
|
|
@@ -432,7 +558,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
432
558
|
// position.
|
|
433
559
|
clearLivePreviews()
|
|
434
560
|
// Restore selection cleared by the action menu's Move click.
|
|
435
|
-
useViewer.getState().setSelection({ selectedIds:
|
|
561
|
+
useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
|
|
436
562
|
setMovingNode(null)
|
|
437
563
|
}
|
|
438
564
|
|
|
@@ -444,13 +570,14 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
444
570
|
// then pre-empts it so only one handler flips.
|
|
445
571
|
window.addEventListener('keydown', onKey, true)
|
|
446
572
|
return () => {
|
|
573
|
+
clearRejection()
|
|
447
574
|
window.removeEventListener('pointermove', onPointerTrack)
|
|
448
575
|
window.removeEventListener('pointermove', onMove)
|
|
449
576
|
window.removeEventListener('pointerup', onPointerUp)
|
|
450
577
|
window.removeEventListener('keydown', onKey, true)
|
|
451
578
|
// Unmount cleanup. `historyPaused === true` here means none of
|
|
452
579
|
// our terminal paths (commit, Esc) ran in this overlay — they
|
|
453
|
-
// each
|
|
580
|
+
// each end the drafts and flip the flag.
|
|
454
581
|
//
|
|
455
582
|
// If `movingNodeOrigin === '3d'`, a 3D move tool finalised
|
|
456
583
|
// while our overlay was still mounted (split view); the live
|
|
@@ -464,21 +591,22 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
464
591
|
// our unmount
|
|
465
592
|
// are now distinguished by the origin flag — no scene-state
|
|
466
593
|
// diff heuristic required.
|
|
594
|
+
const freshPlacement = ownsSubtree
|
|
467
595
|
if (historyPaused) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
596
|
+
const finalisedBy3D = useEditor.getState().movingNodeOrigin === '3d'
|
|
597
|
+
if (!finalisedBy3D && !freshPlacement) {
|
|
598
|
+
if (hasMovedSinceStart) {
|
|
599
|
+
revertOwnWrites()
|
|
472
600
|
}
|
|
473
601
|
}
|
|
474
|
-
|
|
602
|
+
endDrafts()
|
|
475
603
|
}
|
|
476
604
|
// Belt-and-suspenders: clear any live previews on abnormal
|
|
477
605
|
// unmount paths too. Slab / ceiling sessions write to
|
|
478
606
|
// `useLiveTransforms`; wall sessions write to
|
|
479
607
|
// `useLiveNodeOverrides`. In pure 2D view the corresponding 3D
|
|
480
608
|
// tool's cleanup isn't there to clear them for us.
|
|
481
|
-
clearLivePreviews()
|
|
609
|
+
if (!freshPlacement) clearLivePreviews()
|
|
482
610
|
// Sessions that publish Figma-style alignment guides during `apply`
|
|
483
611
|
// (item / shelf / column) leave them in the store; this cleanup runs
|
|
484
612
|
// after every terminal path (commit + Esc both unmount via
|
|
@@ -589,7 +717,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
589
717
|
// Same target guard as Path 1 — pointer must be over the floor
|
|
590
718
|
// plan scene; otherwise we'd react to 3D-canvas moves with garbage
|
|
591
719
|
// plan coords.
|
|
592
|
-
if (!isPointerOverFloorplanScene(event
|
|
720
|
+
if (!isPointerOverFloorplanScene(event)) return
|
|
593
721
|
const m = toMeters(event.clientX, event.clientY)
|
|
594
722
|
if (!m) return
|
|
595
723
|
forcePlace = event.altKey
|
|
@@ -740,7 +868,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
740
868
|
|
|
741
869
|
const onPointerUp = (event: PointerEvent) => {
|
|
742
870
|
if (event.button !== 0) return
|
|
743
|
-
if (!isPointerOverFloorplanScene(event
|
|
871
|
+
if (!isPointerOverFloorplanScene(event)) return
|
|
744
872
|
|
|
745
873
|
const snapped = lastSnapped
|
|
746
874
|
if (!snapped) return
|
|
@@ -754,8 +882,14 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
754
882
|
]
|
|
755
883
|
: snapped.rotation
|
|
756
884
|
const rotationPatch = 'rotation' in movingNode ? { rotation } : {}
|
|
757
|
-
|
|
885
|
+
const atomicPreview =
|
|
886
|
+
isFreshPlacement &&
|
|
887
|
+
(ownsSubtree ||
|
|
888
|
+
('children' in movingNode && movingNode.children.length > 0) ||
|
|
889
|
+
surfaceAttachmentId(useScene.getState().nodes[movingNode.id] ?? movingNode) !== null)
|
|
890
|
+
if (!atomicPreview) setMovingNodeOrigin('2d')
|
|
758
891
|
if (!lastPositionValid && !forcePlace) {
|
|
892
|
+
if (atomicPreview) setMovingNodeOrigin('2d')
|
|
759
893
|
for (const relatedEntry of relatedEntries) {
|
|
760
894
|
relatedEntry.removeAttribute('transform')
|
|
761
895
|
}
|
|
@@ -766,6 +900,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
766
900
|
}
|
|
767
901
|
let selectedId = movingNode.id as AnyNodeId
|
|
768
902
|
if (originalPath) {
|
|
903
|
+
if (atomicPreview) setMovingNodeOrigin('2d')
|
|
769
904
|
// Polyline kinds: shift every point by the committed delta and
|
|
770
905
|
// write `path`. Strip the fresh-placement flags on first drop.
|
|
771
906
|
const dx = sx - originalPosition[0]
|
|
@@ -795,18 +930,21 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
795
930
|
return
|
|
796
931
|
}
|
|
797
932
|
if (isFreshPlacement) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
933
|
+
const committedId = commitFreshPlacementSubtree(
|
|
934
|
+
movingNode.id as AnyNodeId,
|
|
935
|
+
{
|
|
936
|
+
position: [sx, oldY, sz],
|
|
937
|
+
...rotationPatch,
|
|
938
|
+
metadata: stripPlacementMetadataFlags((movingNode as { metadata?: unknown }).metadata),
|
|
939
|
+
visible: true,
|
|
940
|
+
} as Partial<AnyNode>,
|
|
941
|
+
)
|
|
942
|
+
if (!committedId && atomicPreview) {
|
|
943
|
+
boxEl.setAttribute('stroke', '#ef4444')
|
|
944
|
+
return
|
|
945
|
+
}
|
|
946
|
+
if (atomicPreview) setMovingNodeOrigin('2d')
|
|
947
|
+
selectedId = committedId ?? selectedId
|
|
810
948
|
} else {
|
|
811
949
|
useScene.getState().updateNode(
|
|
812
950
|
movingNode.id as AnyNodeId,
|
|
@@ -836,11 +974,9 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
836
974
|
setMovingNodeOrigin('2d')
|
|
837
975
|
if (isFreshPlacement) {
|
|
838
976
|
emitter.emit('tool:cancel')
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
useScene.getState().deleteNode(movingNode.id as AnyNodeId)
|
|
843
|
-
if (wasTracking) temporal.resume()
|
|
977
|
+
if (!ownsSubtree && !isInteractionSubtreeDraft(movingNode.id)) {
|
|
978
|
+
runSceneHistoryDraftWrite(() => useScene.getState().deleteNode(movingNode.id))
|
|
979
|
+
}
|
|
844
980
|
}
|
|
845
981
|
for (const relatedEntry of relatedEntries) {
|
|
846
982
|
relatedEntry.removeAttribute('transform')
|
|
@@ -872,26 +1008,6 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
872
1008
|
return null
|
|
873
1009
|
}
|
|
874
1010
|
|
|
875
|
-
// ── Snapshot helpers (shared shape with floorplan-registry-layer) ───
|
|
876
|
-
//
|
|
877
|
-
// Kept inline here to avoid a circular dependency through a shared
|
|
878
|
-
// utility module. If a third call site shows up, extract.
|
|
879
|
-
|
|
880
|
-
type NodeSnapshot = { id: AnyNodeId; data: Record<string, unknown> }
|
|
881
|
-
|
|
882
|
-
function snapshotNode(node: AnyNode): NodeSnapshot {
|
|
883
|
-
const data: Record<string, unknown> = {}
|
|
884
|
-
for (const [key, value] of Object.entries(node)) {
|
|
885
|
-
if (key === 'id' || key === 'type' || key === 'object') continue
|
|
886
|
-
data[key] = Array.isArray(value) ? [...(value as unknown[])] : value
|
|
887
|
-
}
|
|
888
|
-
return { id: node.id, data }
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function snapshotsToUpdates(snapshots: NodeSnapshot[]) {
|
|
892
|
-
return snapshots.map((s) => ({ id: s.id, data: s.data }))
|
|
893
|
-
}
|
|
894
|
-
|
|
895
1011
|
function deepEqual(a: unknown, b: unknown): boolean {
|
|
896
1012
|
if (a === b) return true
|
|
897
1013
|
if (Array.isArray(a) && Array.isArray(b)) {
|