@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,86 @@
|
|
|
1
|
+
import { type AnyNodeId, type GutterNode, 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 GutterTreeNodeProps {
|
|
12
|
+
nodeId: AnyNodeId
|
|
13
|
+
depth: number
|
|
14
|
+
isLast?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const GutterTreeNode = memo(function GutterTreeNode({
|
|
18
|
+
nodeId,
|
|
19
|
+
depth,
|
|
20
|
+
isLast,
|
|
21
|
+
}: GutterTreeNodeProps) {
|
|
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 GutterNode | 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 || 'Gutter'
|
|
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
|
+
})
|
|
@@ -36,7 +36,16 @@ import {
|
|
|
36
36
|
buildLevelDuplicateCreateOps,
|
|
37
37
|
type LevelDuplicatePreset,
|
|
38
38
|
} from './../../../../../lib/level-duplication'
|
|
39
|
+
import { getDefaultLevelName } from '@pascal-app/core'
|
|
39
40
|
import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
|
|
41
|
+
import {
|
|
42
|
+
formatAreaLabel,
|
|
43
|
+
getAreaUnitLabel,
|
|
44
|
+
getLinearUnitLabel,
|
|
45
|
+
linearUnitToMeters,
|
|
46
|
+
metersToLinearUnit,
|
|
47
|
+
squareMetersToAreaUnit,
|
|
48
|
+
} from './../../../../../lib/measurements'
|
|
40
49
|
import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
|
|
41
50
|
import { cn } from './../../../../../lib/utils'
|
|
42
51
|
import useEditor from './../../../../../store/use-editor'
|
|
@@ -92,6 +101,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
|
|
|
92
101
|
const updateNode = useScene((state) => state.updateNode)
|
|
93
102
|
const mode = useEditor((state) => state.mode)
|
|
94
103
|
const setMode = useEditor((state) => state.setMode)
|
|
104
|
+
const viewerUnit = useViewer((state) => state.unit)
|
|
95
105
|
|
|
96
106
|
if (!siteNode) return null
|
|
97
107
|
|
|
@@ -100,6 +110,14 @@ const PropertyLineSection = memo(function PropertyLineSection() {
|
|
|
100
110
|
const perimeter = calculatePerimeter(points)
|
|
101
111
|
const isEditing = mode === 'edit'
|
|
102
112
|
|
|
113
|
+
// Property-line coordinates and readouts follow the metric/imperial toggle.
|
|
114
|
+
const isImperial = viewerUnit === 'imperial'
|
|
115
|
+
const linearLabel = getLinearUnitLabel(viewerUnit)
|
|
116
|
+
const toDisplayLinear = (meters: number) => metersToLinearUnit(meters, viewerUnit)
|
|
117
|
+
const toStoredLinear = (display: number) => linearUnitToMeters(display, viewerUnit)
|
|
118
|
+
const displayArea = squareMetersToAreaUnit(area, viewerUnit)
|
|
119
|
+
const displayPerimeter = toDisplayLinear(perimeter)
|
|
120
|
+
|
|
103
121
|
const handleToggleEdit = () => {
|
|
104
122
|
setMode(isEditing ? 'select' : 'edit')
|
|
105
123
|
}
|
|
@@ -165,10 +183,16 @@ const PropertyLineSection = memo(function PropertyLineSection() {
|
|
|
165
183
|
{/* Measurements */}
|
|
166
184
|
<div className="relative flex gap-3 pr-3 pb-2 pl-10">
|
|
167
185
|
<div className="text-muted-foreground text-xs">
|
|
168
|
-
Area:
|
|
186
|
+
Area:{' '}
|
|
187
|
+
<span className="text-foreground">
|
|
188
|
+
{displayArea.toFixed(1)} {getAreaUnitLabel(viewerUnit)}
|
|
189
|
+
</span>
|
|
169
190
|
</div>
|
|
170
191
|
<div className="text-muted-foreground text-xs">
|
|
171
|
-
Perimeter:
|
|
192
|
+
Perimeter:{' '}
|
|
193
|
+
<span className="text-foreground">
|
|
194
|
+
{displayPerimeter.toFixed(1)} {linearLabel}
|
|
195
|
+
</span>
|
|
172
196
|
</div>
|
|
173
197
|
</div>
|
|
174
198
|
|
|
@@ -183,21 +207,21 @@ const PropertyLineSection = memo(function PropertyLineSection() {
|
|
|
183
207
|
<input
|
|
184
208
|
className="w-16 rounded border border-border/50 bg-accent/50 px-1.5 py-0.5 text-foreground text-xs focus:border-primary focus:outline-none"
|
|
185
209
|
onChange={(e) =>
|
|
186
|
-
handlePointChange(index, 0, Number.parseFloat(e.target.value) || 0)
|
|
210
|
+
handlePointChange(index, 0, toStoredLinear(Number.parseFloat(e.target.value) || 0))
|
|
187
211
|
}
|
|
188
212
|
step={0.5}
|
|
189
213
|
type="number"
|
|
190
|
-
value={point[0]}
|
|
214
|
+
value={Number(toDisplayLinear(point[0]).toFixed(2))}
|
|
191
215
|
/>
|
|
192
216
|
<label className="shrink-0 text-muted-foreground">Z</label>
|
|
193
217
|
<input
|
|
194
218
|
className="w-16 rounded border border-border/50 bg-accent/50 px-1.5 py-0.5 text-foreground text-xs focus:border-primary focus:outline-none"
|
|
195
219
|
onChange={(e) =>
|
|
196
|
-
handlePointChange(index, 1, Number.parseFloat(e.target.value) || 0)
|
|
220
|
+
handlePointChange(index, 1, toStoredLinear(Number.parseFloat(e.target.value) || 0))
|
|
197
221
|
}
|
|
198
222
|
step={0.5}
|
|
199
223
|
type="number"
|
|
200
|
-
value={point[1]}
|
|
224
|
+
value={Number(toDisplayLinear(point[1]).toFixed(2))}
|
|
201
225
|
/>
|
|
202
226
|
<button
|
|
203
227
|
className={cn(
|
|
@@ -355,13 +379,13 @@ const ReferenceItem = memo(function ReferenceItem({
|
|
|
355
379
|
<img
|
|
356
380
|
alt="Scan"
|
|
357
381
|
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
|
|
358
|
-
src="/icons/mesh.
|
|
382
|
+
src="/icons/mesh.webp"
|
|
359
383
|
/>
|
|
360
384
|
) : (
|
|
361
385
|
<img
|
|
362
386
|
alt="Guide"
|
|
363
387
|
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
|
|
364
|
-
src="/icons/floorplan.
|
|
388
|
+
src="/icons/floorplan.webp"
|
|
365
389
|
/>
|
|
366
390
|
)}
|
|
367
391
|
<InlineRenameInput
|
|
@@ -720,10 +744,10 @@ const LevelItem = memo(function LevelItem({
|
|
|
720
744
|
'h-4 w-4 shrink-0 object-contain transition-all duration-200',
|
|
721
745
|
!isSelected && 'opacity-60 grayscale',
|
|
722
746
|
)}
|
|
723
|
-
src="/icons/level.
|
|
747
|
+
src="/icons/level.webp"
|
|
724
748
|
/>
|
|
725
749
|
<InlineRenameInput
|
|
726
|
-
defaultName={
|
|
750
|
+
defaultName={getDefaultLevelName(level.level)}
|
|
727
751
|
isEditing={isEditing}
|
|
728
752
|
nodeId={level.id}
|
|
729
753
|
onStartEditing={() => setIsEditing(true)}
|
|
@@ -998,7 +1022,7 @@ const LayerToggle = memo(function LayerToggle() {
|
|
|
998
1022
|
'mb-1 h-6 w-6 transition-all',
|
|
999
1023
|
activeTab !== 'structure' && 'opacity-50 grayscale',
|
|
1000
1024
|
)}
|
|
1001
|
-
src="/icons/room.
|
|
1025
|
+
src="/icons/room.webp"
|
|
1002
1026
|
/>
|
|
1003
1027
|
Structure
|
|
1004
1028
|
</div>
|
|
@@ -1034,7 +1058,7 @@ const LayerToggle = memo(function LayerToggle() {
|
|
|
1034
1058
|
'mb-1 h-6 w-6 transition-all',
|
|
1035
1059
|
activeTab !== 'furnish' && 'opacity-50 grayscale',
|
|
1036
1060
|
)}
|
|
1037
|
-
src="/icons/couch.
|
|
1061
|
+
src="/icons/couch.webp"
|
|
1038
1062
|
/>
|
|
1039
1063
|
Furnish
|
|
1040
1064
|
</div>
|
|
@@ -1071,7 +1095,7 @@ const LayerToggle = memo(function LayerToggle() {
|
|
|
1071
1095
|
'mb-1 h-6 w-6 transition-all',
|
|
1072
1096
|
activeTab !== 'zones' && 'opacity-50 grayscale',
|
|
1073
1097
|
)}
|
|
1074
|
-
src="/icons/kitchen.
|
|
1098
|
+
src="/icons/kitchen.webp"
|
|
1075
1099
|
/>
|
|
1076
1100
|
Zones
|
|
1077
1101
|
</div>
|
|
@@ -1096,6 +1120,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
|
|
|
1096
1120
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
1097
1121
|
const setPhase = useEditor((state) => state.setPhase)
|
|
1098
1122
|
const setMode = useEditor((state) => state.setMode)
|
|
1123
|
+
const unit = useViewer((state) => state.unit)
|
|
1099
1124
|
|
|
1100
1125
|
const isSelected = selectedZoneId === zone.id
|
|
1101
1126
|
const isHovered = hoveredId === zone.id
|
|
@@ -1108,8 +1133,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
|
|
|
1108
1133
|
}
|
|
1109
1134
|
}, [isSelected])
|
|
1110
1135
|
|
|
1111
|
-
const
|
|
1112
|
-
const defaultName = `Zone (${area}m²)`
|
|
1136
|
+
const defaultName = `Zone (${formatAreaLabel(calculatePolygonArea(zone.polygon), unit)})`
|
|
1113
1137
|
|
|
1114
1138
|
const handleClick = () => {
|
|
1115
1139
|
setSelection({ zoneId: zone.id })
|
|
@@ -1412,7 +1436,7 @@ const BuildingItem = memo(function BuildingItem({
|
|
|
1412
1436
|
'h-5 w-5 object-contain transition-all',
|
|
1413
1437
|
!isBuildingActive && 'opacity-60 grayscale',
|
|
1414
1438
|
)}
|
|
1415
|
-
src="/icons/building.
|
|
1439
|
+
src="/icons/building.webp"
|
|
1416
1440
|
/>
|
|
1417
1441
|
<span className="truncate font-medium text-sm">{building.name || 'Building'}</span>
|
|
1418
1442
|
</div>
|
|
@@ -1541,10 +1565,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1541
1565
|
useShallow((s) => {
|
|
1542
1566
|
if (!siteNode) return []
|
|
1543
1567
|
return siteNode.children
|
|
1544
|
-
.map((
|
|
1545
|
-
const id = typeof child === 'string' ? child : child.id
|
|
1546
|
-
return s.nodes[id] as BuildingNode | undefined
|
|
1547
|
-
})
|
|
1568
|
+
.map((childId) => s.nodes[childId as AnyNodeId] as BuildingNode | undefined)
|
|
1548
1569
|
.filter((node): node is BuildingNode => node?.type === 'building')
|
|
1549
1570
|
}),
|
|
1550
1571
|
)
|
|
@@ -1571,7 +1592,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1571
1592
|
'h-5 w-5 object-contain transition-all',
|
|
1572
1593
|
phase !== 'site' && 'opacity-60 grayscale',
|
|
1573
1594
|
)}
|
|
1574
|
-
src="/icons/site.
|
|
1595
|
+
src="/icons/site-flag.webp"
|
|
1575
1596
|
/>
|
|
1576
1597
|
<span className="font-medium text-sm">{siteNode.name || 'Site'}</span>
|
|
1577
1598
|
</div>
|
|
@@ -3,19 +3,25 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
3
3
|
import Image from 'next/image'
|
|
4
4
|
import { memo, useCallback, useEffect, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
|
-
import
|
|
6
|
+
import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
|
|
7
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
focusTreeNode,
|
|
10
|
+
handleTreeSelection,
|
|
11
|
+
routeTreeSelectionToNode,
|
|
12
|
+
TreeNode,
|
|
13
|
+
TreeNodeWrapper,
|
|
14
|
+
} from './tree-node'
|
|
9
15
|
import { TreeNodeActions } from './tree-node-actions'
|
|
10
16
|
|
|
11
17
|
const CATEGORY_ICONS: Record<string, string> = {
|
|
12
|
-
door: '/icons/door.
|
|
13
|
-
window: '/icons/window.
|
|
14
|
-
furniture: '/icons/couch.
|
|
15
|
-
appliance: '/icons/appliance.
|
|
16
|
-
kitchen: '/icons/kitchen.
|
|
17
|
-
bathroom: '/icons/bathroom.
|
|
18
|
-
outdoor: '/icons/tree.
|
|
18
|
+
door: '/icons/door.webp',
|
|
19
|
+
window: '/icons/window.webp',
|
|
20
|
+
furniture: '/icons/couch.webp',
|
|
21
|
+
appliance: '/icons/appliance.webp',
|
|
22
|
+
kitchen: '/icons/kitchen.webp',
|
|
23
|
+
bathroom: '/icons/bathroom.webp',
|
|
24
|
+
outdoor: '/icons/tree.webp',
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
interface ItemTreeNodeProps {
|
|
@@ -35,7 +41,8 @@ export const ItemTreeNode = memo(function ItemTreeNode({
|
|
|
35
41
|
const children = useScene(
|
|
36
42
|
useShallow((s) => (s.nodes[nodeId] as ItemNode | undefined)?.children ?? []),
|
|
37
43
|
)
|
|
38
|
-
const
|
|
44
|
+
const node = useScene((s) => s.nodes[nodeId] as ItemNode | undefined)
|
|
45
|
+
const asset = node?.asset
|
|
39
46
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
40
47
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
41
48
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -63,17 +70,15 @@ export const ItemTreeNode = memo(function ItemTreeNode({
|
|
|
63
70
|
const handleClick = useCallback(
|
|
64
71
|
(e: React.MouseEvent) => {
|
|
65
72
|
e.stopPropagation()
|
|
66
|
-
|
|
73
|
+
handleTreeSelection(
|
|
67
74
|
e,
|
|
68
75
|
nodeId,
|
|
69
76
|
useViewer.getState().selection.selectedIds,
|
|
70
77
|
setSelection,
|
|
71
78
|
)
|
|
72
|
-
|
|
73
|
-
useEditor.getState().setPhase('furnish')
|
|
74
|
-
}
|
|
79
|
+
routeTreeSelectionToNode(node)
|
|
75
80
|
},
|
|
76
|
-
[nodeId, setSelection],
|
|
81
|
+
[node, nodeId, setSelection],
|
|
77
82
|
)
|
|
78
83
|
|
|
79
84
|
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
@@ -83,7 +88,8 @@ export const ItemTreeNode = memo(function ItemTreeNode({
|
|
|
83
88
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
84
89
|
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
85
90
|
|
|
86
|
-
const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.
|
|
91
|
+
const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.webp'
|
|
92
|
+
const snapTarget = resolveNodeSnapTarget(node)
|
|
87
93
|
const defaultName = asset?.name || 'Item'
|
|
88
94
|
const hasChildren = children.length > 0
|
|
89
95
|
|
|
@@ -93,7 +99,15 @@ export const ItemTreeNode = memo(function ItemTreeNode({
|
|
|
93
99
|
depth={depth}
|
|
94
100
|
expanded={expanded}
|
|
95
101
|
hasChildren={hasChildren}
|
|
96
|
-
icon={
|
|
102
|
+
icon={
|
|
103
|
+
snapTarget ? (
|
|
104
|
+
<SnapTargetIcon target={snapTarget}>
|
|
105
|
+
<Image alt="" className="object-contain" height={14} src={iconSrc} width={14} />
|
|
106
|
+
</SnapTargetIcon>
|
|
107
|
+
) : (
|
|
108
|
+
<Image alt="" className="object-contain" height={14} src={iconSrc} width={14} />
|
|
109
|
+
)
|
|
110
|
+
}
|
|
97
111
|
isHovered={isHovered}
|
|
98
112
|
isLast={isLast}
|
|
99
113
|
isSelected={isSelected}
|
|
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
3
3
|
import { Layers } from 'lucide-react'
|
|
4
4
|
import { memo, useCallback, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
|
+
import { getDefaultLevelName } from '@pascal-app/core'
|
|
6
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
8
|
import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
8
9
|
import { TreeNodeActions } from './tree-node-actions'
|
|
@@ -35,7 +36,7 @@ export const LevelTreeNode = memo(function LevelTreeNode({
|
|
|
35
36
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
36
37
|
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
37
38
|
|
|
38
|
-
const defaultName =
|
|
39
|
+
const defaultName = getDefaultLevelName(level)
|
|
39
40
|
|
|
40
41
|
return (
|
|
41
42
|
<TreeNodeWrapper
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Icon as IconifyIcon } from '@iconify/react'
|
|
2
|
+
import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import Image from 'next/image'
|
|
5
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
6
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
7
|
+
import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
|
|
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
|
+
import { resolveTreeChildIds, treeContainsDescendant } from './tree-structure'
|
|
18
|
+
|
|
19
|
+
interface RegistryTreeNodeProps {
|
|
20
|
+
nodeId: AnyNodeId
|
|
21
|
+
depth: number
|
|
22
|
+
isLast?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generic, registry-driven tree-node row powered by `def.presentation` and
|
|
27
|
+
* `def.tree`. Replaces the per-kind boilerplate
|
|
28
|
+
* components that differed only in their default name and icon — today the
|
|
29
|
+
* roof vents plus cabinet rows. Register a kind in `treeNodeByType` against
|
|
30
|
+
* this component instead of authoring another copy.
|
|
31
|
+
*/
|
|
32
|
+
export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
33
|
+
nodeId,
|
|
34
|
+
depth,
|
|
35
|
+
isLast,
|
|
36
|
+
}: RegistryTreeNodeProps) {
|
|
37
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
38
|
+
const [expanded, setExpanded] = useState(true)
|
|
39
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
40
|
+
const node = useScene((s) => s.nodes[nodeId])
|
|
41
|
+
const children = useScene(useShallow((s) => resolveTreeChildIds(nodeId, s.nodes)))
|
|
42
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
43
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
44
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
45
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
46
|
+
|
|
47
|
+
const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
|
|
48
|
+
const tree = node ? nodeRegistry.get(node.type)?.tree : undefined
|
|
49
|
+
const icon = presentation?.icon
|
|
50
|
+
const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.webp'
|
|
51
|
+
const iconElement =
|
|
52
|
+
icon?.kind === 'iconify' ? (
|
|
53
|
+
<IconifyIcon className="opacity-60" height={14} icon={icon.name} width={14} />
|
|
54
|
+
) : (
|
|
55
|
+
<Image
|
|
56
|
+
alt=""
|
|
57
|
+
className="object-contain opacity-60"
|
|
58
|
+
height={14}
|
|
59
|
+
src={iconSrc}
|
|
60
|
+
width={14}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
const snapTarget = resolveNodeSnapTarget(node)
|
|
64
|
+
const defaultName =
|
|
65
|
+
node ? tree?.label?.(node, useScene.getState().nodes) || node.name || presentation?.label || 'Node' : 'Node'
|
|
66
|
+
const hasChildren = children.length > 0
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
return useViewer.subscribe((state) => {
|
|
70
|
+
const { selectedIds } = state.selection
|
|
71
|
+
if (selectedIds.length === 0) return
|
|
72
|
+
const nodes = useScene.getState().nodes
|
|
73
|
+
for (const id of selectedIds) {
|
|
74
|
+
if (treeContainsDescendant(nodeId, id as AnyNodeId, nodes)) {
|
|
75
|
+
setExpanded(true)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}, [nodeId])
|
|
81
|
+
|
|
82
|
+
const handleClick = useCallback(
|
|
83
|
+
(e: React.MouseEvent) => {
|
|
84
|
+
e.stopPropagation()
|
|
85
|
+
handleTreeSelection(
|
|
86
|
+
e,
|
|
87
|
+
nodeId,
|
|
88
|
+
useViewer.getState().selection.selectedIds,
|
|
89
|
+
setSelection,
|
|
90
|
+
)
|
|
91
|
+
routeTreeSelectionToNode(node)
|
|
92
|
+
},
|
|
93
|
+
[node, nodeId, setSelection],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<TreeNodeWrapper
|
|
98
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
99
|
+
depth={depth}
|
|
100
|
+
expanded={expanded}
|
|
101
|
+
hasChildren={hasChildren}
|
|
102
|
+
icon={
|
|
103
|
+
snapTarget ? (
|
|
104
|
+
<SnapTargetIcon target={snapTarget}>{iconElement}</SnapTargetIcon>
|
|
105
|
+
) : (
|
|
106
|
+
iconElement
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
isHovered={isHovered}
|
|
110
|
+
isLast={isLast}
|
|
111
|
+
isSelected={isSelected}
|
|
112
|
+
isVisible={isVisible}
|
|
113
|
+
label={
|
|
114
|
+
<InlineRenameInput
|
|
115
|
+
defaultName={defaultName}
|
|
116
|
+
isEditing={isEditing}
|
|
117
|
+
nodeId={nodeId}
|
|
118
|
+
onStartEditing={() => setIsEditing(true)}
|
|
119
|
+
onStopEditing={() => setIsEditing(false)}
|
|
120
|
+
/>
|
|
121
|
+
}
|
|
122
|
+
nodeId={nodeId}
|
|
123
|
+
onClick={handleClick}
|
|
124
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
125
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
126
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
127
|
+
onToggle={() => setExpanded((prev) => !prev)}
|
|
128
|
+
>
|
|
129
|
+
{hasChildren &&
|
|
130
|
+
children.map((childId, index) => (
|
|
131
|
+
<TreeNode
|
|
132
|
+
depth={depth + 1}
|
|
133
|
+
isLast={index === children.length - 1}
|
|
134
|
+
key={childId}
|
|
135
|
+
nodeId={childId as AnyNodeId}
|
|
136
|
+
/>
|
|
137
|
+
))}
|
|
138
|
+
</TreeNodeWrapper>
|
|
139
|
+
)
|
|
140
|
+
})
|
|
@@ -6,7 +6,7 @@ import { memo, useCallback, useEffect, useState } from 'react'
|
|
|
6
6
|
import { useShallow } from 'zustand/react/shallow'
|
|
7
7
|
import useEditor from '../../../../../store/use-editor'
|
|
8
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
9
|
-
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
9
|
+
import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
10
10
|
import { TreeNodeActions } from './tree-node-actions'
|
|
11
11
|
import { DropIndicatorLine, useTreeNodeDrag } from './tree-node-drag'
|
|
12
12
|
|
|
@@ -28,6 +28,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
|
|
|
28
28
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
29
29
|
const setSelection = useViewer((state) => state.setSelection)
|
|
30
30
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
31
|
+
const setRoofHostDragArmedId = useEditor((state) => state.setRoofHostDragArmedId)
|
|
31
32
|
const { drag, dropTarget } = useTreeNodeDrag()
|
|
32
33
|
|
|
33
34
|
const segments = useScene(
|
|
@@ -57,8 +58,11 @@ export const RoofTreeNode = memo(function RoofTreeNode({
|
|
|
57
58
|
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
58
59
|
useEditor.getState().setPhase('structure')
|
|
59
60
|
}
|
|
61
|
+
if (!e.metaKey && !e.ctrlKey && !e.shiftKey) {
|
|
62
|
+
setRoofHostDragArmedId(nodeId)
|
|
63
|
+
}
|
|
60
64
|
},
|
|
61
|
-
[nodeId, setSelection],
|
|
65
|
+
[nodeId, setRoofHostDragArmedId, setSelection],
|
|
62
66
|
)
|
|
63
67
|
|
|
64
68
|
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
@@ -98,7 +102,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
|
|
|
98
102
|
expanded={expanded}
|
|
99
103
|
hasChildren={segments.length > 0}
|
|
100
104
|
icon={
|
|
101
|
-
<Image alt="" className="object-contain" height={14} src="/icons/roof.
|
|
105
|
+
<Image alt="" className="object-contain" height={14} src="/icons/roof.webp" width={14} />
|
|
102
106
|
}
|
|
103
107
|
isDropTarget={isValidDropTarget && isDropTarget}
|
|
104
108
|
isHovered={isHovered || isDropTarget}
|
|
@@ -163,12 +167,36 @@ function RoofSegmentTreeNode({
|
|
|
163
167
|
isLast?: boolean
|
|
164
168
|
}) {
|
|
165
169
|
const [isEditing, setIsEditing] = useState(false)
|
|
170
|
+
const [expanded, setExpanded] = useState(true)
|
|
171
|
+
const accessoryIds = useScene(
|
|
172
|
+
useShallow(
|
|
173
|
+
(s) => (s.nodes[node.id] as RoofSegmentNode | undefined)?.children ?? ([] as string[]),
|
|
174
|
+
),
|
|
175
|
+
)
|
|
166
176
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id))
|
|
167
177
|
const isHovered = useViewer((state) => state.hoveredId === node.id)
|
|
168
178
|
const setSelection = useViewer((state) => state.setSelection)
|
|
169
179
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
170
180
|
const { startDrag, isDragging } = useTreeNodeDrag()
|
|
171
181
|
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
return useViewer.subscribe((state) => {
|
|
184
|
+
const { selectedIds } = state.selection
|
|
185
|
+
if (selectedIds.length === 0) return
|
|
186
|
+
const nodes = useScene.getState().nodes
|
|
187
|
+
for (const id of selectedIds) {
|
|
188
|
+
let current = nodes[id as AnyNodeId]
|
|
189
|
+
while (current?.parentId) {
|
|
190
|
+
if (current.parentId === node.id) {
|
|
191
|
+
setExpanded(true)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
current = nodes[current.parentId as AnyNodeId]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}, [node.id])
|
|
199
|
+
|
|
172
200
|
const handleClick = useCallback(
|
|
173
201
|
(e: React.MouseEvent) => {
|
|
174
202
|
if (isDragging) return
|
|
@@ -192,19 +220,21 @@ function RoofSegmentTreeNode({
|
|
|
192
220
|
|
|
193
221
|
const defaultName = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}x${node.depth.toFixed(1)}m)`
|
|
194
222
|
|
|
223
|
+
const hasAccessories = accessoryIds.length > 0
|
|
224
|
+
|
|
195
225
|
return (
|
|
196
226
|
<div data-drop-child={node.id}>
|
|
197
227
|
<TreeNodeWrapper
|
|
198
228
|
actions={<TreeNodeActions nodeId={node.id} />}
|
|
199
229
|
depth={depth}
|
|
200
|
-
expanded={
|
|
201
|
-
hasChildren={
|
|
230
|
+
expanded={expanded}
|
|
231
|
+
hasChildren={hasAccessories}
|
|
202
232
|
icon={
|
|
203
233
|
<Image
|
|
204
234
|
alt=""
|
|
205
235
|
className="object-contain opacity-60"
|
|
206
236
|
height={14}
|
|
207
|
-
src="/icons/roof.
|
|
237
|
+
src="/icons/roof.webp"
|
|
208
238
|
width={14}
|
|
209
239
|
/>
|
|
210
240
|
}
|
|
@@ -228,8 +258,18 @@ function RoofSegmentTreeNode({
|
|
|
228
258
|
onMouseEnter={() => setHoveredId(node.id)}
|
|
229
259
|
onMouseLeave={() => setHoveredId(null)}
|
|
230
260
|
onPointerDown={handlePointerDown}
|
|
231
|
-
onToggle={() =>
|
|
232
|
-
|
|
261
|
+
onToggle={() => setExpanded((prev) => !prev)}
|
|
262
|
+
>
|
|
263
|
+
{hasAccessories &&
|
|
264
|
+
accessoryIds.map((childId, index) => (
|
|
265
|
+
<TreeNode
|
|
266
|
+
depth={depth + 1}
|
|
267
|
+
isLast={index === accessoryIds.length - 1}
|
|
268
|
+
key={childId}
|
|
269
|
+
nodeId={childId as AnyNodeId}
|
|
270
|
+
/>
|
|
271
|
+
))}
|
|
272
|
+
</TreeNodeWrapper>
|
|
233
273
|
</div>
|
|
234
274
|
)
|
|
235
275
|
}
|