@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,569 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
area,
|
|
4
|
+
type BuildingNode,
|
|
5
|
+
buildUnitReport,
|
|
6
|
+
type CameraControlFrameEvent,
|
|
7
|
+
containsPoint,
|
|
8
|
+
DEFAULT_LEVEL_HEIGHT,
|
|
9
|
+
emitter,
|
|
10
|
+
getLevelDisplayName,
|
|
11
|
+
type LevelNode,
|
|
12
|
+
type NodeEvent,
|
|
13
|
+
resolveSelectionProxyId,
|
|
14
|
+
sceneRegistry,
|
|
15
|
+
type UnitNode,
|
|
16
|
+
type ZoneNode,
|
|
17
|
+
} from '@pascal-app/core'
|
|
18
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
19
|
+
import type { CameraControlsImpl } from '@react-three/drei'
|
|
20
|
+
import { useEffect } from 'react'
|
|
21
|
+
import {
|
|
22
|
+
Box3,
|
|
23
|
+
type Camera,
|
|
24
|
+
type Object3D,
|
|
25
|
+
type OrthographicCamera,
|
|
26
|
+
type PerspectiveCamera,
|
|
27
|
+
Sphere,
|
|
28
|
+
Vector3,
|
|
29
|
+
} from 'three'
|
|
30
|
+
import { nodeDisplayLabel } from './node-display-label'
|
|
31
|
+
import { roomFloorElevation } from './room-handle-drag'
|
|
32
|
+
import { RoomSelectionIndex, type RoomSelectionRecord, resolveRoomHit } from './room-selection'
|
|
33
|
+
import type { SelectionModifierKeys } from './selection-routing'
|
|
34
|
+
|
|
35
|
+
// One selection model for every viewer surface: the published viewer (baked
|
|
36
|
+
// GLB or parametric), its embed and the editor's Preview. Each renderer only
|
|
37
|
+
// turns a pointer hit into a `ViewerPick`; these rules decide what it selects,
|
|
38
|
+
// on `useViewer.selection`:
|
|
39
|
+
// levelId the current floor
|
|
40
|
+
// zoneId the selected room (or zone)
|
|
41
|
+
// selectedIds the selected element or item
|
|
42
|
+
|
|
43
|
+
export type ViewerNodes = Readonly<Record<string, AnyNode>>
|
|
44
|
+
|
|
45
|
+
/** The scene a viewer surface shows: its graph, and the 3D object standing for an id. */
|
|
46
|
+
export type ViewerSource = {
|
|
47
|
+
nodes: ViewerNodes
|
|
48
|
+
objectFor: (id: string) => Object3D | undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A pointer hit, whichever renderer took it: the node and the hit in its level's local XZ. */
|
|
52
|
+
export type ViewerPick = { nodeId: string; point: readonly [number, number] | null }
|
|
53
|
+
|
|
54
|
+
export type ViewerSelection = {
|
|
55
|
+
buildingId: BuildingNode['id'] | null
|
|
56
|
+
levelId: LevelNode['id'] | null
|
|
57
|
+
zoneId: ZoneNode['id'] | null
|
|
58
|
+
selectedIds: string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type ViewerTarget =
|
|
62
|
+
| { kind: 'room'; levelId: string; zoneId: string }
|
|
63
|
+
| { kind: 'node'; levelId: string | null; nodeId: string; zoneId: string | null }
|
|
64
|
+
|
|
65
|
+
const NO_MODIFIERS: SelectionModifierKeys = { alt: false, ctrl: false, meta: false, shift: false }
|
|
66
|
+
|
|
67
|
+
/** Surfaces a room is picked through, room first: its walls, its floor, its ceiling. */
|
|
68
|
+
const ROOM_SURFACES = new Set(['wall', 'slab', 'ceiling'])
|
|
69
|
+
|
|
70
|
+
/** Kinds a viewer click never lands on: they pass the pointer to what lies behind. */
|
|
71
|
+
export const VIEWER_PASS_THROUGH_KINDS: ReadonlySet<string> = new Set([
|
|
72
|
+
'site',
|
|
73
|
+
'building',
|
|
74
|
+
'level',
|
|
75
|
+
'zone',
|
|
76
|
+
'ceiling',
|
|
77
|
+
'scan',
|
|
78
|
+
'guide',
|
|
79
|
+
'spawn',
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
const indexes = new WeakMap<ViewerNodes, Map<string, RoomSelectionIndex>>()
|
|
83
|
+
|
|
84
|
+
function roomIndex(levelId: string, nodes: ViewerNodes) {
|
|
85
|
+
let levels = indexes.get(nodes)
|
|
86
|
+
if (!levels) {
|
|
87
|
+
levels = new Map()
|
|
88
|
+
indexes.set(nodes, levels)
|
|
89
|
+
}
|
|
90
|
+
let index = levels.get(levelId)
|
|
91
|
+
if (!index) {
|
|
92
|
+
index = new RoomSelectionIndex(levelId)
|
|
93
|
+
levels.set(levelId, index)
|
|
94
|
+
}
|
|
95
|
+
index.update(nodes as Record<string, AnyNode>)
|
|
96
|
+
return index
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The rooms the walls of a level close, as the editor detects them. */
|
|
100
|
+
export function viewerRooms(levelId: string, nodes: ViewerNodes): RoomSelectionRecord[] {
|
|
101
|
+
return roomIndex(levelId, nodes).update(nodes as Record<string, AnyNode>)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function levelOfNode(nodeId: string, nodes: ViewerNodes): string | null {
|
|
105
|
+
const seen = new Set<string>()
|
|
106
|
+
let current = nodes[nodeId]
|
|
107
|
+
while (current && !seen.has(current.id)) {
|
|
108
|
+
if (current.type === 'level') return current.id
|
|
109
|
+
seen.add(current.id)
|
|
110
|
+
current = current.parentId ? nodes[current.parentId] : undefined
|
|
111
|
+
}
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildingOfLevel(levelId: string | null, nodes: ViewerNodes): BuildingNode['id'] | null {
|
|
116
|
+
const level = levelId ? nodes[levelId] : undefined
|
|
117
|
+
const parent = level?.parentId ? nodes[level.parentId] : undefined
|
|
118
|
+
if (parent?.type === 'building') return parent.id
|
|
119
|
+
const building = Object.values(nodes).find((node) => node.type === 'building')
|
|
120
|
+
return (building?.id as BuildingNode['id'] | undefined) ?? null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hitPoint = new Vector3()
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The parametric hit source: a node event as a pick, with the hit in its
|
|
127
|
+
* level's local XZ. A hit on a hidden floor, or on a kind a click passes
|
|
128
|
+
* through, is no pick: the pointer goes on to what lies behind it.
|
|
129
|
+
*/
|
|
130
|
+
export function viewerPickFromNodeEvent(event: NodeEvent, nodes: ViewerNodes): ViewerPick | null {
|
|
131
|
+
if (VIEWER_PASS_THROUGH_KINDS.has(event.node.type)) return null
|
|
132
|
+
const levelId = levelOfNode(event.node.id, nodes)
|
|
133
|
+
const level = levelId ? sceneRegistry.nodes.get(levelId) : undefined
|
|
134
|
+
if (level?.visible === false) return null
|
|
135
|
+
if (!level) return { nodeId: event.node.id, point: null }
|
|
136
|
+
level.updateWorldMatrix(true, false)
|
|
137
|
+
hitPoint.set(...event.position)
|
|
138
|
+
level.worldToLocal(hitPoint)
|
|
139
|
+
return { nodeId: event.node.id, point: [hitPoint.x, hitPoint.z] }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* What a pick acts on. A wall, floor or ceiling picks the room it bounds;
|
|
144
|
+
* once that room is selected, its own surfaces are picked as themselves.
|
|
145
|
+
* Anything else (an item, a door, a stair) is picked directly. Alt, Shift,
|
|
146
|
+
* Ctrl and Meta skip the room, as in the editor.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveViewerTarget(
|
|
149
|
+
selection: Pick<ViewerSelection, 'zoneId'>,
|
|
150
|
+
pick: ViewerPick,
|
|
151
|
+
nodes: ViewerNodes,
|
|
152
|
+
modifiers: SelectionModifierKeys = NO_MODIFIERS,
|
|
153
|
+
): ViewerTarget | null {
|
|
154
|
+
const node = nodes[pick.nodeId]
|
|
155
|
+
if (!node || VIEWER_PASS_THROUGH_KINDS.has(node.type)) return null
|
|
156
|
+
const levelId = levelOfNode(node.id, nodes)
|
|
157
|
+
const bypass = modifiers.alt || modifiers.shift || modifiers.ctrl || modifiers.meta
|
|
158
|
+
if (!bypass && levelId && pick.point && ROOM_SURFACES.has(node.type)) {
|
|
159
|
+
const room = resolveRoomHit(
|
|
160
|
+
roomIndex(levelId, nodes),
|
|
161
|
+
levelId,
|
|
162
|
+
node,
|
|
163
|
+
[pick.point[0], pick.point[1]],
|
|
164
|
+
'3d',
|
|
165
|
+
)
|
|
166
|
+
if (room) {
|
|
167
|
+
const { zoneId } = room.key
|
|
168
|
+
return selection.zoneId === zoneId
|
|
169
|
+
? { kind: 'node', levelId, nodeId: node.id, zoneId }
|
|
170
|
+
: { kind: 'room', levelId, zoneId }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const parent = node.parentId ? nodes[node.parentId] : undefined
|
|
174
|
+
const owner = node.type === 'roof-segment' && parent?.type === 'roof' ? parent : node
|
|
175
|
+
const nodeId = resolveSelectionProxyId(owner, nodes as Record<string, AnyNode | undefined>)
|
|
176
|
+
return { kind: 'node', levelId, nodeId, zoneId: null }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The selection a click makes. Empty space clears the room and the element
|
|
181
|
+
* and keeps the floor; nothing pops the user up a level.
|
|
182
|
+
*/
|
|
183
|
+
export function resolveViewerClick(
|
|
184
|
+
selection: ViewerSelection,
|
|
185
|
+
pick: ViewerPick | null,
|
|
186
|
+
nodes: ViewerNodes,
|
|
187
|
+
modifiers: SelectionModifierKeys = NO_MODIFIERS,
|
|
188
|
+
): ViewerSelection {
|
|
189
|
+
const target = pick ? resolveViewerTarget(selection, pick, nodes, modifiers) : null
|
|
190
|
+
if (!target) return { ...selection, zoneId: null, selectedIds: [] }
|
|
191
|
+
const levelId = (target.levelId as LevelNode['id'] | null) ?? selection.levelId
|
|
192
|
+
const buildingId = buildingOfLevel(levelId, nodes) ?? selection.buildingId
|
|
193
|
+
if (target.kind === 'room') {
|
|
194
|
+
return { buildingId, levelId, zoneId: target.zoneId as ZoneNode['id'], selectedIds: [] }
|
|
195
|
+
}
|
|
196
|
+
const toggles = modifiers.shift || modifiers.ctrl || modifiers.meta
|
|
197
|
+
const sameFloor = levelId === selection.levelId
|
|
198
|
+
const selectedIds =
|
|
199
|
+
toggles && sameFloor
|
|
200
|
+
? selection.selectedIds.includes(target.nodeId)
|
|
201
|
+
? selection.selectedIds.filter((id) => id !== target.nodeId)
|
|
202
|
+
: [...selection.selectedIds, target.nodeId]
|
|
203
|
+
: [target.nodeId]
|
|
204
|
+
return {
|
|
205
|
+
buildingId,
|
|
206
|
+
levelId,
|
|
207
|
+
zoneId: target.zoneId as ZoneNode['id'] | null,
|
|
208
|
+
selectedIds,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** What a hover would select: a room's id (its highlight) or the element's (its outline). */
|
|
213
|
+
export function resolveViewerHover(
|
|
214
|
+
selection: Pick<ViewerSelection, 'zoneId'>,
|
|
215
|
+
pick: ViewerPick | null,
|
|
216
|
+
nodes: ViewerNodes,
|
|
217
|
+
): string | null {
|
|
218
|
+
const target = pick ? resolveViewerTarget(selection, pick, nodes) : null
|
|
219
|
+
if (!target) return null
|
|
220
|
+
return target.kind === 'room' ? target.zoneId : target.nodeId
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The current floor reads as the editor shows it: the levels above it step
|
|
225
|
+
* aside (in the stacked display) so its rooms are in view and in reach. Mount
|
|
226
|
+
* it in the surface's DOM tree, beside the editor's own level display, so one
|
|
227
|
+
* hands the flag to the other in order.
|
|
228
|
+
*/
|
|
229
|
+
export function useViewerFloorDisplay() {
|
|
230
|
+
// A walkthrough tours the whole building.
|
|
231
|
+
const walking = useViewer((s) => s.walkthroughMode)
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (walking) return
|
|
234
|
+
useViewer.setState({ hideLevelsAboveSelection: true })
|
|
235
|
+
return () => {
|
|
236
|
+
useViewer.setState({ hideLevelsAboveSelection: false })
|
|
237
|
+
}
|
|
238
|
+
}, [walking])
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Applying it to the shared store ──────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function writeSelection(selection: ViewerSelection) {
|
|
244
|
+
const viewer = useViewer.getState()
|
|
245
|
+
viewer.setSelection(selection)
|
|
246
|
+
if (viewer.focusedUnitId) viewer.setFocusedUnit(null)
|
|
247
|
+
viewer.setHoveredId(null)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function applyViewerClick(
|
|
251
|
+
pick: ViewerPick | null,
|
|
252
|
+
nodes: ViewerNodes,
|
|
253
|
+
modifiers: SelectionModifierKeys = NO_MODIFIERS,
|
|
254
|
+
) {
|
|
255
|
+
writeSelection(resolveViewerClick(useViewer.getState().selection, pick, nodes, modifiers))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function applyViewerHover(pick: ViewerPick | null, nodes: ViewerNodes) {
|
|
259
|
+
const viewer = useViewer.getState()
|
|
260
|
+
const hovered = resolveViewerHover(viewer.selection, pick, nodes)
|
|
261
|
+
viewer.setHoveredId(hovered as never)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** A floor from the floor list: the floor alone, framed. */
|
|
265
|
+
export function selectViewerLevel(levelId: string, source: ViewerSource) {
|
|
266
|
+
writeSelection({
|
|
267
|
+
buildingId: buildingOfLevel(levelId, source.nodes),
|
|
268
|
+
levelId: levelId as LevelNode['id'],
|
|
269
|
+
zoneId: null,
|
|
270
|
+
selectedIds: [],
|
|
271
|
+
})
|
|
272
|
+
frameViewerNode(levelId, source)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The whole building again, framed. */
|
|
276
|
+
export function selectViewerBuilding(source: ViewerSource) {
|
|
277
|
+
const buildingId = buildingOfLevel(null, source.nodes)
|
|
278
|
+
writeSelection({ buildingId, levelId: null, zoneId: null, selectedIds: [] })
|
|
279
|
+
if (buildingId) frameViewerNode(buildingId, source)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A room or zone from its pill, the bar or the sidebar: selected on its floor, framed. */
|
|
283
|
+
export function selectViewerRoom(zoneId: string, source: ViewerSource) {
|
|
284
|
+
const zone = source.nodes[zoneId]
|
|
285
|
+
if (zone?.type !== 'zone' || !zone.parentId) return
|
|
286
|
+
const levelId = zone.parentId as LevelNode['id']
|
|
287
|
+
writeSelection({
|
|
288
|
+
buildingId: buildingOfLevel(levelId, source.nodes),
|
|
289
|
+
levelId,
|
|
290
|
+
zoneId: zone.id,
|
|
291
|
+
selectedIds: [],
|
|
292
|
+
})
|
|
293
|
+
frameViewerNode(zone.id, source)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A unit from the sidebar: its rooms highlighted and framed. The floor stays
|
|
298
|
+
* when it holds one of the unit's rooms, else the unit's lowest floor shows.
|
|
299
|
+
*/
|
|
300
|
+
export function selectViewerUnit(unitId: string, source: ViewerSource) {
|
|
301
|
+
const unit = source.nodes[unitId]
|
|
302
|
+
if (unit?.type !== 'unit') return
|
|
303
|
+
const viewer = useViewer.getState()
|
|
304
|
+
const levels = unitLevels(unit, source.nodes)
|
|
305
|
+
const current = viewer.selection.levelId
|
|
306
|
+
const levelId = (current && levels.includes(current) ? current : (levels[0] ?? current)) as
|
|
307
|
+
| LevelNode['id']
|
|
308
|
+
| null
|
|
309
|
+
viewer.setSelection({
|
|
310
|
+
buildingId: buildingOfLevel(levelId, source.nodes),
|
|
311
|
+
levelId,
|
|
312
|
+
zoneId: null,
|
|
313
|
+
selectedIds: [],
|
|
314
|
+
})
|
|
315
|
+
viewer.setHoveredId(null)
|
|
316
|
+
viewer.setFocusedUnit(unit.id)
|
|
317
|
+
frameViewerNode(unit.id, source)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function unitLevels(unit: UnitNode, nodes: ViewerNodes): string[] {
|
|
321
|
+
const ordinals = new Map<string, number>()
|
|
322
|
+
for (const member of buildUnitReport(unit, nodes).members) {
|
|
323
|
+
if (member.levelId && member.levelOrdinal !== null)
|
|
324
|
+
ordinals.set(member.levelId, member.levelOrdinal)
|
|
325
|
+
}
|
|
326
|
+
return [...ordinals.entries()].sort((a, b) => a[1] - b[1]).map(([id]) => id)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Framing ──────────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
const corner = new Vector3()
|
|
332
|
+
const box = new Box3()
|
|
333
|
+
|
|
334
|
+
function expandByZone(target: Box3, zone: ZoneNode, source: ViewerSource) {
|
|
335
|
+
const level = zone.parentId ? source.objectFor(zone.parentId) : undefined
|
|
336
|
+
level?.updateWorldMatrix(true, false)
|
|
337
|
+
const floorY = roomFloorElevation(source.nodes as Record<string, AnyNode>, zone.id)
|
|
338
|
+
for (const [x, z] of zone.polygon) {
|
|
339
|
+
for (const y of [floorY, floorY + DEFAULT_LEVEL_HEIGHT]) {
|
|
340
|
+
corner.set(x, y, z)
|
|
341
|
+
if (level) corner.applyMatrix4(level.matrixWorld)
|
|
342
|
+
target.expandByPoint(corner)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The frame request for a node: its saved view, else the box it spans. */
|
|
348
|
+
function viewerFrameEvent(id: string, source: ViewerSource): CameraControlFrameEvent | null {
|
|
349
|
+
const node = source.nodes[id]
|
|
350
|
+
if (!node) return null
|
|
351
|
+
const saved = node.camera
|
|
352
|
+
if (saved?.position && saved.target) {
|
|
353
|
+
return { bounds: null, pose: { position: saved.position, target: saved.target } }
|
|
354
|
+
}
|
|
355
|
+
box.makeEmpty()
|
|
356
|
+
if (node.type === 'zone') expandByZone(box, node, source)
|
|
357
|
+
else if (node.type === 'unit') {
|
|
358
|
+
for (const memberId of node.members) {
|
|
359
|
+
const member = source.nodes[memberId]
|
|
360
|
+
if (member?.type === 'zone') expandByZone(box, member, source)
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
const object = source.objectFor(id)
|
|
364
|
+
if (object) box.setFromObject(object)
|
|
365
|
+
}
|
|
366
|
+
if (box.isEmpty()) return null
|
|
367
|
+
return { bounds: { min: box.min.toArray(), max: box.max.toArray() } }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function frameViewerNode(id: string, source: ViewerSource) {
|
|
371
|
+
const event = viewerFrameEvent(id, source)
|
|
372
|
+
if (event) emitter.emit('camera-controls:frame', event)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
type Vec3 = [number, number, number]
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Where a camera frames a box: about 45° above it, far enough for the box to
|
|
379
|
+
* fill the view, along the heading the camera already has so the scene does
|
|
380
|
+
* not swing round.
|
|
381
|
+
*/
|
|
382
|
+
export function framedLookAt(
|
|
383
|
+
event: CameraControlFrameEvent,
|
|
384
|
+
from: { position: Vec3; target: Vec3 },
|
|
385
|
+
fovDegrees = 50,
|
|
386
|
+
): { position: Vec3; target: Vec3 } | null {
|
|
387
|
+
if (event.pose) return event.pose
|
|
388
|
+
if (!event.bounds) return null
|
|
389
|
+
const { min, max } = event.bounds
|
|
390
|
+
const center: Vec3 = [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2]
|
|
391
|
+
const radius = Math.max(Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2, 1)
|
|
392
|
+
const halfFov = (Math.min(Math.max(fovDegrees, 10), 120) * Math.PI) / 360
|
|
393
|
+
const distance = Math.max((radius / Math.sin(halfFov)) * 1.6, 6)
|
|
394
|
+
const dx = from.position[0] - from.target[0]
|
|
395
|
+
const dz = from.position[2] - from.target[2]
|
|
396
|
+
const heading = Math.hypot(dx, dz) > 1e-6 ? Math.atan2(dx, dz) : Math.PI / 4
|
|
397
|
+
const elevation = Math.PI / 4
|
|
398
|
+
const flat = Math.cos(elevation) * distance
|
|
399
|
+
return {
|
|
400
|
+
position: [
|
|
401
|
+
center[0] + Math.sin(heading) * flat,
|
|
402
|
+
center[1] + Math.sin(elevation) * distance,
|
|
403
|
+
center[2] + Math.cos(heading) * flat,
|
|
404
|
+
],
|
|
405
|
+
target: center,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const framePosition = new Vector3()
|
|
410
|
+
const frameTarget = new Vector3()
|
|
411
|
+
const frameSphere = new Sphere()
|
|
412
|
+
|
|
413
|
+
/** Apply the shared framing pose, fitting the orthographic viewport as well. */
|
|
414
|
+
export function frameViewerCamera(
|
|
415
|
+
control: CameraControlsImpl,
|
|
416
|
+
camera: Camera,
|
|
417
|
+
event: CameraControlFrameEvent,
|
|
418
|
+
) {
|
|
419
|
+
control.getPosition(framePosition)
|
|
420
|
+
control.getTarget(frameTarget)
|
|
421
|
+
const pose = framedLookAt(
|
|
422
|
+
event,
|
|
423
|
+
{ position: framePosition.toArray(), target: frameTarget.toArray() },
|
|
424
|
+
(camera as PerspectiveCamera).isPerspectiveCamera
|
|
425
|
+
? (camera as PerspectiveCamera).fov
|
|
426
|
+
: undefined,
|
|
427
|
+
)
|
|
428
|
+
if (!pose) return
|
|
429
|
+
control.setLookAt(...pose.position, ...pose.target, true)
|
|
430
|
+
if ((camera as OrthographicCamera).isOrthographicCamera && event.bounds && !event.pose) {
|
|
431
|
+
box.min.set(...event.bounds.min)
|
|
432
|
+
box.max.set(...event.bounds.max)
|
|
433
|
+
box.getBoundingSphere(frameSphere)
|
|
434
|
+
frameSphere.radius = Math.max(frameSphere.radius * 1.3, 1)
|
|
435
|
+
control.fitToSphere(frameSphere, true)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ── Breadcrumb ───────────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
export type ViewerCrumb = {
|
|
442
|
+
kind: 'building' | 'level' | 'unit' | 'room' | 'node'
|
|
443
|
+
id: string
|
|
444
|
+
label: string
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function zoneLabel(zone: ZoneNode): string {
|
|
448
|
+
return zone.name?.trim() || (zone.spaceRole === 'room' ? 'Room' : 'Zone')
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The bar: building › floor › room › element. An element inside a room shows
|
|
453
|
+
* that room (`roomOf`) without selecting it; a focused unit stands where the
|
|
454
|
+
* room would.
|
|
455
|
+
*/
|
|
456
|
+
export function viewerBreadcrumb(
|
|
457
|
+
selection: ViewerSelection,
|
|
458
|
+
focusedUnitId: string | null,
|
|
459
|
+
nodes: ViewerNodes,
|
|
460
|
+
roomOf: (nodeId: string, levelId: string) => string | null = () => null,
|
|
461
|
+
): ViewerCrumb[] {
|
|
462
|
+
const crumbs: ViewerCrumb[] = []
|
|
463
|
+
const elementId = selection.selectedIds[0] ?? null
|
|
464
|
+
const element = elementId ? nodes[elementId] : undefined
|
|
465
|
+
const levelId = selection.levelId ?? (element ? levelOfNode(element.id, nodes) : null)
|
|
466
|
+
const buildingId = selection.buildingId ?? buildingOfLevel(levelId, nodes)
|
|
467
|
+
const building = buildingId ? nodes[buildingId] : undefined
|
|
468
|
+
if (building?.type === 'building') {
|
|
469
|
+
crumbs.push({ kind: 'building', id: building.id, label: building.name || 'Building' })
|
|
470
|
+
}
|
|
471
|
+
const level = levelId ? nodes[levelId] : undefined
|
|
472
|
+
if (level?.type !== 'level') return crumbs
|
|
473
|
+
crumbs.push({ kind: 'level', id: level.id, label: getLevelDisplayName(level) })
|
|
474
|
+
const unit = focusedUnitId ? nodes[focusedUnitId] : undefined
|
|
475
|
+
if (unit?.type === 'unit') crumbs.push({ kind: 'unit', id: unit.id, label: unit.name })
|
|
476
|
+
const roomId = selection.zoneId ?? (element ? roomOf(element.id, level.id) : null)
|
|
477
|
+
const room = roomId ? nodes[roomId] : undefined
|
|
478
|
+
if (room?.type === 'zone') crumbs.push({ kind: 'room', id: room.id, label: zoneLabel(room) })
|
|
479
|
+
if (element) crumbs.push({ kind: 'node', id: element.id, label: nodeDisplayLabel(element) })
|
|
480
|
+
return crumbs
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** The room or zone of a level around a level-local point: a detected room first. */
|
|
484
|
+
function roomAtLevelPoint(
|
|
485
|
+
levelId: string,
|
|
486
|
+
point: readonly [number, number],
|
|
487
|
+
nodes: ViewerNodes,
|
|
488
|
+
): string | null {
|
|
489
|
+
const at: [number, number] = [point[0], point[1]]
|
|
490
|
+
const room = roomIndex(levelId, nodes).roomAtPoint(levelId, at)
|
|
491
|
+
if (room) return room.key.zoneId
|
|
492
|
+
for (const node of Object.values(nodes)) {
|
|
493
|
+
if (node.type !== 'zone' || node.parentId !== levelId || node.polygon.length < 3) continue
|
|
494
|
+
if (containsPoint([{ outer: node.polygon, holes: node.holes ?? [] }], at)) return node.id
|
|
495
|
+
}
|
|
496
|
+
return null
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const center = new Vector3()
|
|
500
|
+
|
|
501
|
+
/** The room an element stands in, from where its object sits on its floor. */
|
|
502
|
+
export function roomOfObject(nodeId: string, levelId: string, source: ViewerSource) {
|
|
503
|
+
const object = source.objectFor(nodeId)
|
|
504
|
+
const level = source.objectFor(levelId)
|
|
505
|
+
if (!(object && level)) return null
|
|
506
|
+
box.setFromObject(object)
|
|
507
|
+
if (box.isEmpty()) object.getWorldPosition(center)
|
|
508
|
+
else box.getCenter(center)
|
|
509
|
+
level.updateWorldMatrix(true, false)
|
|
510
|
+
level.worldToLocal(center)
|
|
511
|
+
return roomAtLevelPoint(levelId, [center.x, center.z], source.nodes)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── Sidebar ──────────────────────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
export type ViewerSidebarRow = { id: string; label: string; color: string; areaM2: number }
|
|
517
|
+
export type ViewerSidebarSection = { kind: 'units' | 'rooms' | 'zones'; rows: ViewerSidebarRow[] }
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* What sits under the current floor: its units, its rooms, its other zones.
|
|
521
|
+
* An empty section is left out.
|
|
522
|
+
*/
|
|
523
|
+
export function viewerSidebarSections(levelId: string, nodes: ViewerNodes): ViewerSidebarSection[] {
|
|
524
|
+
const zones = Object.values(nodes).filter(
|
|
525
|
+
(node): node is ZoneNode =>
|
|
526
|
+
node.type === 'zone' && node.parentId === levelId && node.visible !== false,
|
|
527
|
+
)
|
|
528
|
+
const zoneRow = (zone: ZoneNode): ViewerSidebarRow => ({
|
|
529
|
+
id: zone.id,
|
|
530
|
+
label: zoneLabel(zone),
|
|
531
|
+
color: zone.color,
|
|
532
|
+
areaM2: area([{ outer: zone.polygon, holes: zone.holes ?? [] }]),
|
|
533
|
+
})
|
|
534
|
+
const byName = (a: ViewerSidebarRow, b: ViewerSidebarRow) => a.label.localeCompare(b.label)
|
|
535
|
+
const units = Object.values(nodes)
|
|
536
|
+
.filter((node): node is UnitNode => node.type === 'unit')
|
|
537
|
+
.flatMap((unit) => {
|
|
538
|
+
const report = buildUnitReport(unit, nodes)
|
|
539
|
+
const here = report.members.filter((member) => member.levelId === levelId)
|
|
540
|
+
if (here.length === 0) return []
|
|
541
|
+
return [
|
|
542
|
+
{
|
|
543
|
+
id: unit.id,
|
|
544
|
+
label: unit.name,
|
|
545
|
+
color: unit.color,
|
|
546
|
+
areaM2: here.reduce((sum, member) => sum + member.areaM2, 0),
|
|
547
|
+
},
|
|
548
|
+
]
|
|
549
|
+
})
|
|
550
|
+
.sort(byName)
|
|
551
|
+
const sections: ViewerSidebarSection[] = [
|
|
552
|
+
{ kind: 'units', rows: units },
|
|
553
|
+
{
|
|
554
|
+
kind: 'rooms',
|
|
555
|
+
rows: zones
|
|
556
|
+
.filter((zone) => zone.spaceRole === 'room')
|
|
557
|
+
.map(zoneRow)
|
|
558
|
+
.sort(byName),
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
kind: 'zones',
|
|
562
|
+
rows: zones
|
|
563
|
+
.filter((zone) => zone.spaceRole !== 'room')
|
|
564
|
+
.map(zoneRow)
|
|
565
|
+
.sort(byName),
|
|
566
|
+
},
|
|
567
|
+
]
|
|
568
|
+
return sections.filter((section) => section.rows.length > 0)
|
|
569
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { emitter } from '@pascal-app/core'
|
|
3
|
+
import useEditor from '../store/use-editor'
|
|
4
|
+
import { toolHudTitle } from './hud-title'
|
|
5
|
+
import {
|
|
6
|
+
getWallDrawVariant,
|
|
7
|
+
selectWallDrawVariant,
|
|
8
|
+
WALL_DRAW_VARIANTS,
|
|
9
|
+
wallDrawVariantOf,
|
|
10
|
+
} from './wall-draw-variant'
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
useEditor.getState().setContinuation('wall', 'room')
|
|
14
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('Rooms variants', () => {
|
|
18
|
+
test('are Rectangle, Polygon and Walls, in the Build panel order', () => {
|
|
19
|
+
expect(WALL_DRAW_VARIANTS.map((variant) => [variant.id, variant.label, variant.mode])).toEqual([
|
|
20
|
+
['rectangle', 'Rectangle', 'rectangle'],
|
|
21
|
+
['polygon', 'Polygon', 'room'],
|
|
22
|
+
['walls', 'Walls', 'single'],
|
|
23
|
+
])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('default to the chain that closes into a room', () => {
|
|
27
|
+
expect(getWallDrawVariant()).toBe('polygon')
|
|
28
|
+
expect(wallDrawVariantOf('unknown')).toBe('polygon')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('picking one sets the wall tool drawing mode, and it sticks', () => {
|
|
32
|
+
for (const variant of WALL_DRAW_VARIANTS) {
|
|
33
|
+
selectWallDrawVariant(variant.id)
|
|
34
|
+
expect(useEditor.getState().getContinuation('wall')).toBe(variant.mode)
|
|
35
|
+
expect(getWallDrawVariant()).toBe(variant.id)
|
|
36
|
+
}
|
|
37
|
+
// Arming the wall tool (e.g. with B) keeps the picked variant.
|
|
38
|
+
selectWallDrawVariant('rectangle')
|
|
39
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
40
|
+
expect(getWallDrawVariant()).toBe('rectangle')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('switching while the wall tool is armed drops the in-flight draft', () => {
|
|
44
|
+
let cancels = 0
|
|
45
|
+
const onCancel = () => {
|
|
46
|
+
cancels += 1
|
|
47
|
+
}
|
|
48
|
+
emitter.on('tool:cancel', onCancel)
|
|
49
|
+
try {
|
|
50
|
+
selectWallDrawVariant('walls')
|
|
51
|
+
expect(cancels).toBe(0)
|
|
52
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
53
|
+
selectWallDrawVariant('rectangle')
|
|
54
|
+
expect(cancels).toBe(1)
|
|
55
|
+
selectWallDrawVariant('rectangle')
|
|
56
|
+
expect(cancels).toBe(1)
|
|
57
|
+
} finally {
|
|
58
|
+
emitter.off('tool:cancel', onCancel)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('name the HUD after the variant in hand', () => {
|
|
63
|
+
expect(toolHudTitle('wall', 'rectangle')).toEqual({
|
|
64
|
+
label: 'Rectangle room',
|
|
65
|
+
icon: { kind: 'url', src: '/icons/room.webp' },
|
|
66
|
+
shortcut: 'B',
|
|
67
|
+
})
|
|
68
|
+
expect(toolHudTitle('wall', 'room')?.label).toBe('Polygon room')
|
|
69
|
+
expect(toolHudTitle('wall', 'single')?.label).toBe('Walls')
|
|
70
|
+
})
|
|
71
|
+
})
|