@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,132 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, type ShelfNode, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import Image from 'next/image'
|
|
6
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
7
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
8
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
9
|
+
import {
|
|
10
|
+
focusTreeNode,
|
|
11
|
+
handleTreeSelection,
|
|
12
|
+
routeTreeSelectionToNode,
|
|
13
|
+
TreeNode,
|
|
14
|
+
TreeNodeWrapper,
|
|
15
|
+
} from './tree-node'
|
|
16
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
17
|
+
|
|
18
|
+
interface ShelfTreeNodeProps {
|
|
19
|
+
nodeId: ShelfNode['id']
|
|
20
|
+
depth: number
|
|
21
|
+
isLast?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sidebar tree entry for shelf. Mirrors `item-tree-node`'s shape so the
|
|
26
|
+
* shelf's hosted items list as collapsible children — same pattern items
|
|
27
|
+
* use for their nested items. The shelf has its own `children: ItemNode[`id`]`
|
|
28
|
+
* field on the schema; items reparent into it via `def.surfaces` + the
|
|
29
|
+
* placement coordinator's shelf strategy.
|
|
30
|
+
*/
|
|
31
|
+
export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
32
|
+
nodeId,
|
|
33
|
+
depth,
|
|
34
|
+
isLast,
|
|
35
|
+
}: ShelfTreeNodeProps) {
|
|
36
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
37
|
+
const [expanded, setExpanded] = useState(true)
|
|
38
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
39
|
+
const children = useScene(
|
|
40
|
+
useShallow((s) => (s.nodes[nodeId] as ShelfNode | undefined)?.children ?? []),
|
|
41
|
+
)
|
|
42
|
+
const node = useScene((s) => s.nodes[nodeId] as ShelfNode | undefined)
|
|
43
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
44
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
45
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
46
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
47
|
+
|
|
48
|
+
// Expand when a descendant is selected — same imperative subscription
|
|
49
|
+
// the item tree-node uses, so we don't re-render when unrelated
|
|
50
|
+
// selection-state ticks.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
return useViewer.subscribe((state) => {
|
|
53
|
+
const { selectedIds } = state.selection
|
|
54
|
+
if (selectedIds.length === 0) return
|
|
55
|
+
const nodes = useScene.getState().nodes
|
|
56
|
+
for (const id of selectedIds) {
|
|
57
|
+
let current = nodes[id as AnyNodeId]
|
|
58
|
+
while (current?.parentId) {
|
|
59
|
+
if (current.parentId === nodeId) {
|
|
60
|
+
setExpanded(true)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
current = nodes[current.parentId as AnyNodeId]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}, [nodeId])
|
|
68
|
+
|
|
69
|
+
const handleClick = useCallback(
|
|
70
|
+
(e: React.MouseEvent) => {
|
|
71
|
+
e.stopPropagation()
|
|
72
|
+
handleTreeSelection(
|
|
73
|
+
e,
|
|
74
|
+
nodeId,
|
|
75
|
+
useViewer.getState().selection.selectedIds,
|
|
76
|
+
setSelection,
|
|
77
|
+
)
|
|
78
|
+
routeTreeSelectionToNode(node)
|
|
79
|
+
},
|
|
80
|
+
[node, nodeId, setSelection],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
84
|
+
const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
|
|
85
|
+
const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
|
|
86
|
+
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
87
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
88
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
89
|
+
|
|
90
|
+
const hasChildren = children.length > 0
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<TreeNodeWrapper
|
|
94
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
95
|
+
depth={depth}
|
|
96
|
+
expanded={expanded}
|
|
97
|
+
hasChildren={hasChildren}
|
|
98
|
+
icon={
|
|
99
|
+
<Image alt="" className="object-contain" height={14} src="/icons/shelf.webp" width={14} />
|
|
100
|
+
}
|
|
101
|
+
isHovered={isHovered}
|
|
102
|
+
isLast={isLast}
|
|
103
|
+
isSelected={isSelected}
|
|
104
|
+
isVisible={isVisible}
|
|
105
|
+
label={
|
|
106
|
+
<InlineRenameInput
|
|
107
|
+
defaultName="Shelf"
|
|
108
|
+
isEditing={isEditing}
|
|
109
|
+
nodeId={nodeId}
|
|
110
|
+
onStartEditing={handleStartEditing}
|
|
111
|
+
onStopEditing={handleStopEditing}
|
|
112
|
+
/>
|
|
113
|
+
}
|
|
114
|
+
nodeId={nodeId}
|
|
115
|
+
onClick={handleClick}
|
|
116
|
+
onDoubleClick={handleDoubleClick}
|
|
117
|
+
onMouseEnter={handleMouseEnter}
|
|
118
|
+
onMouseLeave={handleMouseLeave}
|
|
119
|
+
onToggle={handleToggle}
|
|
120
|
+
>
|
|
121
|
+
{hasChildren &&
|
|
122
|
+
children.map((childId, index) => (
|
|
123
|
+
<TreeNode
|
|
124
|
+
depth={depth + 1}
|
|
125
|
+
isLast={index === children.length - 1}
|
|
126
|
+
key={childId}
|
|
127
|
+
nodeId={childId}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</TreeNodeWrapper>
|
|
131
|
+
)
|
|
132
|
+
})
|
|
@@ -2,6 +2,7 @@ import { type AnyNodeId, type SlabNode, useScene } from '@pascal-app/core'
|
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import Image from 'next/image'
|
|
4
4
|
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import { formatAreaLabel } from './../../../../../lib/measurements'
|
|
5
6
|
import useEditor from './../../../../../store/use-editor'
|
|
6
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
8
|
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
@@ -25,6 +26,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
|
|
|
25
26
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
26
27
|
const setSelection = useViewer((state) => state.setSelection)
|
|
27
28
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
29
|
+
const unit = useViewer((state) => state.unit)
|
|
28
30
|
|
|
29
31
|
const handleClick = useCallback(
|
|
30
32
|
(e: React.MouseEvent) => {
|
|
@@ -45,8 +47,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
|
|
|
45
47
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
46
48
|
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
47
49
|
|
|
48
|
-
const
|
|
49
|
-
const defaultName = `Slab (${area}m²)`
|
|
50
|
+
const defaultName = `Slab (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
|
|
50
51
|
|
|
51
52
|
return (
|
|
52
53
|
<TreeNodeWrapper
|
|
@@ -55,7 +56,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
|
|
|
55
56
|
expanded={false}
|
|
56
57
|
hasChildren={false}
|
|
57
58
|
icon={
|
|
58
|
-
<Image alt="" className="object-contain" height={14} src="/icons/floor.
|
|
59
|
+
<Image alt="" className="object-contain" height={14} src="/icons/floor.webp" width={14} />
|
|
59
60
|
}
|
|
60
61
|
isHovered={isHovered}
|
|
61
62
|
isLast={isLast}
|
|
@@ -91,8 +92,11 @@ function calculatePolygonArea(polygon: Array<[number, number]>): number {
|
|
|
91
92
|
|
|
92
93
|
for (let i = 0; i < n; i++) {
|
|
93
94
|
const j = (i + 1) % n
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
const pi = polygon[i]
|
|
96
|
+
const pj = polygon[j]
|
|
97
|
+
if (!(pi && pj)) continue
|
|
98
|
+
area += pi[0] * pj[1]
|
|
99
|
+
area -= pj[0] * pi[1]
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
return Math.abs(area) / 2
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type AnyNodeId, type SolarPanelNode, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import { SnapTargetIcon } from '../../../snap-target-badge'
|
|
6
|
+
import useEditor from './../../../../../store/use-editor'
|
|
7
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
8
|
+
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
9
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
10
|
+
|
|
11
|
+
interface SolarPanelTreeNodeProps {
|
|
12
|
+
nodeId: AnyNodeId
|
|
13
|
+
depth: number
|
|
14
|
+
isLast?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const SolarPanelTreeNode = memo(function SolarPanelTreeNode({
|
|
18
|
+
nodeId,
|
|
19
|
+
depth,
|
|
20
|
+
isLast,
|
|
21
|
+
}: SolarPanelTreeNodeProps) {
|
|
22
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
23
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
24
|
+
const node = useScene((s) => s.nodes[nodeId] as SolarPanelNode | undefined)
|
|
25
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
26
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
27
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
28
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
29
|
+
|
|
30
|
+
const handleClick = useCallback(
|
|
31
|
+
(e: React.MouseEvent) => {
|
|
32
|
+
e.stopPropagation()
|
|
33
|
+
const handled = handleTreeSelection(
|
|
34
|
+
e,
|
|
35
|
+
nodeId,
|
|
36
|
+
useViewer.getState().selection.selectedIds,
|
|
37
|
+
setSelection,
|
|
38
|
+
)
|
|
39
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
40
|
+
useEditor.getState().setPhase('structure')
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[nodeId, setSelection],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const defaultName = node?.name || 'Solar Panel'
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<TreeNodeWrapper
|
|
50
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
51
|
+
depth={depth}
|
|
52
|
+
expanded={false}
|
|
53
|
+
hasChildren={false}
|
|
54
|
+
icon={
|
|
55
|
+
<SnapTargetIcon target="roof">
|
|
56
|
+
<Image
|
|
57
|
+
alt=""
|
|
58
|
+
className="object-contain opacity-60"
|
|
59
|
+
height={14}
|
|
60
|
+
src="/icons/roof.webp"
|
|
61
|
+
width={14}
|
|
62
|
+
/>
|
|
63
|
+
</SnapTargetIcon>
|
|
64
|
+
}
|
|
65
|
+
isHovered={isHovered}
|
|
66
|
+
isLast={isLast}
|
|
67
|
+
isSelected={isSelected}
|
|
68
|
+
isVisible={isVisible}
|
|
69
|
+
label={
|
|
70
|
+
<InlineRenameInput
|
|
71
|
+
defaultName={defaultName}
|
|
72
|
+
isEditing={isEditing}
|
|
73
|
+
nodeId={nodeId}
|
|
74
|
+
onStartEditing={() => setIsEditing(true)}
|
|
75
|
+
onStopEditing={() => setIsEditing(false)}
|
|
76
|
+
/>
|
|
77
|
+
}
|
|
78
|
+
nodeId={nodeId}
|
|
79
|
+
onClick={handleClick}
|
|
80
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
81
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
82
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
83
|
+
onToggle={() => {}}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
})
|
|
@@ -50,7 +50,13 @@ export const SpawnTreeNode = memo(function SpawnTreeNode({
|
|
|
50
50
|
expanded={false}
|
|
51
51
|
hasChildren={false}
|
|
52
52
|
icon={
|
|
53
|
-
<Image
|
|
53
|
+
<Image
|
|
54
|
+
alt=""
|
|
55
|
+
className="object-contain"
|
|
56
|
+
height={14}
|
|
57
|
+
src="/icons/spawn-point.webp"
|
|
58
|
+
width={14}
|
|
59
|
+
/>
|
|
54
60
|
}
|
|
55
61
|
isHovered={isHovered}
|
|
56
62
|
isLast={isLast}
|
|
@@ -98,7 +98,7 @@ export const StairTreeNode = memo(function StairTreeNode({
|
|
|
98
98
|
expanded={expanded}
|
|
99
99
|
hasChildren={segments.length > 0}
|
|
100
100
|
icon={
|
|
101
|
-
<Image alt="" className="object-contain" height={14} src="/icons/stairs.
|
|
101
|
+
<Image alt="" className="object-contain" height={14} src="/icons/stairs.webp" width={14} />
|
|
102
102
|
}
|
|
103
103
|
isDropTarget={isValidDropTarget && isDropTarget}
|
|
104
104
|
isHovered={isHovered || isDropTarget}
|
|
@@ -206,7 +206,7 @@ function StairSegmentTreeNode({
|
|
|
206
206
|
alt=""
|
|
207
207
|
className="object-contain opacity-60"
|
|
208
208
|
height={14}
|
|
209
|
-
src="/icons/stairs.
|
|
209
|
+
src="/icons/stairs.webp"
|
|
210
210
|
width={14}
|
|
211
211
|
/>
|
|
212
212
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
|
1
|
+
import { type AnyNode, type AnyNodeId, emitter, nodeRegistry, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
2
3
|
import { ChevronRight } from 'lucide-react'
|
|
3
4
|
import { AnimatePresence, motion } from 'motion/react'
|
|
4
5
|
import { forwardRef, memo, useEffect, useRef } from 'react'
|
|
6
|
+
import { resolveNodeSelectionTarget } from '../../../../../lib/selection-routing'
|
|
7
|
+
import useEditor from '../../../../../store/use-editor'
|
|
5
8
|
|
|
6
9
|
export function handleTreeSelection(
|
|
7
10
|
e: React.MouseEvent,
|
|
@@ -53,16 +56,48 @@ export function focusTreeNode(nodeId: AnyNodeId) {
|
|
|
53
56
|
emitter.emit('camera-controls:focus', { nodeId })
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
export function routeTreeSelectionToNode(node: AnyNode | null | undefined) {
|
|
60
|
+
const target = node ? resolveNodeSelectionTarget(node) : null
|
|
61
|
+
if (!target) return
|
|
62
|
+
|
|
63
|
+
const selectedIdsAfterClick = useViewer.getState().selection.selectedIds
|
|
64
|
+
const editor = useEditor.getState()
|
|
65
|
+
let didRoute = false
|
|
66
|
+
|
|
67
|
+
if (target.phase !== editor.phase) {
|
|
68
|
+
editor.setPhase(target.phase)
|
|
69
|
+
didRoute = true
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
target.phase === 'structure' &&
|
|
73
|
+
target.structureLayer &&
|
|
74
|
+
target.structureLayer !== useEditor.getState().structureLayer
|
|
75
|
+
) {
|
|
76
|
+
useEditor.getState().setStructureLayer(target.structureLayer)
|
|
77
|
+
didRoute = true
|
|
78
|
+
}
|
|
79
|
+
if (didRoute) {
|
|
80
|
+
useViewer.getState().setSelection({ selectedIds: selectedIdsAfterClick })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
import { cn } from '../../../../../lib/utils'
|
|
57
85
|
import { BuildingTreeNode } from './building-tree-node'
|
|
58
86
|
import { CeilingTreeNode } from './ceiling-tree-node'
|
|
87
|
+
import { ChimneyTreeNode } from './chimney-tree-node'
|
|
59
88
|
import { ColumnTreeNode } from './column-tree-node'
|
|
60
89
|
import { DoorTreeNode } from './door-tree-node'
|
|
90
|
+
import { DormerTreeNode } from './dormer-tree-node'
|
|
91
|
+
import { ElevatorTreeNode } from './elevator-tree-node'
|
|
61
92
|
import { FenceTreeNode } from './fence-tree-node'
|
|
93
|
+
import { GutterTreeNode } from './gutter-tree-node'
|
|
62
94
|
import { ItemTreeNode } from './item-tree-node'
|
|
63
95
|
import { LevelTreeNode } from './level-tree-node'
|
|
96
|
+
import { RegistryTreeNode } from './registry-tree-node'
|
|
64
97
|
import { RoofTreeNode } from './roof-tree-node'
|
|
98
|
+
import { ShelfTreeNode } from './shelf-tree-node'
|
|
65
99
|
import { SlabTreeNode } from './slab-tree-node'
|
|
100
|
+
import { SolarPanelTreeNode } from './solar-panel-tree-node'
|
|
66
101
|
import { SpawnTreeNode } from './spawn-tree-node'
|
|
67
102
|
import { StairTreeNode } from './stair-tree-node'
|
|
68
103
|
import { WallTreeNode } from './wall-tree-node'
|
|
@@ -75,45 +110,83 @@ interface TreeNodeProps {
|
|
|
75
110
|
isLast?: boolean
|
|
76
111
|
}
|
|
77
112
|
|
|
113
|
+
// Per-kind tree-node components keyed by `node.type`. Lookup replaces
|
|
114
|
+
// the legacy switch — adding a kind to this map is now the only edit
|
|
115
|
+
// needed in this file (the switch's `case '<kind>':` clauses were
|
|
116
|
+
// flagged by the Phase 6 grep gate as the last per-kind dispatch
|
|
117
|
+
// outside the registry; future work moves these to a
|
|
118
|
+
// `def.presentation`-driven generic tree-node and removes this map
|
|
119
|
+
// entirely).
|
|
120
|
+
const treeNodeByType: Record<
|
|
121
|
+
string,
|
|
122
|
+
React.ComponentType<{ depth: number; isLast?: boolean; nodeId: AnyNodeId }>
|
|
123
|
+
> = {
|
|
124
|
+
building: BuildingTreeNode as React.ComponentType<{
|
|
125
|
+
depth: number
|
|
126
|
+
isLast?: boolean
|
|
127
|
+
nodeId: AnyNodeId
|
|
128
|
+
}>,
|
|
129
|
+
cabinet: RegistryTreeNode,
|
|
130
|
+
'cabinet-module': RegistryTreeNode,
|
|
131
|
+
'box-vent': RegistryTreeNode,
|
|
132
|
+
ceiling: CeilingTreeNode,
|
|
133
|
+
chimney: ChimneyTreeNode,
|
|
134
|
+
dormer: DormerTreeNode,
|
|
135
|
+
downspout: RegistryTreeNode,
|
|
136
|
+
'solar-panel': SolarPanelTreeNode,
|
|
137
|
+
column: ColumnTreeNode,
|
|
138
|
+
elevator: ElevatorTreeNode,
|
|
139
|
+
level: LevelTreeNode as React.ComponentType<{
|
|
140
|
+
depth: number
|
|
141
|
+
isLast?: boolean
|
|
142
|
+
nodeId: AnyNodeId
|
|
143
|
+
}>,
|
|
144
|
+
shelf: ShelfTreeNode as React.ComponentType<{
|
|
145
|
+
depth: number
|
|
146
|
+
isLast?: boolean
|
|
147
|
+
nodeId: AnyNodeId
|
|
148
|
+
}>,
|
|
149
|
+
slab: SlabTreeNode,
|
|
150
|
+
spawn: SpawnTreeNode as React.ComponentType<{
|
|
151
|
+
depth: number
|
|
152
|
+
isLast?: boolean
|
|
153
|
+
nodeId: AnyNodeId
|
|
154
|
+
}>,
|
|
155
|
+
wall: WallTreeNode,
|
|
156
|
+
fence: FenceTreeNode,
|
|
157
|
+
gutter: GutterTreeNode,
|
|
158
|
+
measurement: RegistryTreeNode,
|
|
159
|
+
'ridge-vent': RegistryTreeNode,
|
|
160
|
+
'turbine-vent': RegistryTreeNode,
|
|
161
|
+
cupola: RegistryTreeNode,
|
|
162
|
+
'eyebrow-vent': RegistryTreeNode,
|
|
163
|
+
skylight: RegistryTreeNode,
|
|
164
|
+
roof: RoofTreeNode,
|
|
165
|
+
stair: StairTreeNode,
|
|
166
|
+
door: DoorTreeNode,
|
|
167
|
+
window: WindowTreeNode,
|
|
168
|
+
zone: ZoneTreeNode as React.ComponentType<{
|
|
169
|
+
depth: number
|
|
170
|
+
isLast?: boolean
|
|
171
|
+
nodeId: AnyNodeId
|
|
172
|
+
}>,
|
|
173
|
+
item: ItemTreeNode,
|
|
174
|
+
}
|
|
175
|
+
|
|
78
176
|
export const TreeNode = memo(function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
|
|
177
|
+
// Registry-driven row hiding (`def.tree.hidden`) — primitive boolean
|
|
178
|
+
// selector so unrelated scene updates don't re-render every row.
|
|
179
|
+
const shouldHide = useScene((state) => {
|
|
180
|
+
const node = state.nodes[nodeId]
|
|
181
|
+
if (!node) return false
|
|
182
|
+
return nodeRegistry.get(node.type)?.tree?.hidden?.(node, state.nodes) ?? false
|
|
183
|
+
})
|
|
79
184
|
const nodeType = useScene((state) => state.nodes[nodeId]?.type)
|
|
80
|
-
|
|
185
|
+
if (shouldHide) return null
|
|
81
186
|
if (!nodeType) return null
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<BuildingTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `building_${string}`} />
|
|
87
|
-
)
|
|
88
|
-
case 'ceiling':
|
|
89
|
-
return <CeilingTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
90
|
-
case 'column':
|
|
91
|
-
return <ColumnTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
92
|
-
case 'level':
|
|
93
|
-
return <LevelTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `level_${string}`} />
|
|
94
|
-
case 'slab':
|
|
95
|
-
return <SlabTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
96
|
-
case 'spawn':
|
|
97
|
-
return <SpawnTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `spawn_${string}`} />
|
|
98
|
-
case 'wall':
|
|
99
|
-
return <WallTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
100
|
-
case 'fence':
|
|
101
|
-
return <FenceTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
102
|
-
case 'roof':
|
|
103
|
-
return <RoofTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
104
|
-
case 'stair':
|
|
105
|
-
return <StairTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
106
|
-
case 'item':
|
|
107
|
-
return <ItemTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
108
|
-
case 'door':
|
|
109
|
-
return <DoorTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
110
|
-
case 'window':
|
|
111
|
-
return <WindowTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
112
|
-
case 'zone':
|
|
113
|
-
return <ZoneTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `zone_${string}`} />
|
|
114
|
-
default:
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
187
|
+
const Component = treeNodeByType[nodeType]
|
|
188
|
+
if (!Component) return null
|
|
189
|
+
return <Component depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
117
190
|
})
|
|
118
191
|
|
|
119
192
|
interface TreeNodeWrapperProps {
|
|
@@ -234,10 +307,10 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
|
|
234
307
|
</motion.div>
|
|
235
308
|
) : null}
|
|
236
309
|
</button>
|
|
237
|
-
<div className="flex min-w-0 flex-1 items-center gap-
|
|
310
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
238
311
|
<span
|
|
239
312
|
className={cn(
|
|
240
|
-
'flex h-
|
|
313
|
+
'flex h-5 w-5 shrink-0 items-center justify-center transition-all duration-200',
|
|
241
314
|
!isSelected && 'opacity-60 grayscale',
|
|
242
315
|
)}
|
|
243
316
|
>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type AnyNode, type AnyNodeId, nodeRegistry } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Child ids the sidebar tree renders under a node: the kind's
|
|
5
|
+
* `def.tree.childIds` override when declared, otherwise the node's own
|
|
6
|
+
* `children`. Kind-agnostic — kinds that reshape their subtree (hidden
|
|
7
|
+
* derived nodes, flattened containers) do so via the registry hook.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveTreeChildIds(
|
|
10
|
+
nodeId: AnyNodeId,
|
|
11
|
+
nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>,
|
|
12
|
+
): AnyNodeId[] {
|
|
13
|
+
const node = nodes[nodeId]
|
|
14
|
+
if (!node) return []
|
|
15
|
+
const childIds = nodeRegistry.get(node.type)?.tree?.childIds
|
|
16
|
+
if (childIds) return childIds(node, nodes)
|
|
17
|
+
const children = (node as { children?: unknown }).children
|
|
18
|
+
return Array.isArray(children) ? (children as AnyNodeId[]) : []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether `targetId` appears anywhere under `nodeId` in the *rendered*
|
|
23
|
+
* sidebar tree (i.e. walking registry-overridden child ids, not the raw
|
|
24
|
+
* scene graph). Drives auto-expansion when a descendant gets selected.
|
|
25
|
+
*/
|
|
26
|
+
export function treeContainsDescendant(
|
|
27
|
+
nodeId: AnyNodeId,
|
|
28
|
+
targetId: AnyNodeId,
|
|
29
|
+
nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>,
|
|
30
|
+
): boolean {
|
|
31
|
+
for (const childId of resolveTreeChildIds(nodeId, nodes)) {
|
|
32
|
+
if (childId === targetId) return true
|
|
33
|
+
if (treeContainsDescendant(childId, targetId, nodes)) return true
|
|
34
|
+
}
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
@@ -79,7 +79,7 @@ export const WallTreeNode = memo(function WallTreeNode({
|
|
|
79
79
|
expanded={expanded}
|
|
80
80
|
hasChildren={children.length > 0}
|
|
81
81
|
icon={
|
|
82
|
-
<Image alt="" className="object-contain" height={14} src="/icons/wall.
|
|
82
|
+
<Image alt="" className="object-contain" height={14} src="/icons/wall.webp" width={14} />
|
|
83
83
|
}
|
|
84
84
|
isHovered={isHovered}
|
|
85
85
|
isLast={isLast}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
3
|
+
import { type AnyNodeId, type WindowNode, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import Image from 'next/image'
|
|
6
6
|
import { memo, useCallback, useState } from 'react'
|
|
7
|
+
import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
|
|
7
8
|
import useEditor from './../../../../../store/use-editor'
|
|
8
9
|
import { InlineRenameInput } from './inline-rename-input'
|
|
9
10
|
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
@@ -22,6 +23,7 @@ export const WindowTreeNode = memo(function WindowTreeNode({
|
|
|
22
23
|
}: WindowTreeNodeProps) {
|
|
23
24
|
const [isEditing, setIsEditing] = useState(false)
|
|
24
25
|
const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
|
|
26
|
+
const node = useScene((s) => s.nodes[nodeId] as WindowNode | undefined)
|
|
25
27
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
26
28
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
27
29
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -45,6 +47,7 @@ export const WindowTreeNode = memo(function WindowTreeNode({
|
|
|
45
47
|
|
|
46
48
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
47
49
|
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
50
|
+
const snapTarget = resolveNodeSnapTarget(node) ?? 'wall'
|
|
48
51
|
|
|
49
52
|
return (
|
|
50
53
|
<TreeNodeWrapper
|
|
@@ -53,7 +56,9 @@ export const WindowTreeNode = memo(function WindowTreeNode({
|
|
|
53
56
|
expanded={false}
|
|
54
57
|
hasChildren={false}
|
|
55
58
|
icon={
|
|
56
|
-
<
|
|
59
|
+
<SnapTargetIcon target={snapTarget}>
|
|
60
|
+
<Image alt="" className="object-contain" height={14} src="/icons/window.webp" width={14} />
|
|
61
|
+
</SnapTargetIcon>
|
|
57
62
|
}
|
|
58
63
|
isHovered={isHovered}
|
|
59
64
|
isLast={isLast}
|
|
@@ -2,6 +2,7 @@ import { useScene, type ZoneNode } from '@pascal-app/core'
|
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { memo, useCallback, useState } from 'react'
|
|
4
4
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
5
|
+
import { formatAreaLabel } from './../../../../../lib/measurements'
|
|
5
6
|
import { InlineRenameInput } from './inline-rename-input'
|
|
6
7
|
import { focusTreeNode, TreeNodeWrapper } from './tree-node'
|
|
7
8
|
import { TreeNodeActions } from './tree-node-actions'
|
|
@@ -26,6 +27,7 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
|
|
|
26
27
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
27
28
|
const setSelection = useViewer((state) => state.setSelection)
|
|
28
29
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
30
|
+
const unit = useViewer((state) => state.unit)
|
|
29
31
|
|
|
30
32
|
const handleClick = useCallback(() => setSelection({ zoneId: nodeId }), [nodeId, setSelection])
|
|
31
33
|
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
@@ -34,9 +36,7 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
|
|
|
34
36
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
35
37
|
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
const area = calculatePolygonArea(polygon).toFixed(1)
|
|
39
|
-
const defaultName = `Zone (${area}m²)`
|
|
39
|
+
const defaultName = `Zone (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
42
|
<TreeNodeWrapper
|