@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
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
type AnyNodeId,
|
|
7
7
|
bboxAnchors,
|
|
8
8
|
bboxCornerAnchors,
|
|
9
|
+
beginSceneHistoryDraft,
|
|
10
|
+
beginSceneHistoryPauseSession,
|
|
9
11
|
cascadeDirty,
|
|
10
12
|
collectDescendants,
|
|
11
13
|
createSceneApi,
|
|
@@ -15,8 +17,10 @@ import {
|
|
|
15
17
|
getEffectiveNode,
|
|
16
18
|
type MovableConfig,
|
|
17
19
|
nodeRegistry,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
runAsSingleSceneHistoryStep,
|
|
21
|
+
runSceneHistoryDraftWrite,
|
|
22
|
+
sceneHistoryDraftRevertUpdates,
|
|
23
|
+
settleSceneHistoryDrafts,
|
|
20
24
|
useLiveNodeOverrides,
|
|
21
25
|
useLiveTransforms,
|
|
22
26
|
useScene,
|
|
@@ -146,15 +150,55 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
146
150
|
}) => FloorplanMoveTargetSession
|
|
147
151
|
)({ node: movingNode, nodes: sceneNodes, sceneApi })
|
|
148
152
|
|
|
149
|
-
|
|
150
|
-
// so the single-undo dance has a clean baseline to revert to.
|
|
151
|
-
const snapshots = session.affectedIds
|
|
152
|
-
.map((id) => sceneNodes[id])
|
|
153
|
-
.filter((n): n is AnyNode => !!n)
|
|
154
|
-
.map((n) => snapshotNode(n))
|
|
153
|
+
const initialAffectedIds = session.affectedIds.filter((id) => sceneNodes[id])
|
|
155
154
|
|
|
156
|
-
|
|
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
|
+
)
|
|
157
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
|
+
}
|
|
158
202
|
|
|
159
203
|
const clearLivePreviews = () => {
|
|
160
204
|
const liveTransforms = useLiveTransforms.getState()
|
|
@@ -229,15 +273,17 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
229
273
|
hasMovedSinceStart = true
|
|
230
274
|
lastPlanPoint = planPoint
|
|
231
275
|
clearRejection()
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
)
|
|
241
287
|
// Move "tick" — same feedback the 3D move gives, which fires whenever the
|
|
242
288
|
// resolved position changes (any snapping mode, not just grid), so it
|
|
243
289
|
// ticks as the item lands on each new snapped/free position.
|
|
@@ -265,7 +311,14 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
265
311
|
}
|
|
266
312
|
|
|
267
313
|
const commitFinalStateOrRevert = () => {
|
|
268
|
-
|
|
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())
|
|
269
322
|
const freshPlacement = isFreshPlacementMetadata(
|
|
270
323
|
(useScene.getState().nodes[movingNode.id] as { metadata?: unknown } | undefined)
|
|
271
324
|
?.metadata,
|
|
@@ -283,7 +336,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
283
336
|
|
|
284
337
|
// Keep ordinary teardown order. Validated replacements can refuse, so they
|
|
285
338
|
// claim teardown only after success, before clearing the moving node.
|
|
286
|
-
if (!
|
|
339
|
+
if (!freshPlacement) setMovingNodeOrigin('2d')
|
|
287
340
|
|
|
288
341
|
// Sessions with a `commit` hook own their atomic write (e.g.
|
|
289
342
|
// wall move emits creates + deletes + updates via the junction
|
|
@@ -293,7 +346,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
293
346
|
if (commitValid && freshPlacement) {
|
|
294
347
|
// Subtrees finalize from the preview. Staging a session commit would publish
|
|
295
348
|
// an unvalidated graph, including for presets created outside our factory.
|
|
296
|
-
if (!atomicPreview) session.commit?.()
|
|
349
|
+
if (!atomicPreview) ownWrite(() => session.commit?.())
|
|
297
350
|
const stagedNode = useScene.getState().nodes[movingNode.id]
|
|
298
351
|
const preview = usePlacementPreview.getState().node
|
|
299
352
|
const effective = stagedNode ? getEffectiveNode(stagedNode) : null
|
|
@@ -310,67 +363,66 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
310
363
|
showRejection,
|
|
311
364
|
)
|
|
312
365
|
: null
|
|
313
|
-
if (!committedId
|
|
366
|
+
if (!committedId) return false
|
|
367
|
+
setMovingNodeOrigin('2d')
|
|
314
368
|
if (atomicPreview) {
|
|
315
|
-
setMovingNodeOrigin('2d')
|
|
316
369
|
for (const id of session.affectedIds) {
|
|
317
370
|
useLiveTransforms.getState().clear(id)
|
|
318
371
|
useLiveNodeOverrides.getState().clear(id)
|
|
319
372
|
}
|
|
320
373
|
}
|
|
321
|
-
if (historyPaused) {
|
|
322
|
-
resumeSceneHistory(useScene)
|
|
323
|
-
historyPaused = false
|
|
324
|
-
}
|
|
325
374
|
if (committedId) {
|
|
375
|
+
settleSceneHistoryDrafts(session.affectedIds)
|
|
326
376
|
sfxEmitter.emit('sfx:item-place')
|
|
327
377
|
useViewer.getState().setSelection({ selectedIds: [committedId] })
|
|
328
378
|
}
|
|
379
|
+
endDrafts()
|
|
329
380
|
return
|
|
330
381
|
}
|
|
331
382
|
|
|
332
383
|
if (commitValid && session.commit) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
session.commit()
|
|
384
|
+
revertOwnWrites()
|
|
385
|
+
recordDrop(() => {
|
|
386
|
+
session.commit?.()
|
|
387
|
+
endTransientMarkers()
|
|
388
|
+
})
|
|
339
389
|
sfxEmitter.emit('sfx:item-place')
|
|
340
|
-
useViewer.getState().setSelection({ selectedIds:
|
|
390
|
+
useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
|
|
341
391
|
return
|
|
342
392
|
}
|
|
343
393
|
|
|
344
394
|
const sceneState = useScene.getState().nodes
|
|
345
395
|
const finalUpdates: Array<{ id: AnyNodeId; data: Record<string, unknown> }> = []
|
|
346
|
-
|
|
347
|
-
|
|
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]
|
|
348
400
|
if (!current) continue
|
|
349
401
|
const data: Record<string, unknown> = {}
|
|
350
402
|
let changed = false
|
|
351
|
-
for (const [key, before] of Object.entries(
|
|
403
|
+
for (const [key, before] of Object.entries(update.data)) {
|
|
352
404
|
const after = (current as unknown as Record<string, unknown>)[key]
|
|
353
405
|
if (!deepEqual(before, after)) {
|
|
354
406
|
data[key] = Array.isArray(after) ? [...(after as unknown[])] : after
|
|
355
407
|
changed = true
|
|
356
408
|
}
|
|
357
409
|
}
|
|
358
|
-
if (changed) finalUpdates.push({ id:
|
|
410
|
+
if (changed) finalUpdates.push({ id: update.id, data })
|
|
359
411
|
}
|
|
360
412
|
|
|
361
|
-
for (const
|
|
362
|
-
const current = sceneState[
|
|
413
|
+
for (const id of initialAffectedIds) {
|
|
414
|
+
const current = sceneState[id]
|
|
363
415
|
if (!current || !isFreshPlacementMetadata((current as { metadata?: unknown }).metadata)) {
|
|
364
416
|
continue
|
|
365
417
|
}
|
|
366
|
-
const existing = finalUpdates.find((update) => update.id ===
|
|
418
|
+
const existing = finalUpdates.find((update) => update.id === id)
|
|
367
419
|
const metadata = stripPlacementMetadataFlags((current as { metadata?: unknown }).metadata)
|
|
368
420
|
if (existing) {
|
|
369
421
|
existing.data.metadata = metadata
|
|
370
422
|
existing.data.visible = true
|
|
371
423
|
} else {
|
|
372
424
|
finalUpdates.push({
|
|
373
|
-
id
|
|
425
|
+
id,
|
|
374
426
|
data: { metadata, visible: true },
|
|
375
427
|
})
|
|
376
428
|
}
|
|
@@ -381,25 +433,21 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
381
433
|
// 1. Revert to baseline while history is still paused.
|
|
382
434
|
// 2. Resume history.
|
|
383
435
|
// 3. Re-apply the final state — recorded as one tracked change.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
useScene.getState().updateNodes(finalUpdates)
|
|
436
|
+
revertOwnWrites()
|
|
437
|
+
recordDrop(() => {
|
|
438
|
+
useScene.getState().updateNodes(finalUpdates)
|
|
439
|
+
endTransientMarkers()
|
|
440
|
+
})
|
|
390
441
|
sfxEmitter.emit('sfx:item-place')
|
|
391
442
|
// Re-select the moved node(s) — mirrors the legacy 3D move
|
|
392
443
|
// tool. The action menu cleared selection on Move click so
|
|
393
444
|
// selection-gated affordances (slab/ceiling boundary editor,
|
|
394
445
|
// etc.) would unmount during the drag; restoring it here
|
|
395
446
|
// brings them back at the new position.
|
|
396
|
-
useViewer.getState().setSelection({ selectedIds:
|
|
447
|
+
useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
|
|
397
448
|
} else {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
resumeSceneHistory(useScene)
|
|
401
|
-
historyPaused = false
|
|
402
|
-
}
|
|
449
|
+
revertOwnWrites()
|
|
450
|
+
endDrafts()
|
|
403
451
|
}
|
|
404
452
|
}
|
|
405
453
|
|
|
@@ -474,7 +522,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
474
522
|
}
|
|
475
523
|
event.preventDefault()
|
|
476
524
|
event.stopImmediatePropagation()
|
|
477
|
-
session.flipSide()
|
|
525
|
+
ownWrite(() => session.flipSide?.())
|
|
478
526
|
sfxEmitter.emit('sfx:item-rotate')
|
|
479
527
|
return
|
|
480
528
|
}
|
|
@@ -492,22 +540,16 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
492
540
|
if (isFreshPlacementMetadata((movingNode as { metadata?: unknown }).metadata)) {
|
|
493
541
|
emitter.emit('tool:cancel')
|
|
494
542
|
if (!ownsSubtree && !isInteractionSubtreeDraft(movingNode.id))
|
|
495
|
-
useScene.getState().deleteNode(movingNode.id)
|
|
496
|
-
|
|
497
|
-
resumeSceneHistory(useScene)
|
|
498
|
-
historyPaused = false
|
|
499
|
-
}
|
|
543
|
+
ownWrite(() => useScene.getState().deleteNode(movingNode.id))
|
|
544
|
+
endDrafts()
|
|
500
545
|
clearLivePreviews()
|
|
501
546
|
useAlignmentGuides.getState().clear()
|
|
502
547
|
setMovingNode(null)
|
|
503
548
|
return
|
|
504
549
|
}
|
|
505
|
-
// Revert
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
resumeSceneHistory(useScene)
|
|
509
|
-
historyPaused = false
|
|
510
|
-
}
|
|
550
|
+
// Revert as the overlay's own write: no history entry.
|
|
551
|
+
revertOwnWrites()
|
|
552
|
+
endDrafts()
|
|
511
553
|
// Clear any live previews the session wrote. Slab / ceiling
|
|
512
554
|
// 2D move stages a translation delta in `useLiveTransforms`;
|
|
513
555
|
// wall move publishes `{ start, end, ... }` to
|
|
@@ -516,7 +558,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
516
558
|
// position.
|
|
517
559
|
clearLivePreviews()
|
|
518
560
|
// Restore selection cleared by the action menu's Move click.
|
|
519
|
-
useViewer.getState().setSelection({ selectedIds:
|
|
561
|
+
useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
|
|
520
562
|
setMovingNode(null)
|
|
521
563
|
}
|
|
522
564
|
|
|
@@ -535,7 +577,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
535
577
|
window.removeEventListener('keydown', onKey, true)
|
|
536
578
|
// Unmount cleanup. `historyPaused === true` here means none of
|
|
537
579
|
// our terminal paths (commit, Esc) ran in this overlay — they
|
|
538
|
-
// each
|
|
580
|
+
// each end the drafts and flip the flag.
|
|
539
581
|
//
|
|
540
582
|
// If `movingNodeOrigin === '3d'`, a 3D move tool finalised
|
|
541
583
|
// while our overlay was still mounted (split view); the live
|
|
@@ -554,10 +596,10 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
554
596
|
const finalisedBy3D = useEditor.getState().movingNodeOrigin === '3d'
|
|
555
597
|
if (!finalisedBy3D && !freshPlacement) {
|
|
556
598
|
if (hasMovedSinceStart) {
|
|
557
|
-
|
|
599
|
+
revertOwnWrites()
|
|
558
600
|
}
|
|
559
601
|
}
|
|
560
|
-
|
|
602
|
+
endDrafts()
|
|
561
603
|
}
|
|
562
604
|
// Belt-and-suspenders: clear any live previews on abnormal
|
|
563
605
|
// unmount paths too. Slab / ceiling sessions write to
|
|
@@ -933,11 +975,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
933
975
|
if (isFreshPlacement) {
|
|
934
976
|
emitter.emit('tool:cancel')
|
|
935
977
|
if (!ownsSubtree && !isInteractionSubtreeDraft(movingNode.id)) {
|
|
936
|
-
|
|
937
|
-
const wasTracking = (temporal as { isTracking?: boolean }).isTracking !== false
|
|
938
|
-
if (wasTracking) temporal.pause()
|
|
939
|
-
useScene.getState().deleteNode(movingNode.id)
|
|
940
|
-
if (wasTracking) temporal.resume()
|
|
978
|
+
runSceneHistoryDraftWrite(() => useScene.getState().deleteNode(movingNode.id))
|
|
941
979
|
}
|
|
942
980
|
}
|
|
943
981
|
for (const relatedEntry of relatedEntries) {
|
|
@@ -970,33 +1008,6 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
970
1008
|
return null
|
|
971
1009
|
}
|
|
972
1010
|
|
|
973
|
-
// ── Snapshot helpers (shared shape with floorplan-registry-layer) ───
|
|
974
|
-
//
|
|
975
|
-
// Kept inline here to avoid a circular dependency through a shared
|
|
976
|
-
// utility module. If a third call site shows up, extract.
|
|
977
|
-
|
|
978
|
-
type NodeSnapshot = { id: AnyNodeId; data: Record<string, unknown> }
|
|
979
|
-
|
|
980
|
-
function snapshotNode(node: AnyNode): NodeSnapshot {
|
|
981
|
-
const data: Record<string, unknown> = {}
|
|
982
|
-
for (const [key, value] of Object.entries(node)) {
|
|
983
|
-
if (key === 'id' || key === 'type' || key === 'object') continue
|
|
984
|
-
data[key] = Array.isArray(value) ? [...(value as unknown[])] : value
|
|
985
|
-
}
|
|
986
|
-
return { id: node.id, data }
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
function restoreSnapshots(snapshots: NodeSnapshot[]) {
|
|
990
|
-
const updates = snapshots
|
|
991
|
-
.filter(
|
|
992
|
-
(s) =>
|
|
993
|
-
!useScene.getState().nodes[s.id] ||
|
|
994
|
-
!deepEqual(s.data, snapshotNode(useScene.getState().nodes[s.id]!).data),
|
|
995
|
-
)
|
|
996
|
-
.map((s) => ({ id: s.id, data: s.data }))
|
|
997
|
-
if (updates.length) useScene.getState().updateNodes(updates)
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
1011
|
function deepEqual(a: unknown, b: unknown): boolean {
|
|
1001
1012
|
if (a === b) return true
|
|
1002
1013
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode, AnyNodeId } from '@pascal-app/core'
|
|
3
|
+
import { useScene } from '@pascal-app/core'
|
|
4
|
+
import { commitDimensionValue, isDimensionEditAllowed } from './floorplan-dimension-edit-overlay'
|
|
5
|
+
|
|
6
|
+
type Change = { id: AnyNodeId; data: Partial<AnyNode> }
|
|
7
|
+
|
|
8
|
+
function wall(id: string, start: [number, number], end: [number, number]): AnyNode {
|
|
9
|
+
return {
|
|
10
|
+
object: 'node',
|
|
11
|
+
id,
|
|
12
|
+
type: 'wall',
|
|
13
|
+
parentId: 'level_1',
|
|
14
|
+
visible: true,
|
|
15
|
+
metadata: {},
|
|
16
|
+
start,
|
|
17
|
+
end,
|
|
18
|
+
thickness: 0.15,
|
|
19
|
+
children: [],
|
|
20
|
+
} as unknown as AnyNode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function nodes(): Record<string, AnyNode> {
|
|
24
|
+
return {
|
|
25
|
+
level_1: {
|
|
26
|
+
object: 'node',
|
|
27
|
+
id: 'level_1',
|
|
28
|
+
type: 'level',
|
|
29
|
+
parentId: null,
|
|
30
|
+
visible: true,
|
|
31
|
+
metadata: {},
|
|
32
|
+
level: 0,
|
|
33
|
+
children: ['wall_a', 'wall_b', 'dim_1'],
|
|
34
|
+
} as unknown as AnyNode,
|
|
35
|
+
wall_a: wall('wall_a', [0, 0], [3, 0]),
|
|
36
|
+
wall_b: wall('wall_b', [3, 0], [3, 4]),
|
|
37
|
+
dim_1: {
|
|
38
|
+
object: 'node',
|
|
39
|
+
id: 'dim_1',
|
|
40
|
+
type: 'construction-dimension',
|
|
41
|
+
parentId: 'level_1',
|
|
42
|
+
visible: true,
|
|
43
|
+
metadata: {},
|
|
44
|
+
children: [],
|
|
45
|
+
} as unknown as AnyNode,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let initial: ReturnType<typeof useScene.getState>
|
|
50
|
+
let applied: Change[]
|
|
51
|
+
let updated: Change[]
|
|
52
|
+
let dirty: AnyNodeId[]
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
initial = useScene.getState()
|
|
56
|
+
applied = []
|
|
57
|
+
updated = []
|
|
58
|
+
dirty = []
|
|
59
|
+
useScene.setState({
|
|
60
|
+
nodes: nodes() as ReturnType<typeof useScene.getState>['nodes'],
|
|
61
|
+
applyNodeChanges: ((changes: { update?: Change[] }) => {
|
|
62
|
+
applied.push(...(changes.update ?? []))
|
|
63
|
+
}) as ReturnType<typeof useScene.getState>['applyNodeChanges'],
|
|
64
|
+
updateNode: ((id: AnyNodeId, data: Partial<AnyNode>) => {
|
|
65
|
+
updated.push({ id, data })
|
|
66
|
+
}) as ReturnType<typeof useScene.getState>['updateNode'],
|
|
67
|
+
markDirty: ((id: AnyNodeId) => {
|
|
68
|
+
dirty.push(id)
|
|
69
|
+
}) as ReturnType<typeof useScene.getState>['markDirty'],
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
useScene.setState(initial, true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('commitDimensionValue', () => {
|
|
78
|
+
test('a wall dimension drives the wall end and the wall sharing that corner', () => {
|
|
79
|
+
const outcome = commitDimensionValue({
|
|
80
|
+
target: {
|
|
81
|
+
ownerNodeId: 'wall_a' as AnyNodeId,
|
|
82
|
+
witnessStart: [0, 0],
|
|
83
|
+
witnessEnd: [3, 0],
|
|
84
|
+
value: 3,
|
|
85
|
+
},
|
|
86
|
+
nextLength: 3.5,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(outcome).toEqual({ ok: true })
|
|
90
|
+
const byId = new Map(applied.map((change) => [change.id, change.data]))
|
|
91
|
+
expect(byId.get('wall_a' as AnyNodeId)).toEqual({ start: [0, 0], end: [3.5, 0] })
|
|
92
|
+
expect(byId.get('wall_b' as AnyNodeId)).toEqual({ start: [3.5, 0], end: [3, 4] })
|
|
93
|
+
expect(dirty.sort()).toEqual(['wall_a', 'wall_b'] as AnyNodeId[])
|
|
94
|
+
expect(updated).toEqual([])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('an undrivable wall dimension is refused and nothing is written', () => {
|
|
98
|
+
const outcome = commitDimensionValue({
|
|
99
|
+
target: {
|
|
100
|
+
ownerNodeId: 'wall_a' as AnyNodeId,
|
|
101
|
+
witnessStart: [1.5, -1],
|
|
102
|
+
witnessEnd: [1.5, 1],
|
|
103
|
+
value: 2,
|
|
104
|
+
},
|
|
105
|
+
nextLength: 2.5,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(outcome.ok).toBe(false)
|
|
109
|
+
expect(applied).toEqual([])
|
|
110
|
+
expect(updated).toEqual([])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('an undrivable construction dimension keeps the typed text as an override', () => {
|
|
114
|
+
const outcome = commitDimensionValue({
|
|
115
|
+
target: {
|
|
116
|
+
ownerNodeId: 'dim_1' as AnyNodeId,
|
|
117
|
+
witnessStart: [10, 10],
|
|
118
|
+
witnessEnd: [12, 10],
|
|
119
|
+
value: 2,
|
|
120
|
+
},
|
|
121
|
+
nextLength: 3,
|
|
122
|
+
unit: 'metric',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(outcome).toEqual({ ok: true })
|
|
126
|
+
expect(applied).toEqual([])
|
|
127
|
+
expect(updated).toHaveLength(1)
|
|
128
|
+
expect(updated[0]?.id).toBe('dim_1' as AnyNodeId)
|
|
129
|
+
expect(typeof (updated[0]?.data as { textOverride?: unknown }).textOverride).toBe('string')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('isDimensionEditAllowed', () => {
|
|
134
|
+
const idle = {
|
|
135
|
+
workspaceMode: 'edit',
|
|
136
|
+
mode: 'select',
|
|
137
|
+
isPreviewMode: false,
|
|
138
|
+
isCaptureMode: false,
|
|
139
|
+
isFirstPersonMode: false,
|
|
140
|
+
} as const
|
|
141
|
+
|
|
142
|
+
test('only an editable scene in select mode of the edit workspace', () => {
|
|
143
|
+
expect(isDimensionEditAllowed(idle, { readOnly: false })).toBe(true)
|
|
144
|
+
expect(isDimensionEditAllowed(idle, { readOnly: true })).toBe(false)
|
|
145
|
+
expect(isDimensionEditAllowed({ ...idle, workspaceMode: 'studio' }, { readOnly: false })).toBe(
|
|
146
|
+
false,
|
|
147
|
+
)
|
|
148
|
+
expect(isDimensionEditAllowed({ ...idle, workspaceMode: 'sheets' }, { readOnly: false })).toBe(
|
|
149
|
+
false,
|
|
150
|
+
)
|
|
151
|
+
expect(isDimensionEditAllowed({ ...idle, mode: 'delete' }, { readOnly: false })).toBe(false)
|
|
152
|
+
expect(isDimensionEditAllowed({ ...idle, isPreviewMode: true }, { readOnly: false })).toBe(
|
|
153
|
+
false,
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
})
|