@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
|
@@ -43,6 +43,9 @@ type FloorplanDraftPreviewState = {
|
|
|
43
43
|
/** First corner of an open rectangle-wall draft; the live cursor is the
|
|
44
44
|
* opposite corner, so the rectangle tool publishes only per click. */
|
|
45
45
|
wallRectangleDraftStart: WallPlanPoint | null
|
|
46
|
+
/** Corners placed so far by an open Polygon room. Draft only — no wall is
|
|
47
|
+
* written until the polygon completes (`lib/wall-polygon-draft.ts`). */
|
|
48
|
+
wallPolygonDraftPoints: WallPlanPoint[]
|
|
46
49
|
roofDraftQuarterTurn: boolean
|
|
47
50
|
polygonDraftType: FloorplanPolygonDraftType | null
|
|
48
51
|
polygonDraftPoints: WallPlanPoint[]
|
|
@@ -59,6 +62,7 @@ type FloorplanDraftPreviewState = {
|
|
|
59
62
|
setFenceDraftStart(point: WallPlanPoint | null): void
|
|
60
63
|
setRoofDraftStart(point: WallPlanPoint | null): void
|
|
61
64
|
setWallRectangleDraftStart(point: WallPlanPoint | null): void
|
|
65
|
+
setWallPolygonDraftPoints(points: readonly WallPlanPoint[]): void
|
|
62
66
|
setRoofDraftQuarterTurn(quarterTurn: boolean): void
|
|
63
67
|
setPolygonDraft(type: FloorplanPolygonDraftType | null, points: readonly WallPlanPoint[]): void
|
|
64
68
|
reset(): void
|
|
@@ -102,6 +106,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
102
106
|
fenceDraftStart: null,
|
|
103
107
|
roofDraftStart: null,
|
|
104
108
|
wallRectangleDraftStart: null,
|
|
109
|
+
wallPolygonDraftPoints: [],
|
|
105
110
|
roofDraftQuarterTurn: false,
|
|
106
111
|
polygonDraftType: null,
|
|
107
112
|
polygonDraftPoints: [],
|
|
@@ -126,6 +131,12 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
126
131
|
setFenceDraftStart: (point) => set(setPlanPointField('fenceDraftStart', point)),
|
|
127
132
|
setRoofDraftStart: (point) => set(setPlanPointField('roofDraftStart', point)),
|
|
128
133
|
setWallRectangleDraftStart: (point) => set(setPlanPointField('wallRectangleDraftStart', point)),
|
|
134
|
+
setWallPolygonDraftPoints: (points) =>
|
|
135
|
+
set((state) =>
|
|
136
|
+
planPointsEqual(state.wallPolygonDraftPoints, points)
|
|
137
|
+
? state
|
|
138
|
+
: { wallPolygonDraftPoints: points.map(([x, z]) => [x, z]) },
|
|
139
|
+
),
|
|
129
140
|
setRoofDraftQuarterTurn: (quarterTurn) =>
|
|
130
141
|
set((state) =>
|
|
131
142
|
state.roofDraftQuarterTurn === quarterTurn ? state : { roofDraftQuarterTurn: quarterTurn },
|
|
@@ -147,6 +158,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
147
158
|
state.fenceDraftStart === null &&
|
|
148
159
|
state.roofDraftStart === null &&
|
|
149
160
|
state.wallRectangleDraftStart === null &&
|
|
161
|
+
state.wallPolygonDraftPoints.length === 0 &&
|
|
150
162
|
state.roofDraftQuarterTurn === false &&
|
|
151
163
|
state.polygonDraftType === null &&
|
|
152
164
|
state.polygonDraftPoints.length === 0
|
|
@@ -161,6 +173,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
161
173
|
fenceDraftStart: null,
|
|
162
174
|
roofDraftStart: null,
|
|
163
175
|
wallRectangleDraftStart: null,
|
|
176
|
+
wallPolygonDraftPoints: [],
|
|
164
177
|
roofDraftQuarterTurn: false,
|
|
165
178
|
polygonDraftType: null,
|
|
166
179
|
polygonDraftPoints: [],
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { WallPlanPoint } from '@pascal-app/core'
|
|
15
15
|
import { create } from 'zustand'
|
|
16
|
+
import useInteractionScope from './use-interaction-scope'
|
|
16
17
|
|
|
17
18
|
export type FloorplanMarqueeDrag = {
|
|
18
19
|
pointerId: number
|
|
@@ -34,7 +35,10 @@ type FloorplanMarqueeState = {
|
|
|
34
35
|
|
|
35
36
|
export const useFloorplanMarquee = create<FloorplanMarqueeState>((set) => ({
|
|
36
37
|
drag: null,
|
|
37
|
-
begin: (drag) =>
|
|
38
|
+
begin: (drag) => {
|
|
39
|
+
useInteractionScope.getState().begin({ kind: 'box-select' })
|
|
40
|
+
set({ drag })
|
|
41
|
+
},
|
|
38
42
|
setCurrent: (point) =>
|
|
39
43
|
set((state) => {
|
|
40
44
|
const prev = state.drag
|
|
@@ -44,7 +48,13 @@ export const useFloorplanMarquee = create<FloorplanMarqueeState>((set) => ({
|
|
|
44
48
|
}
|
|
45
49
|
return { drag: { ...prev, currentPlanPoint: point } }
|
|
46
50
|
}),
|
|
47
|
-
reset: () =>
|
|
51
|
+
reset: () => {
|
|
52
|
+
set((state) => {
|
|
53
|
+
if (!state.drag) return state
|
|
54
|
+
useInteractionScope.getState().endIf((scope) => scope.kind === 'box-select')
|
|
55
|
+
return { drag: null }
|
|
56
|
+
})
|
|
57
|
+
},
|
|
48
58
|
}))
|
|
49
59
|
|
|
50
60
|
export default useFloorplanMarquee
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuickMeasurementReport } from '@pascal-app/core'
|
|
2
2
|
import { create } from 'zustand'
|
|
3
|
-
import type {
|
|
3
|
+
import type { VisibleScene } from './view-layout'
|
|
4
4
|
|
|
5
5
|
export type QuickMeasurementHudSource = '2d' | '3d'
|
|
6
6
|
|
|
@@ -55,9 +55,9 @@ export const useQuickMeasurementHud = create<QuickMeasurementHudState>((set) =>
|
|
|
55
55
|
|
|
56
56
|
export function selectQuickMeasurementHudEntry(
|
|
57
57
|
state: QuickMeasurementHudState,
|
|
58
|
-
|
|
58
|
+
scene: VisibleScene | null,
|
|
59
59
|
): QuickMeasurementHudEntry | null {
|
|
60
|
-
const source =
|
|
60
|
+
const source = scene === 'split' ? state.activeSource : scene
|
|
61
61
|
return source ? state.sources[source] : null
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -17,11 +17,12 @@ type StairBuildPreviewState = {
|
|
|
17
17
|
point: StairPreviewPoint | null
|
|
18
18
|
/** Yaw (radians), cycled by R / T. */
|
|
19
19
|
rotation: number
|
|
20
|
+
rise: number | null
|
|
20
21
|
/** Set the snapped point. No-ops (skips the store update, so subscribers
|
|
21
22
|
* don't re-render) when the point is unchanged — `grid:move` fires far more
|
|
22
23
|
* often than the snapped cell actually changes. */
|
|
23
24
|
setPoint(point: StairPreviewPoint | null): void
|
|
24
|
-
setPreview(point: StairPreviewPoint | null, rotation: number): void
|
|
25
|
+
setPreview(point: StairPreviewPoint | null, rotation: number, rise?: number | null): void
|
|
25
26
|
rotateBy(deltaRadians: number): void
|
|
26
27
|
reset(): void
|
|
27
28
|
}
|
|
@@ -29,6 +30,7 @@ type StairBuildPreviewState = {
|
|
|
29
30
|
export const useStairBuildPreview = create<StairBuildPreviewState>((set) => ({
|
|
30
31
|
point: null,
|
|
31
32
|
rotation: 0,
|
|
33
|
+
rise: null,
|
|
32
34
|
setPoint: (point) =>
|
|
33
35
|
set((state) => {
|
|
34
36
|
const prev = state.point
|
|
@@ -36,18 +38,24 @@ export const useStairBuildPreview = create<StairBuildPreviewState>((set) => ({
|
|
|
36
38
|
if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
|
|
37
39
|
return { point: point ? [point[0], point[1]] : null }
|
|
38
40
|
}),
|
|
39
|
-
setPreview: (point, rotation) =>
|
|
41
|
+
setPreview: (point, rotation, rise) =>
|
|
40
42
|
set((state) => {
|
|
41
43
|
const samePoint =
|
|
42
44
|
(!point && !state.point) ||
|
|
43
45
|
Boolean(point && state.point && state.point[0] === point[0] && state.point[1] === point[1])
|
|
44
|
-
return samePoint && state.rotation === rotation
|
|
46
|
+
return samePoint && state.rotation === rotation && (rise === undefined || rise === state.rise)
|
|
45
47
|
? state
|
|
46
|
-
: {
|
|
48
|
+
: {
|
|
49
|
+
point: point ? [point[0], point[1]] : null,
|
|
50
|
+
rotation,
|
|
51
|
+
rise: rise === undefined ? state.rise : rise,
|
|
52
|
+
}
|
|
47
53
|
}),
|
|
48
54
|
rotateBy: (deltaRadians) => set((state) => ({ rotation: state.rotation + deltaRadians })),
|
|
49
55
|
reset: () =>
|
|
50
56
|
set((state) =>
|
|
51
|
-
state.point === null && state.rotation === 0
|
|
57
|
+
state.point === null && state.rotation === 0 && state.rise === null
|
|
58
|
+
? state
|
|
59
|
+
: { point: null, rotation: 0, rise: null },
|
|
52
60
|
),
|
|
53
61
|
}))
|
|
@@ -10,8 +10,10 @@ import { create } from 'zustand'
|
|
|
10
10
|
* as translucent ghost meshes mid-drag; the 2D `wallFloorplanMoveTarget`
|
|
11
11
|
* writes here so the floor-plan SVG layer can render the same hint.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* cleared on `commit` and by the move overlay's cleanup)
|
|
13
|
+
* Writers: `packages/nodes/src/wall/floorplan-move.ts` (on each `apply`,
|
|
14
|
+
* cleared on `commit` and by the move overlay's cleanup), and the wall push
|
|
15
|
+
* preview in `lib/room-handle-drag.ts` (the pieces a push would create,
|
|
16
|
+
* cleared when the push previews again, commits or cancels).
|
|
15
17
|
* Reader: `packages/editor/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx`.
|
|
16
18
|
*/
|
|
17
19
|
export type WallMoveGhostBridge = {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { type AnyNodeId, useScene, WallNode } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import useInteractionScope from './use-interaction-scope'
|
|
5
|
+
import {
|
|
6
|
+
handleWallRegionDeleteKey,
|
|
7
|
+
installWallRegionSceneSync,
|
|
8
|
+
resolveActiveWallRegion,
|
|
9
|
+
syncWallRegionSelection,
|
|
10
|
+
useWallRegionSelection,
|
|
11
|
+
} from './use-wall-region-handles-selection'
|
|
12
|
+
|
|
13
|
+
type RafFn = (callback: (time: number) => void) => number
|
|
14
|
+
;(globalThis as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (callback) => {
|
|
15
|
+
callback(0)
|
|
16
|
+
return 0
|
|
17
|
+
}
|
|
18
|
+
;(globalThis as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??= () => {}
|
|
19
|
+
|
|
20
|
+
const WALL_ID = 'wall_region-selection' as AnyNodeId
|
|
21
|
+
const OTHER_WALL_ID = 'wall_region-selection-other' as AnyNodeId
|
|
22
|
+
|
|
23
|
+
function keyEvent(key: string, target: unknown = null) {
|
|
24
|
+
const calls = { prevented: 0, stopped: 0 }
|
|
25
|
+
return {
|
|
26
|
+
calls,
|
|
27
|
+
event: {
|
|
28
|
+
key,
|
|
29
|
+
target: target as EventTarget | null,
|
|
30
|
+
preventDefault: () => {
|
|
31
|
+
calls.prevented += 1
|
|
32
|
+
},
|
|
33
|
+
stopPropagation: () => {
|
|
34
|
+
calls.stopped += 1
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The bun env has no DOM: just enough of `document` and elements for the focus check.
|
|
41
|
+
const originalDocument = (globalThis as { document?: unknown }).document
|
|
42
|
+
const body = { tagName: 'BODY', closest: () => null }
|
|
43
|
+
;(globalThis as { document?: unknown }).document = { body, documentElement: { tagName: 'HTML' } }
|
|
44
|
+
|
|
45
|
+
function element(tagName: string, ancestors: string[] = []) {
|
|
46
|
+
return {
|
|
47
|
+
tagName,
|
|
48
|
+
closest: (selector: string) => (ancestors.includes(selector) ? {} : null),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const uninstallSceneSync = installWallRegionSceneSync()
|
|
53
|
+
|
|
54
|
+
const wall = () => useScene.getState().nodes[WALL_ID] as WallNode | undefined
|
|
55
|
+
|
|
56
|
+
describe('active wall region', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
useScene.setState({ nodes: {}, rootNodeIds: [], dirtyNodes: new Set() } as never)
|
|
59
|
+
for (const id of [WALL_ID, OTHER_WALL_ID]) {
|
|
60
|
+
useScene.getState().createNode(
|
|
61
|
+
WallNode.parse({
|
|
62
|
+
id,
|
|
63
|
+
start: [0, 0],
|
|
64
|
+
end: [4, 0],
|
|
65
|
+
faceRegions: [
|
|
66
|
+
{ id: 'r1', face: 'a', v1: 0.9, finish: 'library:paint-white' },
|
|
67
|
+
{ id: 'r2', face: 'b', u0: 1, finish: 'library:paint-white' },
|
|
68
|
+
],
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
useScene.temporal.getState().clear()
|
|
73
|
+
useScene.temporal.getState().resume()
|
|
74
|
+
useViewer.getState().setSelection({ selectedIds: [WALL_ID] })
|
|
75
|
+
useWallRegionSelection.setState({ active: null, stale: null })
|
|
76
|
+
useInteractionScope.getState().end()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
afterAll(() => {
|
|
80
|
+
uninstallSceneSync()
|
|
81
|
+
useWallRegionSelection.setState({ active: null, stale: null })
|
|
82
|
+
useInteractionScope.getState().end()
|
|
83
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
84
|
+
;(globalThis as { document?: unknown }).document = originalDocument
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('Delete removes only the active region, as one undo step', () => {
|
|
88
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r2' })
|
|
89
|
+
const { event, calls } = keyEvent('Delete')
|
|
90
|
+
expect(handleWallRegionDeleteKey(event)).toBe(true)
|
|
91
|
+
expect(calls).toEqual({ prevented: 1, stopped: 1 })
|
|
92
|
+
expect(wall()).toBeDefined()
|
|
93
|
+
expect(wall()!.faceRegions!.map((region) => region.id)).toEqual(['r1'])
|
|
94
|
+
expect(useWallRegionSelection.getState().active).toBeNull()
|
|
95
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(1)
|
|
96
|
+
useScene.temporal.getState().undo()
|
|
97
|
+
expect(wall()!.faceRegions!.map((region) => region.id)).toEqual(['r1', 'r2'])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('Backspace works too; the last region clears the field', () => {
|
|
101
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
102
|
+
expect(handleWallRegionDeleteKey(keyEvent('Backspace').event)).toBe(true)
|
|
103
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r2' })
|
|
104
|
+
expect(handleWallRegionDeleteKey(keyEvent('Backspace').event)).toBe(true)
|
|
105
|
+
expect(wall()!.faceRegions).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('without an active region the key passes through to the wall delete', () => {
|
|
109
|
+
const { event, calls } = keyEvent('Delete')
|
|
110
|
+
expect(handleWallRegionDeleteKey(event)).toBe(false)
|
|
111
|
+
expect(calls).toEqual({ prevented: 0, stopped: 0 })
|
|
112
|
+
expect(wall()!.faceRegions).toHaveLength(2)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('typing in a field and other keys are left alone', () => {
|
|
116
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
117
|
+
const input = { closest: (selector: string) => (selector.includes('input') ? {} : null) }
|
|
118
|
+
expect(handleWallRegionDeleteKey(keyEvent('Backspace', input).event)).toBe(false)
|
|
119
|
+
expect(handleWallRegionDeleteKey(keyEvent('Escape').event)).toBe(false)
|
|
120
|
+
expect(wall()!.faceRegions).toHaveLength(2)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('an active region on a wall that is no longer selected is ignored and cleared', () => {
|
|
124
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
125
|
+
useViewer.getState().setSelection({ selectedIds: [OTHER_WALL_ID] })
|
|
126
|
+
expect(resolveActiveWallRegion()).toBeNull()
|
|
127
|
+
expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(false)
|
|
128
|
+
syncWallRegionSelection([OTHER_WALL_ID])
|
|
129
|
+
expect(useWallRegionSelection.getState().active).toBeNull()
|
|
130
|
+
const other = useScene.getState().nodes[OTHER_WALL_ID] as WallNode
|
|
131
|
+
expect(other.faceRegions).toHaveLength(2)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('a selection that still holds just the wall keeps the active region', () => {
|
|
135
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
136
|
+
syncWallRegionSelection([WALL_ID])
|
|
137
|
+
expect(useWallRegionSelection.getState().active).toEqual({ wallId: WALL_ID, regionId: 'r1' })
|
|
138
|
+
syncWallRegionSelection([WALL_ID, OTHER_WALL_ID])
|
|
139
|
+
expect(useWallRegionSelection.getState().active).toBeNull()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('focus on the body or the canvas acts on the region', () => {
|
|
143
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
144
|
+
expect(handleWallRegionDeleteKey(keyEvent('Delete', body).event)).toBe(true)
|
|
145
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r2' })
|
|
146
|
+
expect(handleWallRegionDeleteKey(keyEvent('Delete', element('CANVAS')).event)).toBe(true)
|
|
147
|
+
expect(wall()!.faceRegions).toBeUndefined()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('focus on a toolbar button leaves the key alone', () => {
|
|
151
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
152
|
+
const { event, calls } = keyEvent('Delete', element('BUTTON'))
|
|
153
|
+
expect(handleWallRegionDeleteKey(event)).toBe(false)
|
|
154
|
+
expect(calls).toEqual({ prevented: 0, stopped: 0 })
|
|
155
|
+
expect(wall()!.faceRegions).toHaveLength(2)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('focus inside the wall region list acts on the region', () => {
|
|
159
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
160
|
+
const row = element('BUTTON', ['[data-wall-region-list]'])
|
|
161
|
+
expect(handleWallRegionDeleteKey(keyEvent('Delete', row).event)).toBe(true)
|
|
162
|
+
expect(wall()!.faceRegions!.map((region) => region.id)).toEqual(['r2'])
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('mid-gesture the key is swallowed: nothing deleted, the wall stays', () => {
|
|
166
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
167
|
+
useInteractionScope
|
|
168
|
+
.getState()
|
|
169
|
+
.begin({ kind: 'handle-drag', nodeId: WALL_ID, handle: 'paint-region-bound' })
|
|
170
|
+
const { event, calls } = keyEvent('Delete')
|
|
171
|
+
expect(handleWallRegionDeleteKey(event)).toBe(true)
|
|
172
|
+
expect(calls).toEqual({ prevented: 1, stopped: 1 })
|
|
173
|
+
expect(wall()!.faceRegions).toHaveLength(2)
|
|
174
|
+
expect(useWallRegionSelection.getState().active).toEqual({ wallId: WALL_ID, regionId: 'r1' })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('mid-gesture without an active region the key passes through', () => {
|
|
178
|
+
useInteractionScope
|
|
179
|
+
.getState()
|
|
180
|
+
.begin({ kind: 'handle-drag', nodeId: WALL_ID, handle: 'paint-region-bound' })
|
|
181
|
+
expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(false)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('a region that vanished under the user swallows the next Delete once', () => {
|
|
185
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
186
|
+
const regions = wall()!.faceRegions!.filter((region) => region.id !== 'r1')
|
|
187
|
+
useScene.getState().updateNode(WALL_ID, { faceRegions: regions })
|
|
188
|
+
expect(useWallRegionSelection.getState().active).toBeNull()
|
|
189
|
+
expect(useWallRegionSelection.getState().stale).toEqual({ wallId: WALL_ID, regionId: 'r1' })
|
|
190
|
+
|
|
191
|
+
const { event, calls } = keyEvent('Delete')
|
|
192
|
+
expect(handleWallRegionDeleteKey(event)).toBe(true)
|
|
193
|
+
expect(calls).toEqual({ prevented: 1, stopped: 1 })
|
|
194
|
+
expect(wall()).toBeDefined()
|
|
195
|
+
expect(wall()!.faceRegions!.map((region) => region.id)).toEqual(['r2'])
|
|
196
|
+
expect(useWallRegionSelection.getState().stale).toBeNull()
|
|
197
|
+
expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('removing the active region with Delete does not leave it stale', () => {
|
|
201
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
202
|
+
expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(true)
|
|
203
|
+
expect(useWallRegionSelection.getState().stale).toBeNull()
|
|
204
|
+
expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(false)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('a selection change clears the stale region, so Delete falls through', () => {
|
|
208
|
+
useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
|
|
209
|
+
const regions = wall()!.faceRegions!.filter((region) => region.id !== 'r1')
|
|
210
|
+
useScene.getState().updateNode(WALL_ID, { faceRegions: regions })
|
|
211
|
+
expect(useWallRegionSelection.getState().stale).not.toBeNull()
|
|
212
|
+
|
|
213
|
+
useViewer.getState().setSelection({ selectedIds: [OTHER_WALL_ID] })
|
|
214
|
+
syncWallRegionSelection([OTHER_WALL_ID])
|
|
215
|
+
useViewer.getState().setSelection({ selectedIds: [WALL_ID] })
|
|
216
|
+
syncWallRegionSelection([WALL_ID])
|
|
217
|
+
expect(useWallRegionSelection.getState().stale).toBeNull()
|
|
218
|
+
const { event, calls } = keyEvent('Delete')
|
|
219
|
+
expect(handleWallRegionDeleteKey(event)).toBe(false)
|
|
220
|
+
expect(calls).toEqual({ prevented: 0, stopped: 0 })
|
|
221
|
+
})
|
|
222
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import { create } from 'zustand'
|
|
4
|
+
import { hasLiveGestures } from '../lib/gesture-lifecycle'
|
|
5
|
+
import { removeWallRegion } from '../lib/paint-regions'
|
|
6
|
+
import { sfxEmitter } from '../lib/sfx-bus'
|
|
7
|
+
import { isTypingTarget } from '../lib/typing-target'
|
|
8
|
+
import useDeleteConfirmation from './use-delete-confirmation'
|
|
9
|
+
import useInteractionScope from './use-interaction-scope'
|
|
10
|
+
|
|
11
|
+
// The paint region the user last picked on the selected wall (a handle, an
|
|
12
|
+
// outline or a panel row). While it is set, Delete removes that region instead
|
|
13
|
+
// of the wall. Cleared whenever the wall selection changes.
|
|
14
|
+
//
|
|
15
|
+
// `stale` remembers an active region that vanished under the user (undo, another
|
|
16
|
+
// edit) while its wall stayed selected: the next Delete is swallowed rather than
|
|
17
|
+
// deleting the wall the user was not aiming at.
|
|
18
|
+
|
|
19
|
+
export type ActiveWallRegion = { wallId: string; regionId: string }
|
|
20
|
+
|
|
21
|
+
type WallRegionSelectionState = {
|
|
22
|
+
active: ActiveWallRegion | null
|
|
23
|
+
stale: ActiveWallRegion | null
|
|
24
|
+
setActive: (active: ActiveWallRegion | null) => void
|
|
25
|
+
setStale: (stale: ActiveWallRegion | null) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useWallRegionSelection = create<WallRegionSelectionState>((set) => ({
|
|
29
|
+
active: null,
|
|
30
|
+
stale: null,
|
|
31
|
+
setActive: (active) =>
|
|
32
|
+
set((state) =>
|
|
33
|
+
state.active?.wallId === active?.wallId && state.active?.regionId === active?.regionId
|
|
34
|
+
? state
|
|
35
|
+
: active
|
|
36
|
+
? { active, stale: null }
|
|
37
|
+
: { active },
|
|
38
|
+
),
|
|
39
|
+
setStale: (stale) => set((state) => (state.stale === stale ? state : { stale })),
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
function isSoleSelection(wallId: string, selectedIds: readonly string[]) {
|
|
43
|
+
return selectedIds.length === 1 && selectedIds[0] === wallId
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function regionExists(wallId: string, regionId: string) {
|
|
47
|
+
const wall = useScene.getState().nodes[wallId as AnyNodeId]
|
|
48
|
+
if (wall?.type !== 'wall') return false
|
|
49
|
+
return wall.faceRegions?.some((region) => region.id === regionId) ?? false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The active region, only while its wall is the sole selection and the region still exists. */
|
|
53
|
+
export function resolveActiveWallRegion(): ActiveWallRegion | null {
|
|
54
|
+
const active = useWallRegionSelection.getState().active
|
|
55
|
+
if (!active) return null
|
|
56
|
+
if (!isSoleSelection(active.wallId, useViewer.getState().selection.selectedIds)) return null
|
|
57
|
+
return regionExists(active.wallId, active.regionId) ? active : null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveStaleWallRegion(): ActiveWallRegion | null {
|
|
61
|
+
const stale = useWallRegionSelection.getState().stale
|
|
62
|
+
if (!stale) return null
|
|
63
|
+
return isSoleSelection(stale.wallId, useViewer.getState().selection.selectedIds) ? stale : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Clears the active (and stale) region unless the selection is still exactly its wall. */
|
|
67
|
+
export function syncWallRegionSelection(selectedIds: readonly string[]) {
|
|
68
|
+
const { active, stale, setActive, setStale } = useWallRegionSelection.getState()
|
|
69
|
+
if (active && !isSoleSelection(active.wallId, selectedIds)) setActive(null)
|
|
70
|
+
if (stale && !isSoleSelection(stale.wallId, selectedIds)) setStale(null)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drops the active region once it no longer exists in the scene. */
|
|
74
|
+
export function syncWallRegionWithScene() {
|
|
75
|
+
const { active, setActive, setStale } = useWallRegionSelection.getState()
|
|
76
|
+
if (!active || regionExists(active.wallId, active.regionId)) return
|
|
77
|
+
setActive(null)
|
|
78
|
+
const wallStillThere = useScene.getState().nodes[active.wallId as AnyNodeId]?.type === 'wall'
|
|
79
|
+
const selectedIds = useViewer.getState().selection.selectedIds
|
|
80
|
+
if (wallStillThere && isSoleSelection(active.wallId, selectedIds)) setStale(active)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Keeps the active region in step with scene edits (undo, remote edits). Returns the unsubscribe. */
|
|
84
|
+
export function installWallRegionSceneSync() {
|
|
85
|
+
return useScene.subscribe((state, previous) => {
|
|
86
|
+
if (state.nodes !== previous.nodes) syncWallRegionWithScene()
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Delete belongs to the region only when focus is on the scene or the wall's
|
|
91
|
+
// region list; a focused toolbar button or panel control keeps its own key.
|
|
92
|
+
function isSceneOrRegionListTarget(target: EventTarget | null) {
|
|
93
|
+
if (!target) return true
|
|
94
|
+
if (
|
|
95
|
+
typeof document !== 'undefined' &&
|
|
96
|
+
(target === document || target === document.body || target === document.documentElement)
|
|
97
|
+
) {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
const element = target as { tagName?: string; closest?: (selector: string) => unknown }
|
|
101
|
+
if (element.tagName?.toUpperCase() === 'CANVAS') return true
|
|
102
|
+
return !!element.closest?.('[data-wall-region-list]')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function consume(event: KeyEventLike) {
|
|
106
|
+
event.preventDefault()
|
|
107
|
+
event.stopPropagation()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type KeyEventLike = Pick<KeyboardEvent, 'key' | 'target' | 'preventDefault' | 'stopPropagation'>
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Delete / Backspace with an active region removes that region (one undo step)
|
|
114
|
+
* and consumes the key so the global delete never sees it. Mid-gesture, or
|
|
115
|
+
* right after the active region vanished, the key is swallowed so it cannot
|
|
116
|
+
* fall through to deleting the wall. Returns whether the key was consumed.
|
|
117
|
+
*/
|
|
118
|
+
export function handleWallRegionDeleteKey(event: KeyEventLike): boolean {
|
|
119
|
+
if (event.key !== 'Delete' && event.key !== 'Backspace') return false
|
|
120
|
+
if (isTypingTarget(event.target)) return false
|
|
121
|
+
if (!isSceneOrRegionListTarget(event.target)) return false
|
|
122
|
+
if (useDeleteConfirmation.getState().request) return false
|
|
123
|
+
const active = resolveActiveWallRegion()
|
|
124
|
+
const stale = active ? null : resolveStaleWallRegion()
|
|
125
|
+
if (!(active || stale)) return false
|
|
126
|
+
|
|
127
|
+
consume(event)
|
|
128
|
+
const busy = useInteractionScope.getState().scope.kind !== 'idle' || hasLiveGestures()
|
|
129
|
+
if (busy) return true
|
|
130
|
+
if (stale) {
|
|
131
|
+
useWallRegionSelection.getState().setStale(null)
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
if (!active) return true
|
|
135
|
+
// Cleared first so the scene sync does not mistake this removal for a vanished region.
|
|
136
|
+
useWallRegionSelection.getState().setActive(null)
|
|
137
|
+
if (removeWallRegion(active.wallId, active.regionId)) sfxEmitter.emit('sfx:structure-delete')
|
|
138
|
+
return true
|
|
139
|
+
}
|