@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,91 @@
|
|
|
1
|
+
import type { IconRef, LazyComponent } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export type EditorHostPanelWorkspace = string & {}
|
|
4
|
+
|
|
5
|
+
export type EditorHostPanel = {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
icon: IconRef
|
|
9
|
+
component: LazyComponent
|
|
10
|
+
kinds?: readonly string[]
|
|
11
|
+
workspaces?: readonly EditorHostPanelWorkspace[]
|
|
12
|
+
pluginId?: string
|
|
13
|
+
description?: string
|
|
14
|
+
creator?: {
|
|
15
|
+
name: string
|
|
16
|
+
url?: string
|
|
17
|
+
}
|
|
18
|
+
pluginUrl?: string
|
|
19
|
+
defaultInstalled?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDevMode(): boolean {
|
|
23
|
+
try {
|
|
24
|
+
const meta = import.meta as { env?: { DEV?: boolean } }
|
|
25
|
+
if (typeof meta?.env?.DEV === 'boolean') return meta.env.DEV
|
|
26
|
+
} catch {
|
|
27
|
+
// import.meta unavailable in some CJS contexts — fall through.
|
|
28
|
+
}
|
|
29
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV) {
|
|
30
|
+
return process.env.NODE_ENV !== 'production'
|
|
31
|
+
}
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class EditorHostPanelRegistryImpl {
|
|
36
|
+
private readonly panels = new Map<string, EditorHostPanel>()
|
|
37
|
+
private readonly listeners = new Set<() => void>()
|
|
38
|
+
private cached: EditorHostPanel[] = []
|
|
39
|
+
|
|
40
|
+
subscribe = (onChange: () => void): (() => void) => {
|
|
41
|
+
this.listeners.add(onChange)
|
|
42
|
+
return () => {
|
|
43
|
+
this.listeners.delete(onChange)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getSnapshot = (): EditorHostPanel[] => this.cached
|
|
48
|
+
|
|
49
|
+
panelForKind = (kind: string): string | undefined =>
|
|
50
|
+
this.cached.find((panel) => panel.kinds?.includes(kind))?.id
|
|
51
|
+
|
|
52
|
+
getDefaultInstalledPluginIds = (): string[] =>
|
|
53
|
+
Array.from(
|
|
54
|
+
new Set(
|
|
55
|
+
this.cached
|
|
56
|
+
.filter((panel) => panel.pluginId && panel.defaultInstalled)
|
|
57
|
+
.map((panel) => panel.pluginId as string),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
reset(): void {
|
|
62
|
+
this.panels.clear()
|
|
63
|
+
this.emit()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
registerPanel(panel: EditorHostPanel): void {
|
|
67
|
+
if (typeof panel.id !== 'string' || panel.id.length === 0) {
|
|
68
|
+
throw new Error('[editor:host-panels] panel id must be a non-empty string')
|
|
69
|
+
}
|
|
70
|
+
if (this.panels.has(panel.id)) {
|
|
71
|
+
if (isDevMode()) {
|
|
72
|
+
console.warn(`[editor:host-panels] re-registering panel "${panel.id}" (HMR)`)
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error(`[editor:host-panels] duplicate panel id: "${panel.id}" already registered`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
this.panels.set(panel.id, panel)
|
|
78
|
+
this.emit()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private emit(): void {
|
|
82
|
+
this.cached = Array.from(this.panels.values())
|
|
83
|
+
for (const listener of this.listeners) listener()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const editorHostPanelRegistry = new EditorHostPanelRegistryImpl()
|
|
88
|
+
|
|
89
|
+
export function registerEditorHostPanel(panel: EditorHostPanel): void {
|
|
90
|
+
editorHostPanelRegistry.registerPanel(panel)
|
|
91
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createQuickMeasurementPointerScheduler } from './quick-measurement'
|
|
3
|
+
|
|
4
|
+
function pointer(clientX: number, clientY: number): PointerEvent {
|
|
5
|
+
return { clientX, clientY } as PointerEvent
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createFrameDriver() {
|
|
9
|
+
let nextId = 1
|
|
10
|
+
const callbacks = new Map<number, FrameRequestCallback>()
|
|
11
|
+
return {
|
|
12
|
+
driver: {
|
|
13
|
+
request: (callback: FrameRequestCallback) => {
|
|
14
|
+
const id = nextId++
|
|
15
|
+
callbacks.set(id, callback)
|
|
16
|
+
return id
|
|
17
|
+
},
|
|
18
|
+
cancel: (frameId: number) => callbacks.delete(frameId),
|
|
19
|
+
},
|
|
20
|
+
pending: () => callbacks.size,
|
|
21
|
+
runNext: (timestamp: number) => {
|
|
22
|
+
const next = callbacks.entries().next().value as [number, FrameRequestCallback] | undefined
|
|
23
|
+
if (!next) throw new Error('No animation frame scheduled')
|
|
24
|
+
callbacks.delete(next[0])
|
|
25
|
+
next[1](timestamp)
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('createQuickMeasurementPointerScheduler', () => {
|
|
31
|
+
test('keeps only the latest pointer event and caps expensive work', () => {
|
|
32
|
+
const frames = createFrameDriver()
|
|
33
|
+
const processed: PointerEvent[] = []
|
|
34
|
+
const scheduler = createQuickMeasurementPointerScheduler(
|
|
35
|
+
(event) => processed.push(event),
|
|
36
|
+
frames.driver,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
scheduler.enqueue(pointer(1, 1))
|
|
40
|
+
scheduler.enqueue(pointer(8, 4))
|
|
41
|
+
expect(frames.pending()).toBe(1)
|
|
42
|
+
frames.runNext(0)
|
|
43
|
+
expect(processed.map((event) => [event.clientX, event.clientY])).toEqual([[8, 4]])
|
|
44
|
+
|
|
45
|
+
scheduler.enqueue(pointer(12, 4))
|
|
46
|
+
frames.runNext(16)
|
|
47
|
+
expect(processed).toHaveLength(1)
|
|
48
|
+
expect(frames.pending()).toBe(1)
|
|
49
|
+
|
|
50
|
+
scheduler.enqueue(pointer(20, 7))
|
|
51
|
+
frames.runNext(32)
|
|
52
|
+
expect(processed.map((event) => [event.clientX, event.clientY])).toEqual([
|
|
53
|
+
[8, 4],
|
|
54
|
+
[20, 7],
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('ignores sub-pixel jitter and cancels queued work', () => {
|
|
59
|
+
const frames = createFrameDriver()
|
|
60
|
+
const processed: PointerEvent[] = []
|
|
61
|
+
const scheduler = createQuickMeasurementPointerScheduler(
|
|
62
|
+
(event) => processed.push(event),
|
|
63
|
+
frames.driver,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
scheduler.enqueue(pointer(10, 10))
|
|
67
|
+
frames.runNext(0)
|
|
68
|
+
scheduler.enqueue(pointer(10.5, 10.5))
|
|
69
|
+
frames.runNext(32)
|
|
70
|
+
expect(processed).toHaveLength(1)
|
|
71
|
+
|
|
72
|
+
scheduler.enqueue(pointer(20, 20))
|
|
73
|
+
scheduler.clear()
|
|
74
|
+
expect(frames.pending()).toBe(0)
|
|
75
|
+
expect(processed).toHaveLength(1)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
type GeometryContext,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
type QuickMeasurementReport,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
|
|
9
|
+
const QUICK_MEASUREMENT_QUERY_INTERVAL_MS = 30
|
|
10
|
+
const QUICK_MEASUREMENT_MIN_DISTANCE_SQ = 1
|
|
11
|
+
|
|
12
|
+
export function createQuickMeasurementPointerScheduler(
|
|
13
|
+
onPointerMove: (event: PointerEvent) => void,
|
|
14
|
+
frameDriver: {
|
|
15
|
+
request: (callback: FrameRequestCallback) => number
|
|
16
|
+
cancel: (frameId: number) => void
|
|
17
|
+
} = {
|
|
18
|
+
request: (callback) => requestAnimationFrame(callback),
|
|
19
|
+
cancel: (frameId) => cancelAnimationFrame(frameId),
|
|
20
|
+
},
|
|
21
|
+
): {
|
|
22
|
+
enqueue: (event: PointerEvent) => void
|
|
23
|
+
clear: () => void
|
|
24
|
+
} {
|
|
25
|
+
let latestEvent: PointerEvent | null = null
|
|
26
|
+
let frameId: number | null = null
|
|
27
|
+
let lastProcessedAt = Number.NEGATIVE_INFINITY
|
|
28
|
+
let lastClientX = Number.NaN
|
|
29
|
+
let lastClientY = Number.NaN
|
|
30
|
+
|
|
31
|
+
const schedule = () => {
|
|
32
|
+
if (frameId === null) frameId = frameDriver.request(flush)
|
|
33
|
+
}
|
|
34
|
+
const flush = (timestamp: number) => {
|
|
35
|
+
frameId = null
|
|
36
|
+
if (!latestEvent) return
|
|
37
|
+
if (timestamp - lastProcessedAt < QUICK_MEASUREMENT_QUERY_INTERVAL_MS) {
|
|
38
|
+
schedule()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const event = latestEvent
|
|
43
|
+
latestEvent = null
|
|
44
|
+
const deltaX = event.clientX - lastClientX
|
|
45
|
+
const deltaY = event.clientY - lastClientY
|
|
46
|
+
if (
|
|
47
|
+
Number.isFinite(lastClientX) &&
|
|
48
|
+
deltaX * deltaX + deltaY * deltaY < QUICK_MEASUREMENT_MIN_DISTANCE_SQ
|
|
49
|
+
) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lastClientX = event.clientX
|
|
54
|
+
lastClientY = event.clientY
|
|
55
|
+
lastProcessedAt = timestamp
|
|
56
|
+
onPointerMove(event)
|
|
57
|
+
if (latestEvent) schedule()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
enqueue: (event) => {
|
|
62
|
+
latestEvent = event
|
|
63
|
+
schedule()
|
|
64
|
+
},
|
|
65
|
+
clear: () => {
|
|
66
|
+
if (frameId !== null) frameDriver.cancel(frameId)
|
|
67
|
+
latestEvent = null
|
|
68
|
+
frameId = null
|
|
69
|
+
lastProcessedAt = Number.NEGATIVE_INFINITY
|
|
70
|
+
lastClientX = Number.NaN
|
|
71
|
+
lastClientY = Number.NaN
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function quickMeasurementContext(
|
|
77
|
+
node: AnyNode,
|
|
78
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
79
|
+
): GeometryContext {
|
|
80
|
+
const resolve: GeometryContext['resolve'] = <N = AnyNode>(id: AnyNodeId) =>
|
|
81
|
+
nodes[id] as N | undefined
|
|
82
|
+
const childIds =
|
|
83
|
+
'children' in node && Array.isArray(node.children) ? (node.children as AnyNodeId[]) : []
|
|
84
|
+
const children = childIds
|
|
85
|
+
.map((id) => nodes[id])
|
|
86
|
+
.filter((child): child is AnyNode => child !== undefined)
|
|
87
|
+
const parent = node.parentId ? (nodes[node.parentId as AnyNodeId] ?? null) : null
|
|
88
|
+
const siblings =
|
|
89
|
+
parent && 'children' in parent && Array.isArray(parent.children)
|
|
90
|
+
? (parent.children as AnyNodeId[])
|
|
91
|
+
.map((id) => nodes[id])
|
|
92
|
+
.filter(
|
|
93
|
+
(sibling): sibling is AnyNode => sibling !== undefined && sibling.type === node.type,
|
|
94
|
+
)
|
|
95
|
+
: []
|
|
96
|
+
|
|
97
|
+
return { resolve, children, parent, siblings }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveQuickMeasurementReport(
|
|
101
|
+
nodeId: string | null,
|
|
102
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
103
|
+
): QuickMeasurementReport | null {
|
|
104
|
+
if (!nodeId) return null
|
|
105
|
+
const node = nodes[nodeId as AnyNodeId]
|
|
106
|
+
if (!node || node.visible === false) return null
|
|
107
|
+
const quickMeasure = nodeRegistry.get(node.type)?.measurement?.quickMeasure
|
|
108
|
+
return quickMeasure ? quickMeasure(node, quickMeasurementContext(node, nodes)) : null
|
|
109
|
+
}
|
|
@@ -80,7 +80,7 @@ export function duplicateRoofSubtree(
|
|
|
80
80
|
const scene = useScene.getState()
|
|
81
81
|
const sourceRoof = scene.nodes[sourceRoofId]
|
|
82
82
|
|
|
83
|
-
if (
|
|
83
|
+
if (sourceRoof?.type !== 'roof') {
|
|
84
84
|
throw new Error(`Node "${sourceRoofId}" is not a roof`)
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -105,7 +105,7 @@ export function duplicateRoofSubtree(
|
|
|
105
105
|
const segmentClones: RoofSegmentNode[] = []
|
|
106
106
|
for (const childId of sourceRoof.children ?? []) {
|
|
107
107
|
const childNode = scene.nodes[childId as AnyNodeId]
|
|
108
|
-
if (
|
|
108
|
+
if (childNode?.type !== 'roof-segment') {
|
|
109
109
|
continue
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -125,7 +125,7 @@ export function duplicateRoofSubtree(
|
|
|
125
125
|
|
|
126
126
|
const nextScene = useScene.getState()
|
|
127
127
|
const createdRoof = nextScene.nodes[roofClone.id as AnyNodeId]
|
|
128
|
-
if (
|
|
128
|
+
if (createdRoof?.type !== 'roof') {
|
|
129
129
|
throw new Error(`Duplicated roof "${roofClone.id}" was not created`)
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -151,7 +151,7 @@ export function duplicateRoofSubtree(
|
|
|
151
151
|
|
|
152
152
|
const invalidSegment = segmentIds.find((segmentId) => {
|
|
153
153
|
const segment = nextScene.nodes[segmentId as AnyNodeId]
|
|
154
|
-
return
|
|
154
|
+
return segment?.type !== 'roof-segment' || segment.parentId !== createdRoof.id
|
|
155
155
|
})
|
|
156
156
|
if (invalidSegment) {
|
|
157
157
|
throw new Error(
|
|
@@ -175,11 +175,11 @@ export function duplicateRoofSubtree(
|
|
|
175
175
|
|
|
176
176
|
export function clearRoofDuplicateMetadata(
|
|
177
177
|
roofId: AnyNodeId,
|
|
178
|
-
updates: Partial<Pick<RoofNode, 'position' | 'rotation' | 'metadata'>> = {},
|
|
178
|
+
updates: Partial<Pick<RoofNode, 'position' | 'rotation' | 'metadata' | 'visible'>> = {},
|
|
179
179
|
) {
|
|
180
180
|
const scene = useScene.getState()
|
|
181
181
|
const roofNode = scene.nodes[roofId]
|
|
182
|
-
if (
|
|
182
|
+
if (roofNode?.type !== 'roof') {
|
|
183
183
|
return
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -198,7 +198,7 @@ export function clearRoofDuplicateMetadata(
|
|
|
198
198
|
|
|
199
199
|
for (const childId of roofNode.children ?? []) {
|
|
200
200
|
const childNode = scene.nodes[childId as AnyNodeId]
|
|
201
|
-
if (
|
|
201
|
+
if (childNode?.type !== 'roof-segment') {
|
|
202
202
|
continue
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
|
|
2
|
+
import type * as THREE from 'three'
|
|
3
|
+
|
|
4
|
+
export const HOVERED_ROOF_SEGMENT_OUTLINE_PROXY_NAME = '__roof-hover-outline-proxy__'
|
|
5
|
+
|
|
6
|
+
function hoveredRoofSegmentOutlineProxyName(segmentId: string) {
|
|
7
|
+
return `${HOVERED_ROOF_SEGMENT_OUTLINE_PROXY_NAME}:${segmentId}`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getHoveredRoofSegmentOutlineProxy(segmentId: string): THREE.Object3D | null {
|
|
11
|
+
const segment = useScene.getState().nodes[segmentId as AnyNodeId]
|
|
12
|
+
if (!(segment?.type === 'roof-segment' && segment.parentId)) return null
|
|
13
|
+
return (
|
|
14
|
+
sceneRegistry.nodes
|
|
15
|
+
.get(segment.parentId)
|
|
16
|
+
?.getObjectByName(hoveredRoofSegmentOutlineProxyName(segmentId)) ?? null
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getHoveredRoofSegmentOutlineProxyName(segmentId: string): string {
|
|
21
|
+
return hoveredRoofSegmentOutlineProxyName(segmentId)
|
|
22
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
getRoofSegmentWallFaces,
|
|
4
|
+
getScaledDimensions,
|
|
5
|
+
type ItemNode,
|
|
6
|
+
type RoofNode,
|
|
7
|
+
type RoofSegmentNode,
|
|
8
|
+
type RoofSegmentWallFace,
|
|
9
|
+
type RoofWallFaceId,
|
|
10
|
+
sceneRegistry,
|
|
11
|
+
segmentPointToRoofWallFace,
|
|
12
|
+
useScene,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import * as THREE from 'three'
|
|
15
|
+
|
|
16
|
+
const worldPoint = new THREE.Vector3()
|
|
17
|
+
const worldNormal = new THREE.Vector3()
|
|
18
|
+
const localPoint = new THREE.Vector3()
|
|
19
|
+
const localNormal = new THREE.Vector3()
|
|
20
|
+
const inverseMatrix = new THREE.Matrix4()
|
|
21
|
+
|
|
22
|
+
export type RoofWallHit = {
|
|
23
|
+
segment: RoofSegmentNode
|
|
24
|
+
face: RoofSegmentWallFace
|
|
25
|
+
/** Face coords of the hit (u along the face, v above the segment base). */
|
|
26
|
+
u: number
|
|
27
|
+
v: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pointer hits more than this far off the wall plane are not wall hits. */
|
|
31
|
+
const PLANE_TOLERANCE = 0.06
|
|
32
|
+
/** Reject faces whose normal disagrees with the hit normal (slope / soffit). */
|
|
33
|
+
const NORMAL_ALIGNMENT = 0.7
|
|
34
|
+
/** A wall face is vertical; slope faces on low pitches have |ny| ≫ 0. */
|
|
35
|
+
const MAX_NORMAL_Y = 0.4
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a pointer hit on a roof to one of its segments' vertical wall
|
|
39
|
+
* faces (base walls under the roof + the coplanar gable/shed/gambrel end
|
|
40
|
+
* faces). Counterpart of `resolveRoofSegmentHit`, which resolves to the
|
|
41
|
+
* sloped top surface instead.
|
|
42
|
+
*
|
|
43
|
+
* `normal` must be the raw `NodeEvent.normal` (hit-object-local) together
|
|
44
|
+
* with the `object` it came from — roof events can originate from the
|
|
45
|
+
* merged-roof mesh (roof-local frame) or a painted segment mesh
|
|
46
|
+
* (segment-local frame), so the normal is normalised through world space
|
|
47
|
+
* here instead of trusting the event frame.
|
|
48
|
+
*
|
|
49
|
+
* Lives in `@pascal-app/editor` because both the kind-owned door/window
|
|
50
|
+
* tools (in `@pascal-app/nodes`, which depends on editor) and the item
|
|
51
|
+
* placement coordinator (in editor itself) consume it.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveRoofWallHit(
|
|
54
|
+
roof: RoofNode,
|
|
55
|
+
position: [number, number, number],
|
|
56
|
+
normal: [number, number, number] | undefined,
|
|
57
|
+
object: THREE.Object3D | undefined,
|
|
58
|
+
): RoofWallHit | null {
|
|
59
|
+
if (!normal || !object) return null
|
|
60
|
+
|
|
61
|
+
worldPoint.set(position[0], position[1], position[2])
|
|
62
|
+
worldNormal.set(normal[0], normal[1], normal[2])
|
|
63
|
+
object.updateWorldMatrix(true, false)
|
|
64
|
+
worldNormal.transformDirection(object.matrixWorld)
|
|
65
|
+
|
|
66
|
+
const state = useScene.getState()
|
|
67
|
+
let best: { hit: RoofWallHit; score: number } | null = null
|
|
68
|
+
|
|
69
|
+
for (const childId of roof.children ?? []) {
|
|
70
|
+
const segment = state.nodes[childId as AnyNodeId] as RoofSegmentNode | undefined
|
|
71
|
+
if (segment?.type !== 'roof-segment') continue
|
|
72
|
+
const segObj = sceneRegistry.nodes.get(segment.id)
|
|
73
|
+
if (!segObj) continue
|
|
74
|
+
segObj.updateWorldMatrix(true, false)
|
|
75
|
+
|
|
76
|
+
localPoint.copy(worldPoint)
|
|
77
|
+
segObj.worldToLocal(localPoint)
|
|
78
|
+
inverseMatrix.copy(segObj.matrixWorld).invert()
|
|
79
|
+
localNormal.copy(worldNormal).transformDirection(inverseMatrix)
|
|
80
|
+
|
|
81
|
+
if (Math.abs(localNormal.y) > MAX_NORMAL_Y) continue
|
|
82
|
+
|
|
83
|
+
for (const face of getRoofSegmentWallFaces(segment)) {
|
|
84
|
+
const alignment =
|
|
85
|
+
localNormal.x * face.normal[0] +
|
|
86
|
+
localNormal.y * face.normal[1] +
|
|
87
|
+
localNormal.z * face.normal[2]
|
|
88
|
+
if (alignment < NORMAL_ALIGNMENT) continue
|
|
89
|
+
|
|
90
|
+
const { u, v, dist } = segmentPointToRoofWallFace(segment, face.id, [
|
|
91
|
+
localPoint.x,
|
|
92
|
+
localPoint.y,
|
|
93
|
+
localPoint.z,
|
|
94
|
+
])
|
|
95
|
+
if (Math.abs(dist) > PLANE_TOLERANCE) continue
|
|
96
|
+
if (u < -PLANE_TOLERANCE || u > face.length + PLANE_TOLERANCE) continue
|
|
97
|
+
if (v < -PLANE_TOLERANCE) continue
|
|
98
|
+
|
|
99
|
+
const score = Math.abs(dist)
|
|
100
|
+
if (!best || score < best.score) {
|
|
101
|
+
best = { hit: { segment, face, u, v }, score }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return best?.hit ?? null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Overlap guard for nodes sharing a roof-segment wall face — the
|
|
111
|
+
* roof-host analogue of `hasWallChildOverlap`. Hosted children store
|
|
112
|
+
* FACE-LOCAL coords + an explicit `roofFace`, so siblings compare
|
|
113
|
+
* directly: doors/windows are center-anchored in v, wall items
|
|
114
|
+
* bottom-anchored.
|
|
115
|
+
*/
|
|
116
|
+
export function hasRoofFaceChildOverlap(
|
|
117
|
+
segment: RoofSegmentNode,
|
|
118
|
+
faceId: RoofWallFaceId,
|
|
119
|
+
u: number,
|
|
120
|
+
v: number,
|
|
121
|
+
width: number,
|
|
122
|
+
height: number,
|
|
123
|
+
ignoreId?: string,
|
|
124
|
+
): boolean {
|
|
125
|
+
const nodes = useScene.getState().nodes
|
|
126
|
+
const newLeft = u - width / 2
|
|
127
|
+
const newRight = u + width / 2
|
|
128
|
+
const newBottom = v - height / 2
|
|
129
|
+
const newTop = v + height / 2
|
|
130
|
+
|
|
131
|
+
for (const childId of segment.children ?? []) {
|
|
132
|
+
if (childId === ignoreId) continue
|
|
133
|
+
const child = nodes[childId as AnyNodeId]
|
|
134
|
+
if (!child) continue
|
|
135
|
+
if ((child as { roofFace?: RoofWallFaceId }).roofFace !== faceId) continue
|
|
136
|
+
const position = (child as { position?: [number, number, number] }).position
|
|
137
|
+
if (!position) continue
|
|
138
|
+
|
|
139
|
+
let childW: number
|
|
140
|
+
let childBottom: number
|
|
141
|
+
let childTop: number
|
|
142
|
+
if (child.type === 'door' || child.type === 'window') {
|
|
143
|
+
const opening = child as { width: number; height: number }
|
|
144
|
+
childW = opening.width
|
|
145
|
+
childBottom = position[1] - opening.height / 2
|
|
146
|
+
childTop = position[1] + opening.height / 2
|
|
147
|
+
} else if (child.type === 'item') {
|
|
148
|
+
const item = child as ItemNode
|
|
149
|
+
if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side') continue
|
|
150
|
+
const [w, h] = getScaledDimensions(item)
|
|
151
|
+
childW = w
|
|
152
|
+
// Items anchor position[1] at their bottom edge.
|
|
153
|
+
childBottom = position[1]
|
|
154
|
+
childTop = position[1] + h
|
|
155
|
+
} else {
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const xOverlap = newLeft < position[0] + childW / 2 && newRight > position[0] - childW / 2
|
|
160
|
+
const yOverlap = newBottom < childTop && newTop > childBottom
|
|
161
|
+
if (xOverlap && yOverlap) return true
|
|
162
|
+
}
|
|
163
|
+
return false
|
|
164
|
+
}
|