@pascal-app/editor 0.9.1 → 1.0.0-beta.1
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 +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -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 +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- 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-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- 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 +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- 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 +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- 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/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- 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 +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- 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 +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- 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/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- 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 +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- 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 +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -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 +44 -3
- 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/tab-bar.tsx +42 -28
- 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/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- 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 +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- 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 +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- 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 +188 -0
- package/src/lib/measurements.ts +210 -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/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- 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 +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -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 +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -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 +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- 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 +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import { Icon as IconifyIcon } from '@iconify/react'
|
|
1
2
|
import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
|
|
2
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
4
|
import Image from 'next/image'
|
|
4
|
-
import { memo, useCallback, useState } from 'react'
|
|
5
|
-
import
|
|
5
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
6
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
7
|
+
import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
|
|
6
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
focusTreeNode,
|
|
11
|
+
handleTreeSelection,
|
|
12
|
+
routeTreeSelectionToNode,
|
|
13
|
+
TreeNode,
|
|
14
|
+
TreeNodeWrapper,
|
|
15
|
+
} from './tree-node'
|
|
8
16
|
import { TreeNodeActions } from './tree-node-actions'
|
|
17
|
+
import { resolveTreeChildIds, treeContainsDescendant } from './tree-structure'
|
|
9
18
|
|
|
10
19
|
interface RegistryTreeNodeProps {
|
|
11
20
|
nodeId: AnyNodeId
|
|
@@ -14,11 +23,11 @@ interface RegistryTreeNodeProps {
|
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
|
-
* Generic,
|
|
18
|
-
* `def.
|
|
26
|
+
* Generic, registry-driven tree-node row powered by `def.presentation` and
|
|
27
|
+
* `def.tree`. Replaces the per-kind boilerplate
|
|
19
28
|
* components that differed only in their default name and icon — today the
|
|
20
|
-
* roof vents
|
|
21
|
-
*
|
|
29
|
+
* roof vents plus cabinet rows. Register a kind in `treeNodeByType` against
|
|
30
|
+
* this component instead of authoring another copy.
|
|
22
31
|
*/
|
|
23
32
|
export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
24
33
|
nodeId,
|
|
@@ -26,48 +35,76 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
|
26
35
|
isLast,
|
|
27
36
|
}: RegistryTreeNodeProps) {
|
|
28
37
|
const [isEditing, setIsEditing] = useState(false)
|
|
38
|
+
const [expanded, setExpanded] = useState(true)
|
|
29
39
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
30
40
|
const node = useScene((s) => s.nodes[nodeId])
|
|
41
|
+
const children = useScene(useShallow((s) => resolveTreeChildIds(nodeId, s.nodes)))
|
|
31
42
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
32
43
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
33
44
|
const setSelection = useViewer((state) => state.setSelection)
|
|
34
45
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
35
46
|
|
|
36
47
|
const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
|
|
48
|
+
const tree = node ? nodeRegistry.get(node.type)?.tree : undefined
|
|
37
49
|
const icon = presentation?.icon
|
|
38
|
-
const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.
|
|
39
|
-
const
|
|
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])
|
|
40
81
|
|
|
41
82
|
const handleClick = useCallback(
|
|
42
83
|
(e: React.MouseEvent) => {
|
|
43
84
|
e.stopPropagation()
|
|
44
|
-
|
|
85
|
+
handleTreeSelection(
|
|
45
86
|
e,
|
|
46
87
|
nodeId,
|
|
47
88
|
useViewer.getState().selection.selectedIds,
|
|
48
89
|
setSelection,
|
|
49
90
|
)
|
|
50
|
-
|
|
51
|
-
useEditor.getState().setPhase('structure')
|
|
52
|
-
}
|
|
91
|
+
routeTreeSelectionToNode(node)
|
|
53
92
|
},
|
|
54
|
-
[nodeId, setSelection],
|
|
93
|
+
[node, nodeId, setSelection],
|
|
55
94
|
)
|
|
56
95
|
|
|
57
96
|
return (
|
|
58
97
|
<TreeNodeWrapper
|
|
59
98
|
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
60
99
|
depth={depth}
|
|
61
|
-
expanded={
|
|
62
|
-
hasChildren={
|
|
100
|
+
expanded={expanded}
|
|
101
|
+
hasChildren={hasChildren}
|
|
63
102
|
icon={
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
width={14}
|
|
70
|
-
/>
|
|
103
|
+
snapTarget ? (
|
|
104
|
+
<SnapTargetIcon target={snapTarget}>{iconElement}</SnapTargetIcon>
|
|
105
|
+
) : (
|
|
106
|
+
iconElement
|
|
107
|
+
)
|
|
71
108
|
}
|
|
72
109
|
isHovered={isHovered}
|
|
73
110
|
isLast={isLast}
|
|
@@ -87,7 +124,17 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
|
87
124
|
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
88
125
|
onMouseEnter={() => setHoveredId(nodeId)}
|
|
89
126
|
onMouseLeave={() => setHoveredId(null)}
|
|
90
|
-
onToggle={() =>
|
|
91
|
-
|
|
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>
|
|
92
139
|
)
|
|
93
140
|
})
|
|
@@ -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}
|
|
@@ -230,7 +234,7 @@ function RoofSegmentTreeNode({
|
|
|
230
234
|
alt=""
|
|
231
235
|
className="object-contain opacity-60"
|
|
232
236
|
height={14}
|
|
233
|
-
src="/icons/roof.
|
|
237
|
+
src="/icons/roof.webp"
|
|
234
238
|
width={14}
|
|
235
239
|
/>
|
|
236
240
|
}
|
|
@@ -5,9 +5,14 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
5
5
|
import Image from 'next/image'
|
|
6
6
|
import { memo, useCallback, useEffect, useState } from 'react'
|
|
7
7
|
import { useShallow } from 'zustand/react/shallow'
|
|
8
|
-
import useEditor from './../../../../../store/use-editor'
|
|
9
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
focusTreeNode,
|
|
11
|
+
handleTreeSelection,
|
|
12
|
+
routeTreeSelectionToNode,
|
|
13
|
+
TreeNode,
|
|
14
|
+
TreeNodeWrapper,
|
|
15
|
+
} from './tree-node'
|
|
11
16
|
import { TreeNodeActions } from './tree-node-actions'
|
|
12
17
|
|
|
13
18
|
interface ShelfTreeNodeProps {
|
|
@@ -34,6 +39,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
|
34
39
|
const children = useScene(
|
|
35
40
|
useShallow((s) => (s.nodes[nodeId] as ShelfNode | undefined)?.children ?? []),
|
|
36
41
|
)
|
|
42
|
+
const node = useScene((s) => s.nodes[nodeId] as ShelfNode | undefined)
|
|
37
43
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
38
44
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
39
45
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -63,17 +69,15 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
|
63
69
|
const handleClick = useCallback(
|
|
64
70
|
(e: React.MouseEvent) => {
|
|
65
71
|
e.stopPropagation()
|
|
66
|
-
|
|
72
|
+
handleTreeSelection(
|
|
67
73
|
e,
|
|
68
74
|
nodeId,
|
|
69
75
|
useViewer.getState().selection.selectedIds,
|
|
70
76
|
setSelection,
|
|
71
77
|
)
|
|
72
|
-
|
|
73
|
-
useEditor.getState().setPhase('structure')
|
|
74
|
-
}
|
|
78
|
+
routeTreeSelectionToNode(node)
|
|
75
79
|
},
|
|
76
|
-
[nodeId, setSelection],
|
|
80
|
+
[node, nodeId, setSelection],
|
|
77
81
|
)
|
|
78
82
|
|
|
79
83
|
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
@@ -92,7 +96,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
|
92
96
|
expanded={expanded}
|
|
93
97
|
hasChildren={hasChildren}
|
|
94
98
|
icon={
|
|
95
|
-
<Image alt="" className="object-contain" height={14} src="/icons/shelf.
|
|
99
|
+
<Image alt="" className="object-contain" height={14} src="/icons/shelf.webp" width={14} />
|
|
96
100
|
}
|
|
97
101
|
isHovered={isHovered}
|
|
98
102
|
isLast={isLast}
|
|
@@ -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}
|
|
@@ -2,6 +2,7 @@ import { type AnyNodeId, type SolarPanelNode, 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 { SnapTargetIcon } from '../../../snap-target-badge'
|
|
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'
|
|
@@ -51,13 +52,15 @@ export const SolarPanelTreeNode = memo(function SolarPanelTreeNode({
|
|
|
51
52
|
expanded={false}
|
|
52
53
|
hasChildren={false}
|
|
53
54
|
icon={
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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>
|
|
61
64
|
}
|
|
62
65
|
isHovered={isHovered}
|
|
63
66
|
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,6 +56,31 @@ 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'
|
|
@@ -98,10 +126,13 @@ const treeNodeByType: Record<
|
|
|
98
126
|
isLast?: boolean
|
|
99
127
|
nodeId: AnyNodeId
|
|
100
128
|
}>,
|
|
129
|
+
cabinet: RegistryTreeNode,
|
|
130
|
+
'cabinet-module': RegistryTreeNode,
|
|
101
131
|
'box-vent': RegistryTreeNode,
|
|
102
132
|
ceiling: CeilingTreeNode,
|
|
103
133
|
chimney: ChimneyTreeNode,
|
|
104
134
|
dormer: DormerTreeNode,
|
|
135
|
+
downspout: RegistryTreeNode,
|
|
105
136
|
'solar-panel': SolarPanelTreeNode,
|
|
106
137
|
column: ColumnTreeNode,
|
|
107
138
|
elevator: ElevatorTreeNode,
|
|
@@ -124,10 +155,12 @@ const treeNodeByType: Record<
|
|
|
124
155
|
wall: WallTreeNode,
|
|
125
156
|
fence: FenceTreeNode,
|
|
126
157
|
gutter: GutterTreeNode,
|
|
158
|
+
measurement: RegistryTreeNode,
|
|
127
159
|
'ridge-vent': RegistryTreeNode,
|
|
128
160
|
'turbine-vent': RegistryTreeNode,
|
|
129
161
|
cupola: RegistryTreeNode,
|
|
130
162
|
'eyebrow-vent': RegistryTreeNode,
|
|
163
|
+
skylight: RegistryTreeNode,
|
|
131
164
|
roof: RoofTreeNode,
|
|
132
165
|
stair: StairTreeNode,
|
|
133
166
|
door: DoorTreeNode,
|
|
@@ -141,7 +174,15 @@ const treeNodeByType: Record<
|
|
|
141
174
|
}
|
|
142
175
|
|
|
143
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
|
+
})
|
|
144
184
|
const nodeType = useScene((state) => state.nodes[nodeId]?.type)
|
|
185
|
+
if (shouldHide) return null
|
|
145
186
|
if (!nodeType) return null
|
|
146
187
|
const Component = treeNodeByType[nodeType]
|
|
147
188
|
if (!Component) return null
|
|
@@ -266,10 +307,10 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
|
|
266
307
|
</motion.div>
|
|
267
308
|
) : null}
|
|
268
309
|
</button>
|
|
269
|
-
<div className="flex min-w-0 flex-1 items-center gap-
|
|
310
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
270
311
|
<span
|
|
271
312
|
className={cn(
|
|
272
|
-
'flex h-
|
|
313
|
+
'flex h-5 w-5 shrink-0 items-center justify-center transition-all duration-200',
|
|
273
314
|
!isSelected && 'opacity-60 grayscale',
|
|
274
315
|
)}
|
|
275
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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
+
import { editorHostPanelRegistry } from '../../../lib/plugin-panels'
|
|
4
5
|
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
5
6
|
import { cn } from './../../../lib/utils'
|
|
6
7
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip'
|
|
@@ -65,37 +66,50 @@ interface IconRailProps {
|
|
|
65
66
|
* The label renders as a hover tooltip on the right.
|
|
66
67
|
*/
|
|
67
68
|
export function IconRail({ tabs, activeTab, collapsed, onIconClick }: IconRailProps) {
|
|
69
|
+
const pluginPanelIds = new Set(
|
|
70
|
+
editorHostPanelRegistry.getSnapshot().flatMap((panel) =>
|
|
71
|
+
panel.pluginId ? [panel.id] : [],
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
const defaultTabs = tabs.filter((tab) => !pluginPanelIds.has(tab.id) && tab.id !== 'plugins')
|
|
75
|
+
const pluginTabs = tabs.filter((tab) => pluginPanelIds.has(tab.id) || tab.id === 'plugins')
|
|
76
|
+
|
|
77
|
+
const renderTab = (tab: SidebarTab) => {
|
|
78
|
+
const showActive = activeTab === tab.id && !collapsed
|
|
79
|
+
return (
|
|
80
|
+
<Tooltip key={tab.id}>
|
|
81
|
+
<TooltipTrigger asChild>
|
|
82
|
+
<button
|
|
83
|
+
className={cn(
|
|
84
|
+
'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
|
|
85
|
+
showActive
|
|
86
|
+
? 'bg-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
|
|
87
|
+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
|
|
88
|
+
)}
|
|
89
|
+
onClick={() => {
|
|
90
|
+
triggerSFX('sfx:menu-click')
|
|
91
|
+
onIconClick(tab.id)
|
|
92
|
+
}}
|
|
93
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
94
|
+
type="button"
|
|
95
|
+
>
|
|
96
|
+
{tab.icon ?? tab.label.charAt(0)}
|
|
97
|
+
</button>
|
|
98
|
+
</TooltipTrigger>
|
|
99
|
+
<TooltipContent side="right">{tab.label}</TooltipContent>
|
|
100
|
+
</Tooltip>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
68
104
|
return (
|
|
69
105
|
<TooltipProvider delayDuration={0} disableHoverableContent>
|
|
70
106
|
<div className="flex h-full w-14 shrink-0 flex-col items-center gap-1 border-border/50 border-r py-2">
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<TooltipTrigger asChild>
|
|
78
|
-
<button
|
|
79
|
-
className={cn(
|
|
80
|
-
'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
|
|
81
|
-
showActive
|
|
82
|
-
? 'bg-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
|
|
83
|
-
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
|
|
84
|
-
)}
|
|
85
|
-
onClick={() => {
|
|
86
|
-
triggerSFX('sfx:menu-click')
|
|
87
|
-
onIconClick(tab.id)
|
|
88
|
-
}}
|
|
89
|
-
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
90
|
-
type="button"
|
|
91
|
-
>
|
|
92
|
-
{tab.icon ?? tab.label.charAt(0)}
|
|
93
|
-
</button>
|
|
94
|
-
</TooltipTrigger>
|
|
95
|
-
<TooltipContent side="right">{tab.label}</TooltipContent>
|
|
96
|
-
</Tooltip>
|
|
97
|
-
)
|
|
98
|
-
})}
|
|
107
|
+
{defaultTabs.map(renderTab)}
|
|
108
|
+
{pluginTabs.length > 0 && (
|
|
109
|
+
<div className="mt-1 flex w-11 flex-col items-center gap-1 border-border/70 border-t pt-2">
|
|
110
|
+
{pluginTabs.map(renderTab)}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
99
113
|
</div>
|
|
100
114
|
</TooltipProvider>
|
|
101
115
|
)
|