@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
|
@@ -13,11 +13,13 @@ import {
|
|
|
13
13
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
14
|
import { createPortal, type ThreeEvent, useThree } from '@react-three/fiber'
|
|
15
15
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
16
|
-
import { OrthographicCamera, Plane, Vector2, Vector3 } from 'three'
|
|
16
|
+
import { OrthographicCamera, Plane, type Ray, Vector2, Vector3 } from 'three'
|
|
17
17
|
import { GROUP_MOVE_DRAG_LABEL, GROUP_ROTATE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
18
18
|
import { isHistoryShortcut } from '../../lib/history'
|
|
19
19
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
20
|
-
import
|
|
20
|
+
import { intersectSpatialDragPlane } from '../../lib/spatial-drag-plane'
|
|
21
|
+
import { getSpatialPointerId, spatialPointerInput } from '../../lib/spatial-pointer-input'
|
|
22
|
+
import useEditor, { isAngleSnapActive } from '../../store/use-editor'
|
|
21
23
|
import useInteractionScope, {
|
|
22
24
|
useActiveHandleDrag,
|
|
23
25
|
useMovingNode,
|
|
@@ -28,7 +30,7 @@ import {
|
|
|
28
30
|
classifyParticipant,
|
|
29
31
|
collectParticipants,
|
|
30
32
|
computeGroupBox,
|
|
31
|
-
|
|
33
|
+
computeGroupPlanBox,
|
|
32
34
|
levelFrame,
|
|
33
35
|
rotateGroupPatches,
|
|
34
36
|
type Vec3,
|
|
@@ -79,14 +81,6 @@ export function GroupRotateHandle() {
|
|
|
79
81
|
[selectedIds, levelId, nodes],
|
|
80
82
|
)
|
|
81
83
|
|
|
82
|
-
// Gate on the explicit selection (so a single connected wall still gets the
|
|
83
|
-
// per-node handles), but transform the full connected wall/fence component so
|
|
84
|
-
// attached structure rotates rigidly as one piece.
|
|
85
|
-
const fullIds = useMemo(
|
|
86
|
-
() => expandToComponent(participantIds, nodes, levelId),
|
|
87
|
-
[participantIds, levelId, nodes],
|
|
88
|
-
)
|
|
89
|
-
|
|
90
84
|
const shouldRender =
|
|
91
85
|
participantIds.length >= 2 &&
|
|
92
86
|
mode !== 'delete' &&
|
|
@@ -98,7 +92,13 @@ export function GroupRotateHandle() {
|
|
|
98
92
|
|
|
99
93
|
if (!shouldRender) return null
|
|
100
94
|
// Remount when the moving set changes so the rest pivot re-seeds cleanly.
|
|
101
|
-
return
|
|
95
|
+
return (
|
|
96
|
+
<GroupRotateHandleInner
|
|
97
|
+
ids={participantIds}
|
|
98
|
+
key={participantIds.join(',')}
|
|
99
|
+
meshEpoch={meshEpoch}
|
|
100
|
+
/>
|
|
101
|
+
)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch: number }) {
|
|
@@ -125,22 +125,28 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
125
125
|
const baseScale = zoom * ARROW_SCALE * 1.05
|
|
126
126
|
const scale = (isHovered ? 1.12 : 1) * baseScale
|
|
127
127
|
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// - `
|
|
133
|
-
// - `corner` = front-right bbox corner at mid-height → where the gizmo sits
|
|
128
|
+
// Both rest points come from the level-frame box (the dashed boxes' box, also
|
|
129
|
+
// keyboard R/T's pivot), carried to world so they stay on it under a rotated
|
|
130
|
+
// building; the world mesh box only supplies heights.
|
|
131
|
+
// - `pivot` = box center, Y at the group's base → the rotation origin
|
|
132
|
+
// - `corner` = far box corner at mid-height → where the gizmo sits
|
|
134
133
|
const rest = useMemo(() => {
|
|
135
134
|
void meshEpoch
|
|
136
135
|
const box = computeGroupBox(ids)
|
|
137
136
|
if (!box) return null
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
const levelId = useViewer.getState().selection.levelId
|
|
138
|
+
const { matrix } = levelFrame(levelId)
|
|
139
|
+
const plan = computeGroupPlanBox(ids, levelId)
|
|
140
|
+
const pivot = plan
|
|
141
|
+
? new Vector3((plan.minX + plan.maxX) / 2, 0, (plan.minZ + plan.maxZ) / 2).applyMatrix4(
|
|
142
|
+
matrix,
|
|
143
|
+
)
|
|
144
|
+
: new Vector3((box.min.x + box.max.x) / 2, 0, (box.min.z + box.max.z) / 2)
|
|
145
|
+
pivot.y = box.min.y
|
|
146
|
+
const corner = plan
|
|
147
|
+
? new Vector3(plan.maxX + CORNER_OFFSET, 0, plan.maxZ + CORNER_OFFSET).applyMatrix4(matrix)
|
|
148
|
+
: new Vector3(box.max.x + CORNER_OFFSET, 0, box.max.z + CORNER_OFFSET)
|
|
149
|
+
corner.y = (box.min.y + box.max.y) / 2
|
|
144
150
|
return { pivot, corner }
|
|
145
151
|
}, [ids, meshEpoch])
|
|
146
152
|
|
|
@@ -163,6 +169,12 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
163
169
|
if (event.button !== 0) return
|
|
164
170
|
event.stopPropagation()
|
|
165
171
|
suppressBoxSelectForPointer(event)
|
|
172
|
+
const spatialPointerId = getSpatialPointerId(event.nativeEvent)
|
|
173
|
+
const spatialRay = spatialPointerId ? event.ray.clone() : null
|
|
174
|
+
const pointerTarget = event.object as typeof event.object & {
|
|
175
|
+
releasePointerCapture?: (pointerId: number) => void
|
|
176
|
+
setPointerCapture?: (pointerId: number) => void
|
|
177
|
+
}
|
|
166
178
|
|
|
167
179
|
frozenRest.current = { pivot: rest.pivot.clone(), corner: rest.corner.clone() }
|
|
168
180
|
const center = rest.pivot.clone()
|
|
@@ -213,11 +225,15 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
213
225
|
)
|
|
214
226
|
}
|
|
215
227
|
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
if (!spatialRay) {
|
|
229
|
+
setNDC(event.nativeEvent.clientX, event.nativeEvent.clientY)
|
|
230
|
+
raycaster.setFromCamera(ndc, camera)
|
|
231
|
+
}
|
|
218
232
|
const hit = new Vector3()
|
|
219
|
-
if (!raycaster.ray
|
|
233
|
+
if (!intersectSpatialDragPlane(spatialRay ?? raycaster.ray, plane, hit)) return
|
|
220
234
|
const initialAngle = angleOf(hit)
|
|
235
|
+
if (spatialPointerId) pointerTarget.setPointerCapture?.(event.pointerId)
|
|
236
|
+
let altKey = event.nativeEvent.altKey
|
|
221
237
|
|
|
222
238
|
document.body.style.cursor = 'grabbing'
|
|
223
239
|
sfxEmitter.emit('sfx:item-pick')
|
|
@@ -230,15 +246,14 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
230
246
|
})
|
|
231
247
|
setIsDragging(true)
|
|
232
248
|
|
|
233
|
-
const
|
|
234
|
-
setNDC(e.clientX, e.clientY)
|
|
235
|
-
raycaster.setFromCamera(ndc, camera)
|
|
249
|
+
const applyRay = (ray: Ray, freeRotation = false) => {
|
|
236
250
|
const moveHit = new Vector3()
|
|
237
|
-
if (!
|
|
251
|
+
if (!intersectSpatialDragPlane(ray, plane, moveHit)) return
|
|
238
252
|
let delta = angleOf(moveHit) - initialAngle
|
|
239
253
|
while (delta > Math.PI) delta -= 2 * Math.PI
|
|
240
254
|
while (delta < -Math.PI) delta += 2 * Math.PI
|
|
241
|
-
if (!
|
|
255
|
+
if (!freeRotation && isAngleSnapActive())
|
|
256
|
+
delta = Math.round(delta / DEFAULT_ANGLE_STEP) * DEFAULT_ANGLE_STEP
|
|
242
257
|
|
|
243
258
|
// Shared rigid-rotation math (also used by the keyboard group R/T);
|
|
244
259
|
// see `rotateGroupPatches` for the orbit/yaw handedness contract.
|
|
@@ -286,8 +301,14 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
286
301
|
})
|
|
287
302
|
}
|
|
288
303
|
}
|
|
304
|
+
const onMove = (e: PointerEvent) => {
|
|
305
|
+
setNDC(e.clientX, e.clientY)
|
|
306
|
+
raycaster.setFromCamera(ndc, camera)
|
|
307
|
+
applyRay(raycaster.ray, e.altKey)
|
|
308
|
+
}
|
|
289
309
|
|
|
290
310
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
311
|
+
let releaseSpatialCapture: (() => void) | null = null
|
|
291
312
|
const clearLivePreviews = () => {
|
|
292
313
|
const overrides = useLiveNodeOverrides.getState()
|
|
293
314
|
const liveTransforms = useLiveTransforms.getState()
|
|
@@ -303,6 +324,10 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
303
324
|
window.removeEventListener('pointerup', onUp)
|
|
304
325
|
window.removeEventListener('pointercancel', onCancel)
|
|
305
326
|
window.removeEventListener('keydown', onKeyDown, true)
|
|
327
|
+
window.removeEventListener('keyup', onKeyUp, true)
|
|
328
|
+
releaseSpatialCapture?.()
|
|
329
|
+
releaseSpatialCapture = null
|
|
330
|
+
if (spatialPointerId) pointerTarget.releasePointerCapture?.(event.pointerId)
|
|
306
331
|
if (document.body.style.cursor === 'grabbing') document.body.style.cursor = ''
|
|
307
332
|
useScene.temporal.getState().resume()
|
|
308
333
|
useViewer.getState().setInputDragging(false)
|
|
@@ -352,12 +377,19 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
352
377
|
// Escape / ⌘Z abort the rotate — capture phase so they win over the global
|
|
353
378
|
// use-keyboard arms (⌘Z must never history-jump under a live pointer).
|
|
354
379
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
380
|
+
if (e.key === 'Alt') {
|
|
381
|
+
altKey = true
|
|
382
|
+
return
|
|
383
|
+
}
|
|
355
384
|
if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
|
|
356
385
|
e.preventDefault()
|
|
357
386
|
e.stopPropagation()
|
|
358
387
|
swallowNextClick()
|
|
359
388
|
onCancel()
|
|
360
389
|
}
|
|
390
|
+
const onKeyUp = (e: KeyboardEvent) => {
|
|
391
|
+
if (e.key === 'Alt') altKey = false
|
|
392
|
+
}
|
|
361
393
|
|
|
362
394
|
dragCleanupRef.current = () => {
|
|
363
395
|
clearLivePreviews()
|
|
@@ -366,10 +398,22 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
366
398
|
for (const id of affectedIds) {
|
|
367
399
|
useLiveTransforms.getState().clear(id)
|
|
368
400
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
401
|
+
if (spatialPointerId && spatialRay) {
|
|
402
|
+
releaseSpatialCapture = spatialPointerInput.capture(spatialPointerId, {
|
|
403
|
+
onMove: (ray) => {
|
|
404
|
+
spatialRay.copy(ray)
|
|
405
|
+
applyRay(spatialRay, altKey)
|
|
406
|
+
},
|
|
407
|
+
onRelease: onUp,
|
|
408
|
+
onCancel,
|
|
409
|
+
})
|
|
410
|
+
} else {
|
|
411
|
+
window.addEventListener('pointermove', onMove)
|
|
412
|
+
window.addEventListener('pointerup', onUp)
|
|
413
|
+
window.addEventListener('pointercancel', onCancel)
|
|
414
|
+
}
|
|
372
415
|
window.addEventListener('keydown', onKeyDown, true)
|
|
416
|
+
window.addEventListener('keyup', onKeyUp, true)
|
|
373
417
|
}
|
|
374
418
|
|
|
375
419
|
return createPortal(
|
|
@@ -398,6 +442,7 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
398
442
|
onPointerDown={activate}
|
|
399
443
|
onPointerEnter={onHoverEnter}
|
|
400
444
|
onPointerLeave={onHoverLeave}
|
|
445
|
+
{...({ pointerEventsOrder: 10 } as Record<string, unknown>)}
|
|
401
446
|
scale={baseScale}
|
|
402
447
|
/>
|
|
403
448
|
<mesh
|
|
@@ -407,6 +452,7 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
|
|
|
407
452
|
onPointerDown={activate}
|
|
408
453
|
onPointerEnter={onHoverEnter}
|
|
409
454
|
onPointerLeave={onHoverLeave}
|
|
455
|
+
{...({ pointerEventsOrder: 10 } as Record<string, unknown>)}
|
|
410
456
|
renderOrder={1010}
|
|
411
457
|
scale={scale}
|
|
412
458
|
/>
|
|
@@ -12,7 +12,7 @@ import { armGroupMove3d } from './group-move-3d'
|
|
|
12
12
|
import {
|
|
13
13
|
classifyParticipant,
|
|
14
14
|
computeGroupBox,
|
|
15
|
-
|
|
15
|
+
computeGroupPlanBox,
|
|
16
16
|
levelFrame,
|
|
17
17
|
} from './group-transform-shared'
|
|
18
18
|
import { useMeshSettleEpoch } from './use-mesh-settle-epoch'
|
|
@@ -24,8 +24,8 @@ const BOX_PAD = 0.06
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* 3D sibling of the 2D dashed group selection box: a dashed wireframe around
|
|
27
|
-
* the multi-selection's transformable participants
|
|
28
|
-
*
|
|
27
|
+
* the multi-selection's transformable participants that doubles as the
|
|
28
|
+
* group's whole-volume drag
|
|
29
29
|
* handle — move cursor across it, press-drag anywhere on it slides the group,
|
|
30
30
|
* a plain click picks it up. Holding a selection modifier passes the press
|
|
31
31
|
* through so members inside can still be toggled. Rides the live drag delta
|
|
@@ -55,22 +55,30 @@ export function GroupSelectionBox3D() {
|
|
|
55
55
|
const box = useMemo(() => {
|
|
56
56
|
void meshEpoch
|
|
57
57
|
if (participantIds.length === 0) return null
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
// Aligned with the building like the 2D box: level-frame footprint, world
|
|
59
|
+
// height span, turned by the level's rotation.
|
|
60
|
+
const world = computeGroupBox(participantIds)
|
|
61
|
+
const { matrix } = levelFrame(levelId)
|
|
62
|
+
const plan = computeGroupPlanBox(participantIds, levelId)
|
|
63
|
+
if (!(world && plan)) return null
|
|
64
|
+
const center = new THREE.Vector3(
|
|
65
|
+
(plan.minX + plan.maxX) / 2,
|
|
66
|
+
0,
|
|
67
|
+
(plan.minZ + plan.maxZ) / 2,
|
|
68
|
+
).applyMatrix4(matrix)
|
|
69
|
+
center.y = (world.min.y + world.max.y) / 2
|
|
70
|
+
const quaternion = new THREE.Quaternion()
|
|
71
|
+
matrix.decompose(new THREE.Vector3(), quaternion, new THREE.Vector3())
|
|
65
72
|
return {
|
|
66
|
-
size: [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
],
|
|
73
|
+
size: [
|
|
74
|
+
plan.maxX - plan.minX + 2 * BOX_PAD,
|
|
75
|
+
world.max.y - world.min.y + 2 * BOX_PAD,
|
|
76
|
+
plan.maxZ - plan.minZ + 2 * BOX_PAD,
|
|
77
|
+
] as [number, number, number],
|
|
71
78
|
center,
|
|
79
|
+
quaternion,
|
|
72
80
|
}
|
|
73
|
-
}, [participantIds,
|
|
81
|
+
}, [participantIds, levelId, meshEpoch])
|
|
74
82
|
|
|
75
83
|
// Dashed wireframe. Built per box size (rare — selection / commit changes)
|
|
76
84
|
// because LineDashedMaterial measures dashes along the line, so scaling a
|
|
@@ -156,7 +164,7 @@ export function GroupSelectionBox3D() {
|
|
|
156
164
|
]
|
|
157
165
|
|
|
158
166
|
return (
|
|
159
|
-
<group position={position}>
|
|
167
|
+
<group position={position} quaternion={box.quaternion}>
|
|
160
168
|
<primitive object={edges} />
|
|
161
169
|
{/* Invisible whole-volume hit target — the box IS the drag handle. */}
|
|
162
170
|
<mesh
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
CeilingNode,
|
|
7
|
+
LevelNode,
|
|
8
|
+
nodeRegistry,
|
|
9
|
+
registerNode,
|
|
10
|
+
SlabNode,
|
|
11
|
+
sceneRegistry,
|
|
12
|
+
useScene,
|
|
13
|
+
WallNode,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import { BoxGeometry, Group, Mesh } from 'three'
|
|
3
16
|
import { z } from 'zod'
|
|
4
17
|
import {
|
|
5
18
|
classifyParticipant,
|
|
6
19
|
collectParticipants,
|
|
20
|
+
computeGroupPlanBox,
|
|
21
|
+
groupPlanBounds,
|
|
7
22
|
planBoundsCenter,
|
|
8
23
|
rotateGroupPatches,
|
|
9
24
|
rotateGroupSnapshots,
|
|
@@ -472,3 +487,113 @@ describe('group transform participants', () => {
|
|
|
472
487
|
])
|
|
473
488
|
})
|
|
474
489
|
})
|
|
490
|
+
|
|
491
|
+
describe('group plan bounds', () => {
|
|
492
|
+
const withNodes = (nodes: AnyNode[], run: () => void) => {
|
|
493
|
+
const saved = useScene.getState().nodes
|
|
494
|
+
useScene.setState({ nodes: Object.fromEntries(nodes.map((n) => [n.id, n])) as typeof saved })
|
|
495
|
+
try {
|
|
496
|
+
run()
|
|
497
|
+
} finally {
|
|
498
|
+
useScene.setState({ nodes: saved })
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
test('a placed object measures its built mesh in the level frame, or falls back to its anchor', () => {
|
|
503
|
+
const level = new Group()
|
|
504
|
+
level.position.set(10, 0, -4)
|
|
505
|
+
level.rotation.y = Math.PI / 5
|
|
506
|
+
const item = new Mesh(new BoxGeometry(1, 1, 1))
|
|
507
|
+
item.position.set(2, 0.5, 0)
|
|
508
|
+
level.add(item)
|
|
509
|
+
level.updateWorldMatrix(true, true)
|
|
510
|
+
const registry = sceneRegistry.nodes as unknown as Map<string, unknown>
|
|
511
|
+
registry.set('item_plan_bounds_test', item)
|
|
512
|
+
const start = {
|
|
513
|
+
id: 'item_plan_bounds_test' as AnyNodeId,
|
|
514
|
+
kind: 'vec3' as const,
|
|
515
|
+
position: [2, 0, 0] as [number, number, number],
|
|
516
|
+
rotation: [0, 0, 0] as [number, number, number],
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const frameInv = level.matrixWorld.clone().invert()
|
|
520
|
+
const built = groupPlanBounds([start], frameInv)
|
|
521
|
+
expect(built?.minX).toBeCloseTo(1.5)
|
|
522
|
+
expect(built?.maxX).toBeCloseTo(2.5)
|
|
523
|
+
expect(built?.minZ).toBeCloseTo(-0.5)
|
|
524
|
+
expect(built?.maxZ).toBeCloseTo(0.5)
|
|
525
|
+
// Unbuilt while the 3D scene is paused: the placeholder must not count.
|
|
526
|
+
item.geometry.userData.placeholder = true
|
|
527
|
+
expect(groupPlanBounds([start], frameInv)).toEqual({ minX: 2, minZ: 0, maxX: 2, maxZ: 0 })
|
|
528
|
+
} finally {
|
|
529
|
+
registry.delete('item_plan_bounds_test')
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
test('hugs the selected wall outlines; a connected wall stays outside', () => {
|
|
534
|
+
const level = LevelNode.parse({ children: [] })
|
|
535
|
+
const bottom = WallNode.parse({
|
|
536
|
+
parentId: level.id,
|
|
537
|
+
start: [0, 0],
|
|
538
|
+
end: [2.4, 0],
|
|
539
|
+
thickness: 0.2,
|
|
540
|
+
})
|
|
541
|
+
const side = WallNode.parse({
|
|
542
|
+
parentId: level.id,
|
|
543
|
+
start: [2.4, 0],
|
|
544
|
+
end: [2.4, 5.3],
|
|
545
|
+
thickness: 0.2,
|
|
546
|
+
})
|
|
547
|
+
const neighbour = WallNode.parse({
|
|
548
|
+
parentId: level.id,
|
|
549
|
+
start: [2.4, 5.3],
|
|
550
|
+
end: [8, 5.3],
|
|
551
|
+
thickness: 0.2,
|
|
552
|
+
})
|
|
553
|
+
withNodes([level, bottom, side, neighbour], () => {
|
|
554
|
+
const plan = computeGroupPlanBox([bottom.id, side.id], level.id)
|
|
555
|
+
expect(plan?.minX).toBeCloseTo(0)
|
|
556
|
+
expect(plan?.maxX).toBeCloseTo(2.5)
|
|
557
|
+
expect(plan?.minZ).toBeCloseTo(-0.1)
|
|
558
|
+
expect(plan?.maxZ).toBeLessThan(5.5)
|
|
559
|
+
expect(plan?.maxX).toBeLessThan(8)
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
test('two walls with the room floor and ceiling: the box is the drawn outlines, never the meshes', () => {
|
|
564
|
+
// A user's selection whose box came out 13 m wide in 2D-only view.
|
|
565
|
+
const level = LevelNode.parse({ id: 'level_vvkg048zziz9ktiy', children: [] })
|
|
566
|
+
const room: [number, number][] = [
|
|
567
|
+
[-3.6221682640435353, -9.037533979512114],
|
|
568
|
+
[-6.21504662384804, -8.29671159099654],
|
|
569
|
+
[-5.712152486161543, -6.5365821090938],
|
|
570
|
+
[-5.712152486161543, -5],
|
|
571
|
+
[-8.223564365687542, -5],
|
|
572
|
+
[-8.223564365687542, -6.5365821090938],
|
|
573
|
+
[-10.712152486161543, -6.5365821090938],
|
|
574
|
+
[-11.712152486161543, -8.5365821090938],
|
|
575
|
+
[-4.119274126357038, -10.777404497609373],
|
|
576
|
+
]
|
|
577
|
+
const wall15 = WallNode.parse({
|
|
578
|
+
parentId: level.id,
|
|
579
|
+
start: [-10.712152486161543, -6.5365821090938],
|
|
580
|
+
end: [-11.712152486161543, -8.5365821090938],
|
|
581
|
+
})
|
|
582
|
+
const wall12 = WallNode.parse({
|
|
583
|
+
parentId: level.id,
|
|
584
|
+
start: [-11.712152486161543, -8.5365821090938],
|
|
585
|
+
end: [-6.712152486161543, -10.0365821090938],
|
|
586
|
+
})
|
|
587
|
+
const slab = SlabNode.parse({ parentId: level.id, polygon: room, elevation: 0.05 })
|
|
588
|
+
const ceiling = CeilingNode.parse({ parentId: level.id, polygon: room, height: 2.49 })
|
|
589
|
+
withNodes([level, wall15, wall12, slab, ceiling], () => {
|
|
590
|
+
const plan = computeGroupPlanBox([wall15.id, ceiling.id, slab.id, wall12.id], level.id)!
|
|
591
|
+
expect(plan.minX).toBeLessThan(-11.71)
|
|
592
|
+
expect(plan.minX).toBeGreaterThan(-11.95)
|
|
593
|
+
expect(plan.maxX).toBeCloseTo(-3.62, 1)
|
|
594
|
+
expect(plan.minZ).toBeCloseTo(-10.78, 1)
|
|
595
|
+
expect(plan.maxZ).toBeCloseTo(-5, 1)
|
|
596
|
+
expect(plan.maxX - plan.minX).toBeLessThan(8.4)
|
|
597
|
+
})
|
|
598
|
+
})
|
|
599
|
+
})
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
calculateLevelMiters,
|
|
5
|
+
getWallPlanFootprint,
|
|
4
6
|
nodeRegistry,
|
|
5
7
|
resolveBuildingForLevel,
|
|
6
8
|
sceneRegistry,
|
|
9
|
+
useScene,
|
|
10
|
+
type WallNode,
|
|
7
11
|
} from '@pascal-app/core'
|
|
8
|
-
import { Box3, Matrix4 } from 'three'
|
|
12
|
+
import { Box3, type BufferGeometry, Matrix4, type Object3D, Vector3 } from 'three'
|
|
9
13
|
|
|
10
14
|
// Shared plumbing for the group transform gizmos (rotate + move). Both operate
|
|
11
15
|
// on the same multi-selection: classify each participant by how its placement
|
|
@@ -242,48 +246,6 @@ export function collectParticipants(
|
|
|
242
246
|
return { starts, links }
|
|
243
247
|
}
|
|
244
248
|
|
|
245
|
-
// Grow a selection to the full connected component of walls/fences: any
|
|
246
|
-
// endpoint node transitively reachable through shared junctions from a selected
|
|
247
|
-
// endpoint node joins in, so the whole rigid structure transforms as one piece
|
|
248
|
-
// (rather than tearing/stretching at the boundary). Non-endpoint selections
|
|
249
|
-
// (items, columns) pass through unchanged.
|
|
250
|
-
export function expandToComponent(
|
|
251
|
-
selectedIds: string[],
|
|
252
|
-
sceneNodes: Record<string, AnyNode | undefined>,
|
|
253
|
-
levelId: string | null,
|
|
254
|
-
): string[] {
|
|
255
|
-
const endpoints: { id: string; start: Vec2; end: Vec2 }[] = []
|
|
256
|
-
for (const [id, node] of Object.entries(sceneNodes)) {
|
|
257
|
-
if (classifyParticipant(node, levelId, sceneNodes) === 'endpoint') {
|
|
258
|
-
const n = node as AnyNode & { start: Vec2; end: Vec2 }
|
|
259
|
-
endpoints.push({ id, start: [n.start[0], n.start[1]], end: [n.end[0], n.end[1]] })
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const included = new Set(selectedIds)
|
|
263
|
-
if (!endpoints.some((e) => included.has(e.id))) return selectedIds
|
|
264
|
-
|
|
265
|
-
let changed = true
|
|
266
|
-
while (changed) {
|
|
267
|
-
changed = false
|
|
268
|
-
for (const e of endpoints) {
|
|
269
|
-
if (included.has(e.id)) continue
|
|
270
|
-
const touches = endpoints.some(
|
|
271
|
-
(o) =>
|
|
272
|
-
included.has(o.id) &&
|
|
273
|
-
(nearPoint(e.start, o.start) ||
|
|
274
|
-
nearPoint(e.start, o.end) ||
|
|
275
|
-
nearPoint(e.end, o.start) ||
|
|
276
|
-
nearPoint(e.end, o.end)),
|
|
277
|
-
)
|
|
278
|
-
if (touches) {
|
|
279
|
-
included.add(e.id)
|
|
280
|
-
changed = true
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return Array.from(included)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
249
|
// Per-node field patch, keyed for `useLiveNodeOverrides.setMany` during a live
|
|
288
250
|
// preview and for the single batched `updateNodes` on commit.
|
|
289
251
|
export type GroupPatch = readonly [AnyNodeId, Record<string, unknown>]
|
|
@@ -389,9 +351,24 @@ export function rotateGroupSnapshots(
|
|
|
389
351
|
|
|
390
352
|
export type GroupPlanBounds = { minX: number; minZ: number; maxX: number; maxZ: number }
|
|
391
353
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
354
|
+
const planCorner = new Vector3()
|
|
355
|
+
const planMatrix = new Matrix4()
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The selection's footprint in the level frame — the plan's own coordinates,
|
|
359
|
+
* measured from node data wherever the plan draws from data: walls by their
|
|
360
|
+
* mitered outline, polygon hosts (slab, ceiling, zone) by their polygon, fences
|
|
361
|
+
* by their run. Only placed objects (items, columns, stairs…) measure their
|
|
362
|
+
* meshes: bounds go through `frameInv × matrixWorld` (a world box can't be
|
|
363
|
+
* carried into a rotated building's frame), placeholders — unbuilt while the
|
|
364
|
+
* 3D scene is paused in 2D-only view — are skipped, and a node with no built
|
|
365
|
+
* mesh falls back to its anchor.
|
|
366
|
+
*/
|
|
367
|
+
export function groupPlanBounds(
|
|
368
|
+
starts: ParticipantStart[],
|
|
369
|
+
frameInv: Matrix4,
|
|
370
|
+
): GroupPlanBounds | null {
|
|
371
|
+
const nodes = useScene.getState().nodes
|
|
395
372
|
let minX = Number.POSITIVE_INFINITY
|
|
396
373
|
let minZ = Number.POSITIVE_INFINITY
|
|
397
374
|
let maxX = Number.NEGATIVE_INFINITY
|
|
@@ -402,43 +379,72 @@ function participantExtents(starts: ParticipantStart[]): GroupPlanBounds | null
|
|
|
402
379
|
maxX = Math.max(maxX, x)
|
|
403
380
|
maxZ = Math.max(maxZ, z)
|
|
404
381
|
}
|
|
382
|
+
const miters = new Map<string | null, ReturnType<typeof calculateLevelMiters>>()
|
|
383
|
+
const wallOutline = (wall: WallNode) => {
|
|
384
|
+
const levelId = wall.parentId ?? null
|
|
385
|
+
let levelMiters = miters.get(levelId)
|
|
386
|
+
if (!levelMiters) {
|
|
387
|
+
levelMiters = calculateLevelMiters(
|
|
388
|
+
Object.values(nodes).filter(
|
|
389
|
+
(node): node is WallNode => node?.type === 'wall' && (node.parentId ?? null) === levelId,
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
miters.set(levelId, levelMiters)
|
|
393
|
+
}
|
|
394
|
+
return getWallPlanFootprint(wall, levelMiters)
|
|
395
|
+
}
|
|
396
|
+
const measureMeshes = (id: string) => {
|
|
397
|
+
const obj = sceneRegistry.nodes.get(id as AnyNodeId)
|
|
398
|
+
if (!obj) return false
|
|
399
|
+
obj.updateWorldMatrix(true, true)
|
|
400
|
+
let found = false
|
|
401
|
+
obj.traverse((child: Object3D) => {
|
|
402
|
+
const geometry = (child as Object3D & { geometry?: BufferGeometry }).geometry
|
|
403
|
+
if (!child.visible || !geometry || geometry.userData.placeholder) return
|
|
404
|
+
// Always fresh: geometries rebuilt in place keep a stale cached box.
|
|
405
|
+
geometry.computeBoundingBox()
|
|
406
|
+
const bounds = geometry.boundingBox
|
|
407
|
+
if (!bounds || bounds.isEmpty()) return
|
|
408
|
+
planMatrix.multiplyMatrices(frameInv, child.matrixWorld)
|
|
409
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
410
|
+
planCorner
|
|
411
|
+
.set(
|
|
412
|
+
corner & 1 ? bounds.max.x : bounds.min.x,
|
|
413
|
+
corner & 2 ? bounds.max.y : bounds.min.y,
|
|
414
|
+
corner & 4 ? bounds.max.z : bounds.min.z,
|
|
415
|
+
)
|
|
416
|
+
.applyMatrix4(planMatrix)
|
|
417
|
+
reach(planCorner.x, planCorner.z)
|
|
418
|
+
}
|
|
419
|
+
found = true
|
|
420
|
+
})
|
|
421
|
+
return found
|
|
422
|
+
}
|
|
405
423
|
for (const s of starts) {
|
|
424
|
+
const node = nodes[s.id]
|
|
406
425
|
if (s.kind === 'endpoint') {
|
|
426
|
+
const outline = node?.type === 'wall' ? wallOutline(node) : []
|
|
427
|
+
if (outline.length > 0) {
|
|
428
|
+
for (const point of outline) reach(point.x, point.y)
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
407
431
|
reach(s.start[0], s.start[1])
|
|
408
432
|
reach(s.end[0], s.end[1])
|
|
433
|
+
const path = (node as { path?: unknown } | undefined)?.path
|
|
434
|
+
if (isVec2Array(path)) for (const [x, z] of path) reach(x, z)
|
|
409
435
|
} else if (s.kind === 'polygon') {
|
|
410
|
-
for (const [x, z] of s.polygon)
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
} else {
|
|
436
|
+
for (const [x, z] of s.polygon) reach(x, z)
|
|
437
|
+
} else if (!measureMeshes(s.id)) {
|
|
414
438
|
reach(s.position[0], s.position[2])
|
|
415
439
|
}
|
|
416
440
|
}
|
|
417
|
-
|
|
418
|
-
return { minX, minZ, maxX, maxZ }
|
|
441
|
+
return Number.isFinite(minX) ? { minX, minZ, maxX, maxZ } : null
|
|
419
442
|
}
|
|
420
443
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// points alone sit metres inside a wide selection's real footprint, so a
|
|
426
|
-
// gesture that pivots on the data extents orbits a different point than the
|
|
427
|
-
// idle keyboard rotate and the rotate gizmos, which both use the mesh box.
|
|
428
|
-
export function groupPlanBounds(
|
|
429
|
-
box: Box3 | null,
|
|
430
|
-
starts: ParticipantStart[],
|
|
431
|
-
frameInv: Matrix4,
|
|
432
|
-
): GroupPlanBounds | null {
|
|
433
|
-
if (!box) return participantExtents(starts)
|
|
434
|
-
const min = box.min.clone().applyMatrix4(frameInv)
|
|
435
|
-
const max = box.max.clone().applyMatrix4(frameInv)
|
|
436
|
-
return {
|
|
437
|
-
minX: Math.min(min.x, max.x),
|
|
438
|
-
minZ: Math.min(min.z, max.z),
|
|
439
|
-
maxX: Math.max(min.x, max.x),
|
|
440
|
-
maxZ: Math.max(min.z, max.z),
|
|
441
|
-
}
|
|
444
|
+
/** `groupPlanBounds` for a selection: the dashed boxes and gizmos measure this. */
|
|
445
|
+
export function computeGroupPlanBox(ids: string[], levelId: string | null): GroupPlanBounds | null {
|
|
446
|
+
const { starts } = collectParticipants(ids, useScene.getState().nodes, levelId)
|
|
447
|
+
return groupPlanBounds(starts, levelFrame(levelId).inverse)
|
|
442
448
|
}
|
|
443
449
|
|
|
444
450
|
export const planBoundsCenter = (b: GroupPlanBounds): Vec2 => [
|