@pascal-app/editor 0.8.0 → 0.9.2
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 +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
nodeRegistry,
|
|
5
|
+
resolveBuildingForLevel,
|
|
6
|
+
sceneRegistry,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
import { Box3, Matrix4 } from 'three'
|
|
9
|
+
|
|
10
|
+
// Shared plumbing for the group transform gizmos (rotate + move). Both operate
|
|
11
|
+
// on the same multi-selection: classify each participant by how its placement
|
|
12
|
+
// transforms, snapshot pre-drag state, and pull connected wall/fence neighbours
|
|
13
|
+
// along so junctions stay welded.
|
|
14
|
+
|
|
15
|
+
// Outward clearance from a bbox corner so a gizmo doesn't sit on the geometry.
|
|
16
|
+
export const CORNER_OFFSET = 0.3
|
|
17
|
+
// Two endpoints within this distance count as the same junction — a hair looser
|
|
18
|
+
// than the store's 1e-6 so near-but-not-exact corners still hold together.
|
|
19
|
+
const JUNCTION_EPS = 1e-4
|
|
20
|
+
|
|
21
|
+
export type Vec2 = [number, number]
|
|
22
|
+
export type Vec3 = [number, number, number]
|
|
23
|
+
|
|
24
|
+
const isVec3 = (v: unknown): v is Vec3 =>
|
|
25
|
+
Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number')
|
|
26
|
+
const isVec2 = (v: unknown): v is Vec2 =>
|
|
27
|
+
Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === 'number')
|
|
28
|
+
const isVec2Array = (v: unknown): v is Vec2[] => Array.isArray(v) && v.length > 0 && v.every(isVec2)
|
|
29
|
+
|
|
30
|
+
// How a participant's placement transforms rigidly around / with the group:
|
|
31
|
+
// - 'vec3' position + [x,y,z] rotation (items, …)
|
|
32
|
+
// - 'scalar' position + numeric rotation (columns)
|
|
33
|
+
// - 'endpoint' start/end tuples (walls, fences)
|
|
34
|
+
// - 'polygon' [x,z] vertex arrays (slabs, ceilings, zones) — the placement
|
|
35
|
+
// lives in the vertices themselves (plus optional hole rings)
|
|
36
|
+
export type ParticipantKind = 'vec3' | 'scalar' | 'endpoint' | 'polygon'
|
|
37
|
+
|
|
38
|
+
// A selected node qualifies when it belongs to the active level's horizontal
|
|
39
|
+
// frame: either parented to that level, or declared building-scoped and parented
|
|
40
|
+
// to the active level's building. Doors/windows parent to their wall, so they're
|
|
41
|
+
// excluded here and ride their wall.
|
|
42
|
+
function isInGroupTransformScope(
|
|
43
|
+
node: AnyNode | undefined,
|
|
44
|
+
levelId: string | null,
|
|
45
|
+
sceneNodes: Record<string, AnyNode | undefined>,
|
|
46
|
+
): boolean {
|
|
47
|
+
if (!node || !levelId) return false
|
|
48
|
+
if (node.parentId === levelId) return true
|
|
49
|
+
|
|
50
|
+
if (nodeRegistry.get(node.type)?.floorplanScope !== 'building') {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const buildingId = resolveBuildingForLevel(
|
|
55
|
+
levelId as AnyNodeId,
|
|
56
|
+
sceneNodes as Record<AnyNodeId, AnyNode>,
|
|
57
|
+
)
|
|
58
|
+
return Boolean(buildingId && node.parentId === buildingId)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getLegacyScenePosition(node: AnyNode): Vec3 | null {
|
|
62
|
+
if (node.type !== 'elevator') return null
|
|
63
|
+
const object = sceneRegistry.nodes.get(node.id)
|
|
64
|
+
if (!object) return [0, 0, 0]
|
|
65
|
+
return [object.position.x, object.position.y, object.position.z]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getParticipantPosition(node: AnyNode): Vec3 | null {
|
|
69
|
+
const p = (node as { position?: unknown }).position
|
|
70
|
+
if (isVec3(p)) return p
|
|
71
|
+
return getLegacyScenePosition(node)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getParticipantScalarRotation(node: AnyNode): number | null {
|
|
75
|
+
const r = (node as { rotation?: unknown }).rotation
|
|
76
|
+
if (typeof r === 'number' && Number.isFinite(r)) return r
|
|
77
|
+
if (node.type !== 'elevator') return null
|
|
78
|
+
return sceneRegistry.nodes.get(node.id)?.rotation.y ?? 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Shape-only classification of a positioned placement (no level-scope check).
|
|
82
|
+
// Used for polygon hosts' attached children, whose parent is the host rather
|
|
83
|
+
// than the level.
|
|
84
|
+
function classifyPlacementShape(node: AnyNode): 'vec3' | 'scalar' | null {
|
|
85
|
+
const p = getParticipantPosition(node)
|
|
86
|
+
const r = (node as { rotation?: unknown }).rotation
|
|
87
|
+
if (isVec3(p) && isVec3(r)) return 'vec3'
|
|
88
|
+
if (isVec3(p) && getParticipantScalarRotation(node) !== null) return 'scalar'
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function classifyParticipant(
|
|
93
|
+
node: AnyNode | undefined,
|
|
94
|
+
levelId: string | null,
|
|
95
|
+
sceneNodes: Record<string, AnyNode | undefined>,
|
|
96
|
+
): ParticipantKind | null {
|
|
97
|
+
if (!node || !isInGroupTransformScope(node, levelId, sceneNodes)) return null
|
|
98
|
+
const shape = classifyPlacementShape(node)
|
|
99
|
+
if (shape) return shape
|
|
100
|
+
const start = (node as { start?: unknown }).start
|
|
101
|
+
const end = (node as { end?: unknown }).end
|
|
102
|
+
if (isVec2(start) && isVec2(end)) return 'endpoint'
|
|
103
|
+
if (isVec2Array((node as { polygon?: unknown }).polygon)) return 'polygon'
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Pre-drag placement snapshot + how to transform it. `holes` is null when the
|
|
108
|
+
// kind carries no holes field (zone), so patches never write one onto it.
|
|
109
|
+
export type ParticipantStart =
|
|
110
|
+
| { id: AnyNodeId; kind: 'vec3'; position: Vec3; rotation: Vec3 }
|
|
111
|
+
| { id: AnyNodeId; kind: 'scalar'; position: Vec3; rotation: number }
|
|
112
|
+
| { id: AnyNodeId; kind: 'endpoint'; start: Vec2; end: Vec2 }
|
|
113
|
+
| { id: AnyNodeId; kind: 'polygon'; polygon: Vec2[]; holes: Vec2[][] | null }
|
|
114
|
+
|
|
115
|
+
// An unselected wall/fence sharing a junction with a transforming endpoint. Only
|
|
116
|
+
// the touching endpoint(s) follow, so the neighbour stays attached while its far
|
|
117
|
+
// end stays put (it stretches, mirroring single-wall move).
|
|
118
|
+
export type LinkedNeighbor = {
|
|
119
|
+
id: AnyNodeId
|
|
120
|
+
start: Vec2
|
|
121
|
+
end: Vec2
|
|
122
|
+
startLinked: boolean
|
|
123
|
+
endLinked: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const nearPoint = (a: Vec2, b: Vec2) =>
|
|
127
|
+
Math.abs(a[0] - b[0]) <= JUNCTION_EPS && Math.abs(a[1] - b[1]) <= JUNCTION_EPS
|
|
128
|
+
|
|
129
|
+
// Snapshot the selected participants and the connected (unselected) wall/fence
|
|
130
|
+
// neighbours whose shared endpoints should follow the transform.
|
|
131
|
+
export function collectParticipants(
|
|
132
|
+
ids: string[],
|
|
133
|
+
sceneNodes: Record<string, AnyNode | undefined>,
|
|
134
|
+
levelId: string | null,
|
|
135
|
+
): { starts: ParticipantStart[]; links: LinkedNeighbor[] } {
|
|
136
|
+
const starts: ParticipantStart[] = []
|
|
137
|
+
for (const id of ids) {
|
|
138
|
+
const node = sceneNodes[id]
|
|
139
|
+
const kind = classifyParticipant(node, levelId, sceneNodes)
|
|
140
|
+
if (!node || !kind) continue
|
|
141
|
+
if (kind === 'vec3') {
|
|
142
|
+
const n = node as AnyNode & { position: Vec3; rotation: Vec3 }
|
|
143
|
+
const position = getParticipantPosition(node)
|
|
144
|
+
if (!position) continue
|
|
145
|
+
starts.push({
|
|
146
|
+
id: id as AnyNodeId,
|
|
147
|
+
kind,
|
|
148
|
+
position: [position[0], position[1], position[2]],
|
|
149
|
+
rotation: [n.rotation[0], n.rotation[1], n.rotation[2]],
|
|
150
|
+
})
|
|
151
|
+
} else if (kind === 'scalar') {
|
|
152
|
+
const position = getParticipantPosition(node)
|
|
153
|
+
const rotation = getParticipantScalarRotation(node)
|
|
154
|
+
if (!(position && rotation !== null)) continue
|
|
155
|
+
starts.push({
|
|
156
|
+
id: id as AnyNodeId,
|
|
157
|
+
kind,
|
|
158
|
+
position: [position[0], position[1], position[2]],
|
|
159
|
+
rotation,
|
|
160
|
+
})
|
|
161
|
+
} else if (kind === 'endpoint') {
|
|
162
|
+
const n = node as AnyNode & { start: Vec2; end: Vec2 }
|
|
163
|
+
starts.push({
|
|
164
|
+
id: id as AnyNodeId,
|
|
165
|
+
kind,
|
|
166
|
+
start: [n.start[0], n.start[1]],
|
|
167
|
+
end: [n.end[0], n.end[1]],
|
|
168
|
+
})
|
|
169
|
+
} else {
|
|
170
|
+
const n = node as AnyNode & { polygon: Vec2[]; holes?: Vec2[][] }
|
|
171
|
+
starts.push({
|
|
172
|
+
id: id as AnyNodeId,
|
|
173
|
+
kind,
|
|
174
|
+
polygon: n.polygon.map(([x, z]) => [x, z] as Vec2),
|
|
175
|
+
holes: Array.isArray(n.holes)
|
|
176
|
+
? n.holes.map((hole) => hole.map(([x, z]) => [x, z] as Vec2))
|
|
177
|
+
: null,
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Polygon hosts (slab/ceiling) rebuild their geometry from vertices rather
|
|
183
|
+
// than transforming a group, so — unlike wall children, which ride the wall
|
|
184
|
+
// mesh — their attached children (ceiling-mounted items) must transform
|
|
185
|
+
// explicitly. Their positions are stored in the level frame (the host group
|
|
186
|
+
// sits at the origin), so the same rigid patches apply.
|
|
187
|
+
const includedIds = new Set<string>(starts.map((s) => s.id))
|
|
188
|
+
for (const s of [...starts]) {
|
|
189
|
+
if (s.kind !== 'polygon') continue
|
|
190
|
+
const host = sceneNodes[s.id]
|
|
191
|
+
const childIds = (host as { children?: string[] } | undefined)?.children
|
|
192
|
+
if (!Array.isArray(childIds)) continue
|
|
193
|
+
for (const childId of childIds) {
|
|
194
|
+
if (includedIds.has(childId)) continue
|
|
195
|
+
const child = sceneNodes[childId]
|
|
196
|
+
if (!child) continue
|
|
197
|
+
const shape = classifyPlacementShape(child)
|
|
198
|
+
const position = child ? getParticipantPosition(child) : null
|
|
199
|
+
if (!(shape && position)) continue
|
|
200
|
+
if (shape === 'vec3') {
|
|
201
|
+
const c = child as AnyNode & { rotation: Vec3 }
|
|
202
|
+
starts.push({
|
|
203
|
+
id: childId as AnyNodeId,
|
|
204
|
+
kind: 'vec3',
|
|
205
|
+
position: [position[0], position[1], position[2]],
|
|
206
|
+
rotation: [c.rotation[0], c.rotation[1], c.rotation[2]],
|
|
207
|
+
})
|
|
208
|
+
} else {
|
|
209
|
+
const rotation = getParticipantScalarRotation(child)
|
|
210
|
+
if (rotation === null) continue
|
|
211
|
+
starts.push({
|
|
212
|
+
id: childId as AnyNodeId,
|
|
213
|
+
kind: 'scalar',
|
|
214
|
+
position: [position[0], position[1], position[2]],
|
|
215
|
+
rotation,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
includedIds.add(childId)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const endpoints: Vec2[] = []
|
|
223
|
+
for (const s of starts) {
|
|
224
|
+
if (s.kind === 'endpoint') endpoints.push(s.start, s.end)
|
|
225
|
+
}
|
|
226
|
+
const links: LinkedNeighbor[] = []
|
|
227
|
+
if (endpoints.length > 0) {
|
|
228
|
+
const selected = new Set(starts.map((s) => s.id))
|
|
229
|
+
for (const [nid, node] of Object.entries(sceneNodes)) {
|
|
230
|
+
if (selected.has(nid as AnyNodeId)) continue
|
|
231
|
+
if (classifyParticipant(node, levelId, sceneNodes) !== 'endpoint') continue
|
|
232
|
+
const n = node as AnyNode & { start: Vec2; end: Vec2 }
|
|
233
|
+
const start: Vec2 = [n.start[0], n.start[1]]
|
|
234
|
+
const end: Vec2 = [n.end[0], n.end[1]]
|
|
235
|
+
const startLinked = endpoints.some((p) => nearPoint(start, p))
|
|
236
|
+
const endLinked = endpoints.some((p) => nearPoint(end, p))
|
|
237
|
+
if (startLinked || endLinked) {
|
|
238
|
+
links.push({ id: nid as AnyNodeId, start, end, startLinked, endLinked })
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { starts, links }
|
|
243
|
+
}
|
|
244
|
+
|
|
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
|
+
// Per-node field patch, keyed for `useLiveNodeOverrides.setMany` during a live
|
|
288
|
+
// preview and for the single batched `updateNodes` on commit.
|
|
289
|
+
export type GroupPatch = readonly [AnyNodeId, Record<string, unknown>]
|
|
290
|
+
|
|
291
|
+
// Rigid group rotation: orbit each participant's anchor point(s) CCW by
|
|
292
|
+
// `delta` (atan2 x→z sense) around `center` (level-frame XZ) and turn yaws by
|
|
293
|
+
// `-delta` to match three.js Y-rotation handedness (same convention as the
|
|
294
|
+
// single-item rotate handle in item/definition.ts). Endpoint nodes
|
|
295
|
+
// (walls/fences) have no yaw — swinging both endpoints around the pivot
|
|
296
|
+
// rotates them rigidly; their curveOffset sagitta is rotation-invariant, so
|
|
297
|
+
// arcs are preserved. Linked neighbours' shared endpoints land exactly on the
|
|
298
|
+
// selected wall's rotated endpoint (rot is deterministic), keeping junctions
|
|
299
|
+
// welded while the far end stays put.
|
|
300
|
+
export function rotateGroupPatches(
|
|
301
|
+
starts: ParticipantStart[],
|
|
302
|
+
links: LinkedNeighbor[],
|
|
303
|
+
center: { x: number; z: number },
|
|
304
|
+
delta: number,
|
|
305
|
+
): GroupPatch[] {
|
|
306
|
+
const cos = Math.cos(delta)
|
|
307
|
+
const sin = Math.sin(delta)
|
|
308
|
+
const rot = (x: number, z: number): Vec2 => {
|
|
309
|
+
const dx = x - center.x
|
|
310
|
+
const dz = z - center.z
|
|
311
|
+
return [center.x + dx * cos - dz * sin, center.z + dx * sin + dz * cos]
|
|
312
|
+
}
|
|
313
|
+
const patches: GroupPatch[] = []
|
|
314
|
+
for (const s of starts) {
|
|
315
|
+
if (s.kind === 'endpoint') {
|
|
316
|
+
patches.push([s.id, { start: rot(s.start[0], s.start[1]), end: rot(s.end[0], s.end[1]) }])
|
|
317
|
+
} else if (s.kind === 'polygon') {
|
|
318
|
+
const patch: Record<string, unknown> = { polygon: s.polygon.map(([x, z]) => rot(x, z)) }
|
|
319
|
+
if (s.holes) patch.holes = s.holes.map((hole) => hole.map(([x, z]) => rot(x, z)))
|
|
320
|
+
patches.push([s.id, patch])
|
|
321
|
+
} else {
|
|
322
|
+
const [px, pz] = rot(s.position[0], s.position[2])
|
|
323
|
+
const position: Vec3 = [px, s.position[1], pz]
|
|
324
|
+
const rotation =
|
|
325
|
+
s.kind === 'vec3'
|
|
326
|
+
? ([s.rotation[0], s.rotation[1] - delta, s.rotation[2]] as Vec3)
|
|
327
|
+
: s.rotation - delta
|
|
328
|
+
patches.push([s.id, { position, rotation }])
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const l of links) {
|
|
332
|
+
patches.push([
|
|
333
|
+
l.id,
|
|
334
|
+
{
|
|
335
|
+
start: l.startLinked ? rot(l.start[0], l.start[1]) : l.start,
|
|
336
|
+
end: l.endLinked ? rot(l.end[0], l.end[1]) : l.end,
|
|
337
|
+
},
|
|
338
|
+
])
|
|
339
|
+
}
|
|
340
|
+
return patches
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Rotate the SNAPSHOTS themselves (same math as `rotateGroupPatches`, but
|
|
344
|
+
// producing new snapshot shapes instead of node patches). Lets an in-flight
|
|
345
|
+
// group move re-seed its rest state after a mid-drag R/T: subsequent
|
|
346
|
+
// translate patches then place the rotated layout at the live delta.
|
|
347
|
+
export function rotateGroupSnapshots(
|
|
348
|
+
starts: ParticipantStart[],
|
|
349
|
+
links: LinkedNeighbor[],
|
|
350
|
+
center: { x: number; z: number },
|
|
351
|
+
delta: number,
|
|
352
|
+
): { starts: ParticipantStart[]; links: LinkedNeighbor[] } {
|
|
353
|
+
const cos = Math.cos(delta)
|
|
354
|
+
const sin = Math.sin(delta)
|
|
355
|
+
const rot = (x: number, z: number): Vec2 => {
|
|
356
|
+
const dx = x - center.x
|
|
357
|
+
const dz = z - center.z
|
|
358
|
+
return [center.x + dx * cos - dz * sin, center.z + dx * sin + dz * cos]
|
|
359
|
+
}
|
|
360
|
+
const rotatedStarts = starts.map((s): ParticipantStart => {
|
|
361
|
+
if (s.kind === 'endpoint') {
|
|
362
|
+
return { ...s, start: rot(s.start[0], s.start[1]), end: rot(s.end[0], s.end[1]) }
|
|
363
|
+
}
|
|
364
|
+
if (s.kind === 'polygon') {
|
|
365
|
+
return {
|
|
366
|
+
...s,
|
|
367
|
+
polygon: s.polygon.map(([x, z]) => rot(x, z)),
|
|
368
|
+
holes: s.holes ? s.holes.map((hole) => hole.map(([x, z]) => rot(x, z))) : null,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const [px, pz] = rot(s.position[0], s.position[2])
|
|
372
|
+
const position: Vec3 = [px, s.position[1], pz]
|
|
373
|
+
if (s.kind === 'vec3') {
|
|
374
|
+
return { ...s, position, rotation: [s.rotation[0], s.rotation[1] - delta, s.rotation[2]] }
|
|
375
|
+
}
|
|
376
|
+
return { ...s, position, rotation: s.rotation - delta }
|
|
377
|
+
})
|
|
378
|
+
// Only the welded (linked) endpoints follow the rotation; the far ends
|
|
379
|
+
// stay put, exactly like the per-tick patch path.
|
|
380
|
+
const rotatedLinks = links.map(
|
|
381
|
+
(l): LinkedNeighbor => ({
|
|
382
|
+
...l,
|
|
383
|
+
start: l.startLinked ? rot(l.start[0], l.start[1]) : l.start,
|
|
384
|
+
end: l.endLinked ? rot(l.end[0], l.end[1]) : l.end,
|
|
385
|
+
}),
|
|
386
|
+
)
|
|
387
|
+
return { starts: rotatedStarts, links: rotatedLinks }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Level-frame XZ extents of the participant DATA — the mesh-free sibling of
|
|
391
|
+
// `computeGroupBox`, used when meshes aren't mounted yet and to re-seed
|
|
392
|
+
// alignment anchors after a mid-drag rotation.
|
|
393
|
+
export function participantExtents(
|
|
394
|
+
starts: ParticipantStart[],
|
|
395
|
+
): { minX: number; minZ: number; maxX: number; maxZ: number } | null {
|
|
396
|
+
let minX = Number.POSITIVE_INFINITY
|
|
397
|
+
let minZ = Number.POSITIVE_INFINITY
|
|
398
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
399
|
+
let maxZ = Number.NEGATIVE_INFINITY
|
|
400
|
+
const reach = (x: number, z: number) => {
|
|
401
|
+
minX = Math.min(minX, x)
|
|
402
|
+
minZ = Math.min(minZ, z)
|
|
403
|
+
maxX = Math.max(maxX, x)
|
|
404
|
+
maxZ = Math.max(maxZ, z)
|
|
405
|
+
}
|
|
406
|
+
for (const s of starts) {
|
|
407
|
+
if (s.kind === 'endpoint') {
|
|
408
|
+
reach(s.start[0], s.start[1])
|
|
409
|
+
reach(s.end[0], s.end[1])
|
|
410
|
+
} else if (s.kind === 'polygon') {
|
|
411
|
+
for (const [x, z] of s.polygon) {
|
|
412
|
+
reach(x, z)
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
reach(s.position[0], s.position[2])
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (!Number.isFinite(minX)) return null
|
|
419
|
+
return { minX, minZ, maxX, maxZ }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Rigid group slide: shift every participant (and each linked neighbour's
|
|
423
|
+
// shared endpoint) by the same level-frame XZ delta. Y and rotations untouched.
|
|
424
|
+
export function translateGroupPatches(
|
|
425
|
+
starts: ParticipantStart[],
|
|
426
|
+
links: LinkedNeighbor[],
|
|
427
|
+
dx: number,
|
|
428
|
+
dz: number,
|
|
429
|
+
): GroupPatch[] {
|
|
430
|
+
const patches: GroupPatch[] = []
|
|
431
|
+
const shift = ([x, z]: Vec2): Vec2 => [x + dx, z + dz]
|
|
432
|
+
for (const s of starts) {
|
|
433
|
+
if (s.kind === 'endpoint') {
|
|
434
|
+
patches.push([s.id, { start: shift(s.start), end: shift(s.end) }])
|
|
435
|
+
} else if (s.kind === 'polygon') {
|
|
436
|
+
const patch: Record<string, unknown> = { polygon: s.polygon.map(shift) }
|
|
437
|
+
if (s.holes) patch.holes = s.holes.map((hole) => hole.map(shift))
|
|
438
|
+
patches.push([s.id, patch])
|
|
439
|
+
} else {
|
|
440
|
+
patches.push([s.id, { position: [s.position[0] + dx, s.position[1], s.position[2] + dz] }])
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const l of links) {
|
|
444
|
+
patches.push([
|
|
445
|
+
l.id,
|
|
446
|
+
{
|
|
447
|
+
start: l.startLinked ? [l.start[0] + dx, l.start[1] + dz] : l.start,
|
|
448
|
+
end: l.endLinked ? [l.end[0] + dx, l.end[1] + dz] : l.end,
|
|
449
|
+
},
|
|
450
|
+
])
|
|
451
|
+
}
|
|
452
|
+
return patches
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Frozen world matrix of the level group + its inverse. A node's placement
|
|
456
|
+
// (`position` / `start` / `end`) is stored in its parent level's frame, but the
|
|
457
|
+
// gizmos raycast the ground plane in WORLD space. When the building is rotated
|
|
458
|
+
// those frames diverge, so a world-space drag delta / rotation pivot must be
|
|
459
|
+
// converted into the level frame before it's written back to placements —
|
|
460
|
+
// otherwise the move drifts off-axis from the cursor and the rotation orbits a
|
|
461
|
+
// displaced centre. Returns identity matrices when the level isn't mounted, which
|
|
462
|
+
// collapses to the old behaviour (world == local) for an unrotated building.
|
|
463
|
+
export function levelFrame(levelId: string | null): { matrix: Matrix4; inverse: Matrix4 } {
|
|
464
|
+
const obj = levelId ? sceneRegistry.nodes.get(levelId as AnyNodeId) : null
|
|
465
|
+
if (!obj) return { matrix: new Matrix4(), inverse: new Matrix4() }
|
|
466
|
+
obj.updateWorldMatrix(true, false)
|
|
467
|
+
const matrix = obj.matrixWorld.clone()
|
|
468
|
+
return { matrix, inverse: matrix.clone().invert() }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// World-space union bounding box of the selected meshes, or null if none are
|
|
472
|
+
// mounted yet. Used to place the gizmos (which are portalled to the scene root,
|
|
473
|
+
// so they live in world space); placement writes convert back to the level frame
|
|
474
|
+
// via `levelFrame`.
|
|
475
|
+
export function computeGroupBox(ids: string[]): Box3 | null {
|
|
476
|
+
const box = new Box3()
|
|
477
|
+
const tmp = new Box3()
|
|
478
|
+
let found = false
|
|
479
|
+
for (const id of ids) {
|
|
480
|
+
const obj = sceneRegistry.nodes.get(id)
|
|
481
|
+
if (!obj) continue
|
|
482
|
+
obj.updateWorldMatrix(true, true)
|
|
483
|
+
tmp.setFromObject(obj)
|
|
484
|
+
if (tmp.isEmpty()) continue
|
|
485
|
+
box.union(tmp)
|
|
486
|
+
found = true
|
|
487
|
+
}
|
|
488
|
+
return found ? box : null
|
|
489
|
+
}
|