@pascal-app/editor 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +132 -0
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { type AnyNode, DoorNode, type OpenWallEnd, WallNode } from '@pascal-app/core'
|
|
3
|
+
import { analyseOpenWallEnds } from './use-open-wall-ends'
|
|
4
|
+
|
|
5
|
+
const wall = (id: string, start: [number, number], end: [number, number]) =>
|
|
6
|
+
({
|
|
7
|
+
id,
|
|
8
|
+
type: 'wall',
|
|
9
|
+
object: 'node',
|
|
10
|
+
parentId: 'level_a',
|
|
11
|
+
visible: true,
|
|
12
|
+
children: [],
|
|
13
|
+
metadata: {},
|
|
14
|
+
start,
|
|
15
|
+
end,
|
|
16
|
+
thickness: 0.1,
|
|
17
|
+
}) as unknown as WallNode
|
|
18
|
+
|
|
19
|
+
// A 4 x 4 room whose last corner is left 4 cm open (bodies are 0.1 thick).
|
|
20
|
+
const walls = () => [
|
|
21
|
+
wall('wall_1', [0, 0], [4, 0]),
|
|
22
|
+
wall('wall_2', [4, 0], [4, 4]),
|
|
23
|
+
wall('wall_3', [4, 4], [0, 4]),
|
|
24
|
+
wall('wall_4', [0, 4], [0, 0.09]),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
describe('open wall end analysis shared by the floor plan and 3D view', () => {
|
|
28
|
+
test('finds the open corner', () => {
|
|
29
|
+
const ends = analyseOpenWallEnds('level_a', walls())
|
|
30
|
+
expect(ends.some((end) => end.wallId === 'wall_4' && end.end === 'end')).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('two views reading the same walls get one result', () => {
|
|
34
|
+
const shared = walls()
|
|
35
|
+
const first = analyseOpenWallEnds('level_a', [...shared])
|
|
36
|
+
expect(analyseOpenWallEnds('level_a', [...shared])).toBe(first)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('a changed wall is analysed again', () => {
|
|
40
|
+
const shared = walls()
|
|
41
|
+
const first = analyseOpenWallEnds('level_a', shared)
|
|
42
|
+
const closed = [...shared.slice(0, 3), wall('wall_4', [0, 4], [0, 0])]
|
|
43
|
+
const next = analyseOpenWallEnds('level_a', closed)
|
|
44
|
+
expect(next).not.toBe(first)
|
|
45
|
+
expect(next.some((end) => end.wallId === 'wall_4')).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('the open-end preview reads what the walls host', () => {
|
|
50
|
+
// An 88° near-miss T: the source wall stops 9 cm short of the long target wall.
|
|
51
|
+
const level = 'level_t'
|
|
52
|
+
const tWall = (id: string, start: [number, number], end: [number, number], thickness = 0.1) =>
|
|
53
|
+
WallNode.parse({ id, parentId: level, start, end, thickness })
|
|
54
|
+
const scene = () => {
|
|
55
|
+
const tip: [number, number] = [2 + (3 - 0.09) / Math.tan((88 * Math.PI) / 180), 0.09]
|
|
56
|
+
const source = tWall('wall_source', [2, 3], tip, 0.01)
|
|
57
|
+
return {
|
|
58
|
+
source,
|
|
59
|
+
walls: [
|
|
60
|
+
source,
|
|
61
|
+
tWall('wall_target', [-20, 0], [30, 0], 0.01),
|
|
62
|
+
tWall('wall_anchor', [2, 3], [-20, 3]),
|
|
63
|
+
tWall('wall_left', [-20, 3], [-20, 0]),
|
|
64
|
+
],
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const sourceEnd = (ends: OpenWallEnd[]) =>
|
|
68
|
+
ends.find((end) => end.wallId === 'wall_source' && end.end === 'end')
|
|
69
|
+
|
|
70
|
+
test('without an opening the preview squares the corner', () => {
|
|
71
|
+
const point = sourceEnd(analyseOpenWallEnds(level, scene().walls))?.candidate?.point
|
|
72
|
+
expect(point?.[0]).toBeCloseTo(2, 6)
|
|
73
|
+
expect(point?.[1]).toBeCloseTo(0, 6)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('a door flush with the anchored end keeps the join straight, and only the door changing re-runs it', () => {
|
|
77
|
+
const { source, walls } = scene()
|
|
78
|
+
const door = DoorNode.parse({
|
|
79
|
+
id: 'door_source',
|
|
80
|
+
parentId: source.id,
|
|
81
|
+
wallId: source.id,
|
|
82
|
+
position: [0.4, 1.05, 0],
|
|
83
|
+
width: 0.8,
|
|
84
|
+
})
|
|
85
|
+
const hosted: AnyNode[] = [
|
|
86
|
+
{ ...source, children: [door.id] } as AnyNode,
|
|
87
|
+
...walls.slice(1),
|
|
88
|
+
door as AnyNode,
|
|
89
|
+
]
|
|
90
|
+
const first = analyseOpenWallEnds(level, hosted)
|
|
91
|
+
const point = sourceEnd(first)?.candidate?.point
|
|
92
|
+
expect(point?.[0]).toBeCloseTo(2 + 3 / Math.tan((88 * Math.PI) / 180), 6)
|
|
93
|
+
expect(point?.[1]).toBeCloseTo(0, 6)
|
|
94
|
+
|
|
95
|
+
const narrower = { ...door, width: 0.6 } as AnyNode
|
|
96
|
+
const next = analyseOpenWallEnds(level, [...hosted.slice(0, -1), narrower])
|
|
97
|
+
expect(next).not.toBe(first)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNode, findOpenWallEnds, type OpenWallEnd, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
6
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
7
|
+
import {
|
|
8
|
+
collapseMutualOpenWallEnds,
|
|
9
|
+
joinOpenWallEnd,
|
|
10
|
+
openWallEndKey,
|
|
11
|
+
visibleOpenWallEnds,
|
|
12
|
+
} from '../lib/floorplan/open-wall-ends'
|
|
13
|
+
import { sfxEmitter } from '../lib/sfx-bus'
|
|
14
|
+
import useEditor from '../store/use-editor'
|
|
15
|
+
import { useFloorplanDraftPreview } from '../store/use-floorplan-draft-preview'
|
|
16
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
17
|
+
|
|
18
|
+
type Boundary = Extract<AnyNode, { type: 'wall' | 'separator' }>
|
|
19
|
+
|
|
20
|
+
const NO_ENDS: OpenWallEnd[] = []
|
|
21
|
+
|
|
22
|
+
const isBoundary = (node: AnyNode): node is Boundary =>
|
|
23
|
+
node.type === 'wall' || node.type === 'separator'
|
|
24
|
+
|
|
25
|
+
let lastAnalysis: {
|
|
26
|
+
levelId: string
|
|
27
|
+
nodes: readonly AnyNode[]
|
|
28
|
+
ends: OpenWallEnd[]
|
|
29
|
+
} | null = null
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The room-graph pass behind the open-end markers, shared by the floor plan
|
|
33
|
+
* and the 3D view: split view mounts both, and they must not pay for it twice.
|
|
34
|
+
* `nodes` is the level's walls and separators plus what the walls host: an
|
|
35
|
+
* opening can stop core squaring a corner, and the preview must land where
|
|
36
|
+
* Join walls will. Recomputed only when one of those nodes changes (compared
|
|
37
|
+
* by reference), never per frame or on unrelated edits.
|
|
38
|
+
*/
|
|
39
|
+
export function analyseOpenWallEnds(levelId: string, nodes: readonly AnyNode[]): OpenWallEnd[] {
|
|
40
|
+
const last = lastAnalysis
|
|
41
|
+
if (
|
|
42
|
+
last &&
|
|
43
|
+
last.levelId === levelId &&
|
|
44
|
+
last.nodes.length === nodes.length &&
|
|
45
|
+
last.nodes.every((node, index) => node === nodes[index])
|
|
46
|
+
) {
|
|
47
|
+
return last.ends
|
|
48
|
+
}
|
|
49
|
+
const ends = findOpenWallEnds(Object.fromEntries(nodes.map((node) => [node.id, node])), levelId)
|
|
50
|
+
lastAnalysis = { levelId, nodes, ends }
|
|
51
|
+
return ends
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Moving, placing, reshaping or dragging a handle: the markers step aside. */
|
|
55
|
+
export function useOpenWallEndsSuppressed(): boolean {
|
|
56
|
+
return useInteractionScope(
|
|
57
|
+
(state) =>
|
|
58
|
+
state.scope.kind === 'moving' ||
|
|
59
|
+
state.scope.kind === 'placing' ||
|
|
60
|
+
state.scope.kind === 'reshaping' ||
|
|
61
|
+
state.scope.kind === 'handle-drag',
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type OpenWallEndsState = {
|
|
66
|
+
/** The wall tool (any variant) or Divide is active. */
|
|
67
|
+
drawing: boolean
|
|
68
|
+
/** A wall or divide segment is mid-draft: markers stay visible but out of the way. */
|
|
69
|
+
drafting: boolean
|
|
70
|
+
/** Ends to mark, mutual pairs collapsed to the end that slides straight. */
|
|
71
|
+
ends: OpenWallEnd[]
|
|
72
|
+
/** Walls on the active level that bound no room. */
|
|
73
|
+
roomlessWallIds: string[]
|
|
74
|
+
/** Whether the overlay is up at all (drawing, or a near miss to show). */
|
|
75
|
+
shown: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Open wall ends on the active level and when to show them. On while walls or
|
|
80
|
+
* rooms are being drawn; otherwise only when a wall that bounds no room has a
|
|
81
|
+
* near miss.
|
|
82
|
+
*/
|
|
83
|
+
export function useOpenWallEnds(): OpenWallEndsState {
|
|
84
|
+
const levelId = useViewer((state) => state.selection.levelId)
|
|
85
|
+
const mode = useEditor((state) => state.mode)
|
|
86
|
+
const tool = useEditor((state) => state.tool)
|
|
87
|
+
const spaces = useEditor(
|
|
88
|
+
useShallow((state) => Object.values(state.spaces).filter((space) => space.levelId === levelId)),
|
|
89
|
+
)
|
|
90
|
+
const isDividing = useInteractionScope((state) => state.scope.kind === 'room-divide')
|
|
91
|
+
const dividingDraft = useInteractionScope(
|
|
92
|
+
(state) => state.scope.kind === 'room-divide' && state.scope.points.length > 0,
|
|
93
|
+
)
|
|
94
|
+
const wallDrafting = useFloorplanDraftPreview(
|
|
95
|
+
(state) =>
|
|
96
|
+
state.wallDraftStart !== null ||
|
|
97
|
+
state.wallRectangleDraftStart !== null ||
|
|
98
|
+
state.wallPolygonDraftPoints.length > 0,
|
|
99
|
+
)
|
|
100
|
+
// Walls and separators first, then every node a wall hosts — one flat list,
|
|
101
|
+
// so a door moving re-runs the analysis but an unrelated item edit does not.
|
|
102
|
+
const analysisNodes = useScene(
|
|
103
|
+
useShallow((state) => {
|
|
104
|
+
const level = levelId ? state.nodes[levelId as AnyNode['id']] : undefined
|
|
105
|
+
if (level?.type !== 'level') return [] as AnyNode[]
|
|
106
|
+
const boundaries = level.children
|
|
107
|
+
.map((id) => state.nodes[id])
|
|
108
|
+
.filter((node): node is Boundary => !!node && isBoundary(node))
|
|
109
|
+
const hosted = boundaries.flatMap((node) =>
|
|
110
|
+
node.type === 'wall'
|
|
111
|
+
? node.children
|
|
112
|
+
.map((id) => state.nodes[id])
|
|
113
|
+
.filter((child): child is AnyNode => child !== undefined)
|
|
114
|
+
: [],
|
|
115
|
+
)
|
|
116
|
+
return [...boundaries, ...hosted]
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
const boundaries = useMemo(() => analysisNodes.filter(isBoundary), [analysisNodes])
|
|
120
|
+
|
|
121
|
+
const drawing = (mode === 'build' && tool === 'wall') || isDividing
|
|
122
|
+
const roomWallIds = useMemo(() => {
|
|
123
|
+
const ids = new Set<string>()
|
|
124
|
+
for (const space of spaces) for (const wallId of space.wallIds) ids.add(wallId)
|
|
125
|
+
return ids
|
|
126
|
+
}, [spaces])
|
|
127
|
+
const roomlessWallIds = useMemo(
|
|
128
|
+
() =>
|
|
129
|
+
boundaries
|
|
130
|
+
.filter((node) => node.type === 'wall' && !roomWallIds.has(node.id))
|
|
131
|
+
.map((node) => node.id as string),
|
|
132
|
+
[boundaries, roomWallIds],
|
|
133
|
+
)
|
|
134
|
+
const hasWalls = boundaries.some((node) => node.type === 'wall')
|
|
135
|
+
const shouldAnalyse = !!levelId && hasWalls && (drawing || roomlessWallIds.length > 0)
|
|
136
|
+
const openEnds = useMemo(
|
|
137
|
+
() => (shouldAnalyse && levelId ? analyseOpenWallEnds(levelId, analysisNodes) : NO_ENDS),
|
|
138
|
+
[shouldAnalyse, levelId, analysisNodes],
|
|
139
|
+
)
|
|
140
|
+
const ends = useMemo(() => {
|
|
141
|
+
const walls = new Map(boundaries.map((node) => [node.id as string, node]))
|
|
142
|
+
return collapseMutualOpenWallEnds(
|
|
143
|
+
visibleOpenWallEnds(openEnds, roomWallIds, drawing),
|
|
144
|
+
(wallId) => {
|
|
145
|
+
const wall = walls.get(wallId)
|
|
146
|
+
return wall?.type === 'wall'
|
|
147
|
+
? [wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]]
|
|
148
|
+
: null
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
}, [openEnds, roomWallIds, drawing, boundaries])
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
drawing,
|
|
155
|
+
drafting: wallDrafting || dividingDraft,
|
|
156
|
+
ends,
|
|
157
|
+
roomlessWallIds,
|
|
158
|
+
shown: drawing || ends.length > 0,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const HOVER_RELEASE_MS = 250
|
|
163
|
+
|
|
164
|
+
function clearReleaseTimer(timer: { current: ReturnType<typeof setTimeout> | null }) {
|
|
165
|
+
if (timer.current) clearTimeout(timer.current)
|
|
166
|
+
timer.current = null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Hover / pin / join for one view's markers — the floor plan and the 3D view
|
|
171
|
+
* each keep their own, so hovering a dot in one does not pop a pill in the
|
|
172
|
+
* other. A pinned pill closes on Escape or a press outside any
|
|
173
|
+
* `[data-open-wall-end]` element.
|
|
174
|
+
*/
|
|
175
|
+
export function useOpenWallEndFocus(ends: readonly OpenWallEnd[], drafting: boolean) {
|
|
176
|
+
const [hoveredKey, setHoveredKey] = useState<string | null>(null)
|
|
177
|
+
const [pinnedKey, setPinnedKey] = useState<string | null>(null)
|
|
178
|
+
const [refusal, setRefusal] = useState<{ key: string; message: string } | null>(null)
|
|
179
|
+
const releaseTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
180
|
+
|
|
181
|
+
const clearTimer = () => clearReleaseTimer(releaseTimer)
|
|
182
|
+
useEffect(() => () => clearReleaseTimer(releaseTimer), [])
|
|
183
|
+
|
|
184
|
+
const activeKey = pinnedKey ?? hoveredKey
|
|
185
|
+
const activeEnd = drafting
|
|
186
|
+
? null
|
|
187
|
+
: (ends.find((end) => end.candidate && openWallEndKey(end) === activeKey) ?? null)
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!pinnedKey) return
|
|
191
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
192
|
+
const target = event.target instanceof Element ? event.target : null
|
|
193
|
+
if (target?.closest('[data-open-wall-end]')) return
|
|
194
|
+
setPinnedKey(null)
|
|
195
|
+
}
|
|
196
|
+
window.addEventListener('pointerdown', onPointerDown, true)
|
|
197
|
+
return () => window.removeEventListener('pointerdown', onPointerDown, true)
|
|
198
|
+
}, [pinnedKey])
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!activeEnd) return
|
|
202
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
203
|
+
if (event.key !== 'Escape') return
|
|
204
|
+
event.stopPropagation()
|
|
205
|
+
clearReleaseTimer(releaseTimer)
|
|
206
|
+
setPinnedKey(null)
|
|
207
|
+
setHoveredKey(null)
|
|
208
|
+
setRefusal(null)
|
|
209
|
+
}
|
|
210
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
211
|
+
return () => window.removeEventListener('keydown', onKeyDown, true)
|
|
212
|
+
}, [activeEnd])
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
activeKey,
|
|
216
|
+
activeEnd,
|
|
217
|
+
refusal: activeEnd && refusal?.key === openWallEndKey(activeEnd) ? refusal.message : null,
|
|
218
|
+
keepHover: (key: string) => {
|
|
219
|
+
clearTimer()
|
|
220
|
+
setHoveredKey(key)
|
|
221
|
+
},
|
|
222
|
+
releaseHover: () => {
|
|
223
|
+
clearTimer()
|
|
224
|
+
releaseTimer.current = setTimeout(() => setHoveredKey(null), HOVER_RELEASE_MS)
|
|
225
|
+
},
|
|
226
|
+
togglePin: (key: string) => setPinnedKey((current) => (current === key ? null : key)),
|
|
227
|
+
pin: (key: string) => setPinnedKey(key),
|
|
228
|
+
join: (end: OpenWallEnd) => {
|
|
229
|
+
const key = openWallEndKey(end)
|
|
230
|
+
const message = joinOpenWallEnd(end)
|
|
231
|
+
setRefusal(message ? { key, message } : null)
|
|
232
|
+
if (message) return
|
|
233
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
234
|
+
setPinnedKey(null)
|
|
235
|
+
setHoveredKey(null)
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { type AnyNode, registerNode } from '@pascal-app/core'
|
|
3
|
+
import { placementNoticeOf, transientDraftId } from './use-placement-notice'
|
|
4
|
+
|
|
5
|
+
// The editor asks a node's definition for its placement notice; it names no kind.
|
|
6
|
+
function registerKind(placementNotice?: unknown): string {
|
|
7
|
+
const kind = `test:notice-${crypto.randomUUID()}`
|
|
8
|
+
registerNode({
|
|
9
|
+
kind,
|
|
10
|
+
schemaVersion: 1,
|
|
11
|
+
category: 'furnish',
|
|
12
|
+
defaults: () => ({}),
|
|
13
|
+
capabilities: {},
|
|
14
|
+
...(placementNotice ? { placementNotice } : {}),
|
|
15
|
+
} as never)
|
|
16
|
+
return kind
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const nodeOf = (id: string, type: string, metadata?: Record<string, unknown>) =>
|
|
20
|
+
({ id, type, parentId: 'level_a', metadata: metadata ?? {} }) as unknown as AnyNode
|
|
21
|
+
|
|
22
|
+
describe('the placement notice of the node in hand', () => {
|
|
23
|
+
test("comes from the node's definition, at its live pose", () => {
|
|
24
|
+
const kind = registerKind((node: AnyNode, ctx: { live?: { position: number[] } }) => ({
|
|
25
|
+
line: `${node.id} at ${ctx.live?.position.join(',') ?? 'rest'}`,
|
|
26
|
+
}))
|
|
27
|
+
const nodes = { a: nodeOf('a', kind) }
|
|
28
|
+
expect(placementNoticeOf(nodes, 'a')).toEqual({ line: 'a at rest' })
|
|
29
|
+
expect(placementNoticeOf(nodes, 'a', { position: [1, 0, 2], rotation: 0 })).toEqual({
|
|
30
|
+
line: 'a at 1,0,2',
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('is null for a kind that declares none, and for a node not in the scene', () => {
|
|
35
|
+
const nodes = { b: nodeOf('b', registerKind()) }
|
|
36
|
+
expect(placementNoticeOf(nodes, 'b')).toBeNull()
|
|
37
|
+
expect(placementNoticeOf(nodes, 'missing')).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("a tool's transient draft is found whatever its kind", () => {
|
|
41
|
+
const nodes = {
|
|
42
|
+
kept: nodeOf('kept', registerKind()),
|
|
43
|
+
draft: nodeOf('draft', registerKind(), { isTransient: true }),
|
|
44
|
+
}
|
|
45
|
+
expect(transientDraftId(nodes) as string | null).toBe('draft')
|
|
46
|
+
expect(transientDraftId({ kept: nodes.kept })).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
type LiveTransformLike,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
type PlacementNotice,
|
|
7
|
+
useLiveTransforms,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { useMemo } from 'react'
|
|
11
|
+
|
|
12
|
+
/** The notice `nodeId`'s kind gives for where it stands (`placementNotice`), if it declares one. */
|
|
13
|
+
export function placementNoticeOf(
|
|
14
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
15
|
+
nodeId: string,
|
|
16
|
+
live?: LiveTransformLike,
|
|
17
|
+
): PlacementNotice | null {
|
|
18
|
+
const node = nodes[nodeId as AnyNodeId]
|
|
19
|
+
const notice = node ? nodeRegistry.get(node.type)?.placementNotice : undefined
|
|
20
|
+
if (!(node && notice)) return null
|
|
21
|
+
return notice(node as never, live ? { nodes, live } : { nodes })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What the node in hand, or the selected node, says about where it stands, read at its live pose
|
|
26
|
+
* while it is placed or moved. Subscribes to the scene only while a node is given, so an
|
|
27
|
+
* always-mounted caller doesn't re-render on every write.
|
|
28
|
+
*/
|
|
29
|
+
export function usePlacementNotice(nodeId: string | null | undefined): PlacementNotice | null {
|
|
30
|
+
const nodes = useScene((state) => (nodeId ? state.nodes : null))
|
|
31
|
+
const live = useLiveTransforms((state) => (nodeId ? state.get(nodeId) : undefined))
|
|
32
|
+
return useMemo(
|
|
33
|
+
() => (nodeId && nodes ? placementNoticeOf(nodes, nodeId, live) : null),
|
|
34
|
+
[nodes, live, nodeId],
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The node a construction tool is placing: its transient draft. */
|
|
39
|
+
export const transientDraftId = (nodes: Readonly<Record<string, AnyNode>>) =>
|
|
40
|
+
Object.values(nodes).find(
|
|
41
|
+
(node) => !!(node.metadata as { isTransient?: boolean } | null)?.isTransient,
|
|
42
|
+
)?.id ?? null
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { useCallback, useSyncExternalStore } from 'react'
|
|
6
|
+
import { selectionEnabled } from '../lib/interaction/scope'
|
|
7
|
+
import {
|
|
8
|
+
type RoomKey,
|
|
9
|
+
RoomSelectionIndex,
|
|
10
|
+
type RoomSelectionRecord,
|
|
11
|
+
resolveRoomHit,
|
|
12
|
+
} from '../lib/room-selection'
|
|
13
|
+
import { shouldInterceptRoom } from '../lib/room-selection-commands'
|
|
14
|
+
import { type SelectionModifierKeys, selectionModifiersFromEvent } from '../lib/selection-routing'
|
|
15
|
+
import useEditor from '../store/use-editor'
|
|
16
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
17
|
+
|
|
18
|
+
const indexes = new Map<string, RoomSelectionIndex>()
|
|
19
|
+
const subscriptions = new Map<string, { listeners: Set<() => void>; dispose: () => void }>()
|
|
20
|
+
const EMPTY: RoomSelectionRecord[] = []
|
|
21
|
+
|
|
22
|
+
export function getRoomSelectionIndex(levelId: string) {
|
|
23
|
+
let index = indexes.get(levelId)
|
|
24
|
+
if (!index) {
|
|
25
|
+
index = new RoomSelectionIndex(levelId)
|
|
26
|
+
indexes.set(levelId, index)
|
|
27
|
+
}
|
|
28
|
+
index.update(useScene.getState().nodes)
|
|
29
|
+
return index
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function subscribeLevel(levelId: string | null, listener: () => void) {
|
|
33
|
+
if (!levelId) return () => {}
|
|
34
|
+
let subscription = subscriptions.get(levelId)
|
|
35
|
+
if (!subscription) {
|
|
36
|
+
const listeners = new Set<() => void>()
|
|
37
|
+
const dispose = useScene.subscribe((state, previous) => {
|
|
38
|
+
if (state.nodes === previous.nodes) return
|
|
39
|
+
const rooms = getRoomSelectionIndex(levelId).update(state.nodes)
|
|
40
|
+
const editor = useEditor.getState()
|
|
41
|
+
if (
|
|
42
|
+
editor.room?.levelId === levelId &&
|
|
43
|
+
!rooms.some((room) => room.key.zoneId === editor.room?.zoneId)
|
|
44
|
+
)
|
|
45
|
+
editor.clearRoom()
|
|
46
|
+
for (const notify of listeners) notify()
|
|
47
|
+
})
|
|
48
|
+
subscription = { listeners, dispose }
|
|
49
|
+
subscriptions.set(levelId, subscription)
|
|
50
|
+
}
|
|
51
|
+
subscription.listeners.add(listener)
|
|
52
|
+
return () => {
|
|
53
|
+
subscription.listeners.delete(listener)
|
|
54
|
+
if (!subscription.listeners.size) {
|
|
55
|
+
subscription.dispose()
|
|
56
|
+
subscriptions.delete(levelId)
|
|
57
|
+
indexes.delete(levelId)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useRoomRecords(levelId: string | null) {
|
|
63
|
+
const subscribe = useCallback((notify: () => void) => subscribeLevel(levelId, notify), [levelId])
|
|
64
|
+
const snapshot = useCallback(
|
|
65
|
+
() => (levelId ? getRoomSelectionIndex(levelId).update(useScene.getState().nodes) : EMPTY),
|
|
66
|
+
[levelId],
|
|
67
|
+
)
|
|
68
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function useSelectedRoom() {
|
|
72
|
+
const key = useEditor((state) => state.room)
|
|
73
|
+
const rooms = useRoomRecords(key?.levelId ?? null)
|
|
74
|
+
return rooms.find((room) => room.key.zoneId === key?.zoneId) ?? null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The room a selected zone is, for the room panel: its record when the room
|
|
79
|
+
* index knows it, `{ record: null }` for a room the walls do not close, null
|
|
80
|
+
* for anything that is not a room.
|
|
81
|
+
*/
|
|
82
|
+
export function useZoneRoom(zoneId: string | null): { record: RoomSelectionRecord | null } | null {
|
|
83
|
+
const levelId = useScene((state) => {
|
|
84
|
+
const zone = zoneId ? state.nodes[zoneId as AnyNodeId] : undefined
|
|
85
|
+
return zone?.type === 'zone' && zone.spaceRole === 'room' ? (zone.parentId ?? null) : null
|
|
86
|
+
})
|
|
87
|
+
const rooms = useRoomRecords(levelId)
|
|
88
|
+
if (!(zoneId && levelId)) return null
|
|
89
|
+
return { record: rooms.find((room) => room.key.zoneId === zoneId) ?? null }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function useHighlightedRoom() {
|
|
93
|
+
const key = useEditor((state) => state.hoveredRoom ?? state.room)
|
|
94
|
+
const rooms = useRoomRecords(key?.levelId ?? null)
|
|
95
|
+
return rooms.find((room) => room.key.zoneId === key?.zoneId) ?? null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const hitPoint: [number, number] = [0, 0]
|
|
99
|
+
|
|
100
|
+
export function resolveEditorRoomHit(
|
|
101
|
+
node: AnyNode | null,
|
|
102
|
+
levelId: string,
|
|
103
|
+
point: readonly [number, number],
|
|
104
|
+
view: '2d' | '3d',
|
|
105
|
+
) {
|
|
106
|
+
if (node && node.type !== 'wall' && node.type !== 'slab' && node.type !== 'ceiling') return null
|
|
107
|
+
const index = getRoomSelectionIndex(levelId)
|
|
108
|
+
hitPoint[0] = point[0]
|
|
109
|
+
hitPoint[1] = point[1]
|
|
110
|
+
const room = resolveRoomHit(index, levelId, node, hitPoint, view)
|
|
111
|
+
return room?.key ?? null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Rooms pick inside the building; from site a click picks the whole building. */
|
|
115
|
+
export function roomPickingEnabled() {
|
|
116
|
+
const editor = useEditor.getState()
|
|
117
|
+
return (
|
|
118
|
+
editor.phase !== 'site' &&
|
|
119
|
+
editor.mode === 'select' &&
|
|
120
|
+
!useViewer.getState().focusedUnitId &&
|
|
121
|
+
selectionEnabled(useInteractionScope.getState().scope)
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function hoverRoomFromHit(
|
|
126
|
+
hit: RoomKey | null,
|
|
127
|
+
modifiers: SelectionModifierKeys,
|
|
128
|
+
nodeId: string | null,
|
|
129
|
+
) {
|
|
130
|
+
const editor = useEditor.getState()
|
|
131
|
+
const room = shouldInterceptRoom(hit, modifiers, nodeId) ? hit : null
|
|
132
|
+
editor.setHoveredRoom(room)
|
|
133
|
+
if (room) useViewer.getState().setHoveredId(null)
|
|
134
|
+
return !!room
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function resolvePlanRoomHit(nodeId: string | null, point: readonly [number, number]) {
|
|
138
|
+
const node = nodeId ? useScene.getState().nodes[nodeId as AnyNodeId] : null
|
|
139
|
+
const levelId = node?.parentId ?? useViewer.getState().selection.levelId
|
|
140
|
+
return levelId ? resolveEditorRoomHit(node ?? null, levelId, point, '2d') : null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { selectionModifiersFromEvent }
|