@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,18 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { AssetInput } from '@pascal-app/core'
|
|
4
|
-
import { resolveCdnUrl } from '@pascal-app/viewer'
|
|
5
|
-
import Image from 'next/image'
|
|
4
|
+
import { resolveCdnUrl, useViewer } from '@pascal-app/viewer'
|
|
6
5
|
import { useEffect } from 'react'
|
|
7
|
-
import {
|
|
8
|
-
Tooltip,
|
|
9
|
-
TooltipContent,
|
|
10
|
-
TooltipTrigger,
|
|
11
|
-
} from './../../../components/ui/primitives/tooltip'
|
|
12
6
|
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
13
7
|
import { cn } from './../../../lib/utils'
|
|
14
8
|
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
|
15
|
-
import {
|
|
9
|
+
import { resolveAssetSnapTarget, SnapTargetBadge } from '../snap-target-badge'
|
|
10
|
+
import { CATALOG_ITEMS, type CatalogItem } from './catalog-items'
|
|
16
11
|
|
|
17
12
|
export function ItemCatalog({
|
|
18
13
|
category,
|
|
@@ -41,9 +36,9 @@ export function ItemCatalog({
|
|
|
41
36
|
const setMode = useEditor((state) => state.setMode)
|
|
42
37
|
const setTool = useEditor((state) => state.setTool)
|
|
43
38
|
|
|
44
|
-
const sourceItems = itemsOverride ?? CATALOG_ITEMS
|
|
39
|
+
const sourceItems: CatalogItem[] = itemsOverride ?? CATALOG_ITEMS
|
|
45
40
|
// Server-provided results bypass all local filtering; otherwise filter by category/search/tags
|
|
46
|
-
const filteredItems =
|
|
41
|
+
const filteredItems: CatalogItem[] =
|
|
47
42
|
overrideItems ??
|
|
48
43
|
(() => {
|
|
49
44
|
const categoryItems = search
|
|
@@ -58,22 +53,6 @@ export function ItemCatalog({
|
|
|
58
53
|
})
|
|
59
54
|
})()
|
|
60
55
|
|
|
61
|
-
const categoryItems = filteredItems
|
|
62
|
-
|
|
63
|
-
// Auto-select first item if current selection is not in the filtered list
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
|
|
66
|
-
if (!isCurrentItemInCategory && categoryItems.length > 0) {
|
|
67
|
-
setSelectedItem(categoryItems[0] as AssetInput)
|
|
68
|
-
}
|
|
69
|
-
}, [categoryItems, selectedItem?.src, setSelectedItem])
|
|
70
|
-
|
|
71
|
-
const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
|
|
72
|
-
if (attachTo === 'wall' || attachTo === 'wall-side') return '/icons/wall.png'
|
|
73
|
-
if (attachTo === 'ceiling') return '/icons/ceiling.png'
|
|
74
|
-
return null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
56
|
if (filteredItems.length === 0 && emptyState) {
|
|
78
57
|
return <>{emptyState}</>
|
|
79
58
|
}
|
|
@@ -86,7 +65,7 @@ export function ItemCatalog({
|
|
|
86
65
|
{leadingTile}
|
|
87
66
|
{filteredItems.map((item, index) => {
|
|
88
67
|
const isSelected = selectedItem?.src === item?.src
|
|
89
|
-
const
|
|
68
|
+
const snapTarget = resolveAssetSnapTarget(item?.attachTo)
|
|
90
69
|
return (
|
|
91
70
|
<button
|
|
92
71
|
className={cn(
|
|
@@ -96,8 +75,12 @@ export function ItemCatalog({
|
|
|
96
75
|
key={index}
|
|
97
76
|
onClick={() => {
|
|
98
77
|
triggerSFX('sfx:menu-click')
|
|
78
|
+
// Drop the current selection before arming placement — keeping
|
|
79
|
+
// it would route shortcuts (rotate & co) to both the ghost and
|
|
80
|
+
// the selected node.
|
|
81
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
99
82
|
setSelectedItem(item)
|
|
100
|
-
setTool('item')
|
|
83
|
+
setTool(item.tool ?? 'item')
|
|
101
84
|
setMode('build')
|
|
102
85
|
}}
|
|
103
86
|
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
@@ -110,14 +93,8 @@ export function ItemCatalog({
|
|
|
110
93
|
loading="eager"
|
|
111
94
|
src={resolveCdnUrl(item.thumbnail) || ''}
|
|
112
95
|
/>
|
|
113
|
-
{
|
|
114
|
-
<
|
|
115
|
-
<img
|
|
116
|
-
alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
|
|
117
|
-
className="h-4 w-4"
|
|
118
|
-
src={attachmentIcon}
|
|
119
|
-
/>
|
|
120
|
-
</div>
|
|
96
|
+
{snapTarget && (
|
|
97
|
+
<SnapTargetBadge className="absolute right-1 bottom-1" target={snapTarget} />
|
|
121
98
|
)}
|
|
122
99
|
</div>
|
|
123
100
|
<span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { LevelNode } from '@pascal-app/core'
|
|
4
4
|
import { useEffect, useState } from 'react'
|
|
5
5
|
import type { LevelDuplicatePreset } from '../../lib/level-duplication'
|
|
6
|
-
import { getLevelDisplayName } from '
|
|
6
|
+
import { getLevelDisplayName } from '@pascal-app/core'
|
|
7
7
|
import { cn } from '../../lib/utils'
|
|
8
8
|
import {
|
|
9
9
|
Dialog,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { Copy, Trash2 } from 'lucide-react'
|
|
6
|
+
import { deleteSelection, duplicateSelectionAndPickUp } from '../../editor/group-actions'
|
|
7
|
+
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
8
|
+
import { PanelWrapper } from './panel-wrapper'
|
|
9
|
+
import { formatSelectionBreakdown } from './selection-breakdown'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Docked right-side panel for a MULTI-selection — the compact sibling of the
|
|
13
|
+
* single-node inspector, rendered by `PanelManager` when more than one node
|
|
14
|
+
* is selected. Same collapsed-by-default `PanelWrapper` shell (header always
|
|
15
|
+
* visible; the shared desktop collapse state carries across single ↔ multi
|
|
16
|
+
* swaps). Actions mirror the floating group pill: Duplicate clones the
|
|
17
|
+
* selection and picks the clones up, Delete removes the whole selection
|
|
18
|
+
* (including its bulk-delete confirm). Unlike the pill, the docked panel
|
|
19
|
+
* stays visible during interactions. `footer` is the host-injected slot
|
|
20
|
+
* (e.g. community's "Save to my catalog").
|
|
21
|
+
*/
|
|
22
|
+
export function MultiSelectionPanel({ footer }: { footer?: React.ReactNode }) {
|
|
23
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
24
|
+
const setSelection = useViewer((s) => s.setSelection)
|
|
25
|
+
// String selector — recomputed on scene ticks, but the === compare keeps
|
|
26
|
+
// unrelated mutations from re-rendering the panel.
|
|
27
|
+
const breakdown = useScene((s) =>
|
|
28
|
+
formatSelectionBreakdown(selectedIds.map((id) => s.nodes[id as AnyNodeId]?.type)),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<PanelWrapper
|
|
33
|
+
footer={footer}
|
|
34
|
+
icon="/icons/select.webp"
|
|
35
|
+
onClose={() => setSelection({ selectedIds: [] })}
|
|
36
|
+
title={`${selectedIds.length} selected`}
|
|
37
|
+
width={320}
|
|
38
|
+
>
|
|
39
|
+
{breakdown && <div className="px-3 py-3 text-muted-foreground text-xs">{breakdown}</div>}
|
|
40
|
+
<div className="border-border/50 border-t p-3">
|
|
41
|
+
<ActionGroup>
|
|
42
|
+
<ActionButton
|
|
43
|
+
icon={<Copy className="h-4 w-4" />}
|
|
44
|
+
label="Duplicate"
|
|
45
|
+
onClick={() => duplicateSelectionAndPickUp()}
|
|
46
|
+
/>
|
|
47
|
+
<ActionButton
|
|
48
|
+
className="border-red-500/40 text-red-200 hover:bg-red-500/15"
|
|
49
|
+
icon={<Trash2 className="h-4 w-4" />}
|
|
50
|
+
label="Delete"
|
|
51
|
+
onClick={() => deleteSelection()}
|
|
52
|
+
/>
|
|
53
|
+
</ActionGroup>
|
|
54
|
+
</div>
|
|
55
|
+
</PanelWrapper>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -6,26 +6,26 @@ export type NodeDisplay = {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
|
|
9
|
-
item: { icon: '/icons/
|
|
10
|
-
wall: { icon: '/icons/wall.
|
|
11
|
-
door: { icon: '/icons/door.
|
|
12
|
-
window: { icon: '/icons/window.
|
|
13
|
-
slab: { icon: '/icons/floor.
|
|
14
|
-
ceiling: { icon: '/icons/ceiling.
|
|
15
|
-
column: { icon: '/icons/column.
|
|
16
|
-
elevator: { icon: '/icons/elevator.
|
|
17
|
-
fence: { icon: '/icons/fence.
|
|
18
|
-
roof: { icon: '/icons/roof.
|
|
19
|
-
'roof-segment': { icon: '/icons/roof.
|
|
20
|
-
stair: { icon: '/icons/
|
|
21
|
-
'stair-segment': { icon: '/icons/
|
|
22
|
-
scan: { icon: '/icons/mesh.
|
|
23
|
-
guide: { icon: '/icons/floorplan.
|
|
9
|
+
item: { icon: '/icons/item.webp', label: 'Item' },
|
|
10
|
+
wall: { icon: '/icons/wall.webp', label: 'Wall' },
|
|
11
|
+
door: { icon: '/icons/door.webp', label: 'Door' },
|
|
12
|
+
window: { icon: '/icons/window.webp', label: 'Window' },
|
|
13
|
+
slab: { icon: '/icons/floor.webp', label: 'Slab' },
|
|
14
|
+
ceiling: { icon: '/icons/ceiling.webp', label: 'Ceiling' },
|
|
15
|
+
column: { icon: '/icons/column.webp', label: 'Column' },
|
|
16
|
+
elevator: { icon: '/icons/elevator.webp', label: 'Elevator' },
|
|
17
|
+
fence: { icon: '/icons/fence.webp', label: 'Fence' },
|
|
18
|
+
roof: { icon: '/icons/roof.webp', label: 'Roof' },
|
|
19
|
+
'roof-segment': { icon: '/icons/roof.webp', label: 'Roof segment' },
|
|
20
|
+
stair: { icon: '/icons/stairs.webp', label: 'Stair' },
|
|
21
|
+
'stair-segment': { icon: '/icons/stairs.webp', label: 'Stair segment' },
|
|
22
|
+
scan: { icon: '/icons/mesh.webp', label: '3D Scan' },
|
|
23
|
+
guide: { icon: '/icons/floorplan.webp', label: 'Guide image' },
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
|
|
27
|
-
if (!node) return { icon: '/icons/select.
|
|
28
|
-
const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.
|
|
27
|
+
if (!node) return { icon: '/icons/select.webp', label: 'Selection' }
|
|
28
|
+
const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.webp', label: node.type }
|
|
29
29
|
// Item nodes carry an asset with its own thumbnail/name
|
|
30
30
|
if (node.type === 'item') {
|
|
31
31
|
return {
|
|
@@ -28,8 +28,9 @@ import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
|
28
28
|
import useEditor from '../../../store/use-editor'
|
|
29
29
|
import { MobilePanelSheet } from './mobile-panel-sheet'
|
|
30
30
|
import { MobileSelectionBar } from './mobile-selection-bar'
|
|
31
|
+
import { MultiSelectionPanel } from './multi-selection-panel'
|
|
31
32
|
import { getNodeDisplay } from './node-display'
|
|
32
|
-
import {
|
|
33
|
+
import { resetDesktopInspectorCollapsed } from './panel-wrapper'
|
|
33
34
|
import { ParametricInspector } from './parametric-inspector'
|
|
34
35
|
import { ReferencePanel } from './reference-panel'
|
|
35
36
|
|
|
@@ -168,15 +169,18 @@ function MobilePanelLayer({
|
|
|
168
169
|
)
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
export function PanelManager({
|
|
172
|
+
export function PanelManager({
|
|
173
|
+
inspectorFooter,
|
|
174
|
+
multiSelectionFooter,
|
|
175
|
+
}: {
|
|
176
|
+
inspectorFooter?: React.ReactNode
|
|
177
|
+
multiSelectionFooter?: React.ReactNode
|
|
178
|
+
}) {
|
|
172
179
|
const isMobile = useIsMobile()
|
|
173
180
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
174
181
|
const selectedZoneId = useViewer((s) => s.selection.zoneId)
|
|
175
182
|
const setSelection = useViewer((s) => s.setSelection)
|
|
176
183
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
|
177
|
-
const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen)
|
|
178
|
-
const mode = useEditor((s) => s.mode)
|
|
179
|
-
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
|
180
184
|
// Only subscribe to the *type* of the single-selected node — string primitive
|
|
181
185
|
// so we don't re-render on unrelated scene mutations.
|
|
182
186
|
const selectedNodeType = useScene((s) => {
|
|
@@ -190,6 +194,27 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
|
|
|
190
194
|
return id ? (s.nodes[id as AnyNodeId] ?? null) : null
|
|
191
195
|
})
|
|
192
196
|
|
|
197
|
+
// Node and reference selection are mutually exclusive: selecting a guide
|
|
198
|
+
// clears the node selection (handleGuideSelect), but node selection never
|
|
199
|
+
// cleared a lingering reference — so clicking a wall with a floorplan
|
|
200
|
+
// selected kept showing the reference panel. Clear the stale reference the
|
|
201
|
+
// moment a scene selection appears.
|
|
202
|
+
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (selectedIds.length > 0 || selectedZoneId) {
|
|
205
|
+
setSelectedReferenceId(null)
|
|
206
|
+
}
|
|
207
|
+
}, [selectedIds, selectedZoneId, setSelectedReferenceId])
|
|
208
|
+
|
|
209
|
+
// The inspector's expanded state is shared across panel swaps, but a fresh
|
|
210
|
+
// selection after everything was deselected should open collapsed again.
|
|
211
|
+
const hasAnySelection = selectedIds.length > 0 || Boolean(selectedZoneId) || Boolean(selectedReferenceId)
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (!hasAnySelection) {
|
|
214
|
+
resetDesktopInspectorCollapsed()
|
|
215
|
+
}
|
|
216
|
+
}, [hasAnySelection])
|
|
217
|
+
|
|
193
218
|
if (isMobile) {
|
|
194
219
|
if (selectedReferenceId) {
|
|
195
220
|
return <MobilePanelLayer isReference={true} node={null} panel={<ReferencePanel />} />
|
|
@@ -208,15 +233,6 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
|
|
|
208
233
|
return <ReferencePanel />
|
|
209
234
|
}
|
|
210
235
|
|
|
211
|
-
if (
|
|
212
|
-
isPaintPanelOpen &&
|
|
213
|
-
mode === 'material-paint' &&
|
|
214
|
-
activePaintMaterial?.material?.properties &&
|
|
215
|
-
!activePaintMaterial.materialPreset
|
|
216
|
-
) {
|
|
217
|
-
return <PaintPanel />
|
|
218
|
-
}
|
|
219
|
-
|
|
220
236
|
if (selectedZoneId && selectedIds.length === 0) {
|
|
221
237
|
return (
|
|
222
238
|
<ParametricInspector
|
|
@@ -228,5 +244,11 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
|
|
|
228
244
|
)
|
|
229
245
|
}
|
|
230
246
|
|
|
247
|
+
// Multi-selection: compact docked panel (desktop only — the mobile branch
|
|
248
|
+
// above keeps today's behavior and renders nothing for multi-selections).
|
|
249
|
+
if (selectedIds.length > 1) {
|
|
250
|
+
return <MultiSelectionPanel footer={multiSelectionFooter} />
|
|
251
|
+
}
|
|
252
|
+
|
|
231
253
|
return panelForType(selectedNodeType, inspectorFooter)
|
|
232
254
|
}
|
|
@@ -17,6 +17,14 @@ const DRAG_MARGIN = 8
|
|
|
17
17
|
// Pointer travel (px) below which a header press is treated as a click
|
|
18
18
|
// (toggles collapse) rather than a drag.
|
|
19
19
|
const CLICK_SLOP = 4
|
|
20
|
+
let desktopInspectorCollapsed = true
|
|
21
|
+
|
|
22
|
+
/** Forget the shared expanded state. Called when the last selection clears so
|
|
23
|
+
* a fresh selection opens the inspector collapsed — the sharing is only meant
|
|
24
|
+
* to survive swaps between panels (roof ↔ segment), not a close/reopen. */
|
|
25
|
+
export function resetDesktopInspectorCollapsed() {
|
|
26
|
+
desktopInspectorCollapsed = true
|
|
27
|
+
}
|
|
20
28
|
|
|
21
29
|
function clamp(value: number, min: number, max: number): number {
|
|
22
30
|
return Math.min(Math.max(value, min), Math.max(min, max))
|
|
@@ -52,7 +60,7 @@ export const InspectorFooterContext = createContext<React.ReactNode>(null)
|
|
|
52
60
|
|
|
53
61
|
interface PanelWrapperProps {
|
|
54
62
|
title: string
|
|
55
|
-
/** Either a URL path (legacy panels pass `/icons/floor.
|
|
63
|
+
/** Either a URL path (legacy panels pass `/icons/floor.webp` etc.,
|
|
56
64
|
* rendered via next/image) OR a React node (registry-driven
|
|
57
65
|
* inspector renders `<Icon icon="lucide:fence" />` from
|
|
58
66
|
* `def.presentation.icon`). */
|
|
@@ -85,8 +93,21 @@ export function PanelWrapper({
|
|
|
85
93
|
const panelRef = useRef<HTMLDivElement>(null)
|
|
86
94
|
|
|
87
95
|
// The whole panel is collapsed to just its header by default; the chevron
|
|
88
|
-
// expands it to reveal the inspector body.
|
|
89
|
-
|
|
96
|
+
// expands it to reveal the inspector body. Keep the desktop value shared
|
|
97
|
+
// across inspector swaps (roof ↔ segment, etc.) so navigating between
|
|
98
|
+
// related panels preserves whether the user left the inspector open.
|
|
99
|
+
const [collapsed, setCollapsedState] = useState(desktopInspectorCollapsed)
|
|
100
|
+
|
|
101
|
+
const setCollapsed = useCallback(
|
|
102
|
+
(next: boolean | ((previous: boolean) => boolean)) => {
|
|
103
|
+
setCollapsedState((previous) => {
|
|
104
|
+
const resolved = typeof next === 'function' ? next(previous) : next
|
|
105
|
+
desktopInspectorCollapsed = resolved
|
|
106
|
+
return resolved
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
[],
|
|
110
|
+
)
|
|
90
111
|
|
|
91
112
|
// Drag-to-reposition from the header. `offset` is a translation applied on
|
|
92
113
|
// top of the default `top-20 right-4` anchor; null until first dragged.
|
|
@@ -14,6 +14,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
14
14
|
import { Icon } from '@iconify/react'
|
|
15
15
|
import { Move, Trash2 } from 'lucide-react'
|
|
16
16
|
import { type ComponentType, lazy, Suspense, useCallback } from 'react'
|
|
17
|
+
import { resolveMoveActionNode } from '../../../lib/direct-manipulation'
|
|
17
18
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
18
19
|
import { collectZoneContentIds } from '../../../lib/zone-content'
|
|
19
20
|
import useEditor from '../../../store/use-editor'
|
|
@@ -62,9 +63,22 @@ export function ParametricInspector({
|
|
|
62
63
|
const handleUpdate = useCallback(
|
|
63
64
|
(patch: Partial<AnyNode>) => {
|
|
64
65
|
if (!selectedId) return
|
|
65
|
-
useScene.getState()
|
|
66
|
+
const scene = useScene.getState()
|
|
67
|
+
const node = scene.nodes[selectedId]
|
|
68
|
+
if (parametrics?.derive && node) {
|
|
69
|
+
const next = { ...node, ...patch } as AnyNode
|
|
70
|
+
patch = { ...patch, ...parametrics.derive(next, patch) }
|
|
71
|
+
}
|
|
72
|
+
// Bundle the edited node + any reconcile follow-ups into ONE
|
|
73
|
+
// updateNodes call so a single inspector edit is a single undo step.
|
|
74
|
+
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = [{ id: selectedId, data: patch }]
|
|
75
|
+
if (parametrics?.reconcile && node) {
|
|
76
|
+
const next = { ...node, ...patch } as AnyNode
|
|
77
|
+
updates.push(...parametrics.reconcile(node as AnyNode, next))
|
|
78
|
+
}
|
|
79
|
+
scene.updateNodes(updates)
|
|
66
80
|
},
|
|
67
|
-
[selectedId],
|
|
81
|
+
[selectedId, parametrics],
|
|
68
82
|
)
|
|
69
83
|
|
|
70
84
|
const clearSelection = useCallback(() => {
|
|
@@ -77,10 +91,11 @@ export function ParametricInspector({
|
|
|
77
91
|
|
|
78
92
|
const handleMove = useCallback(() => {
|
|
79
93
|
if (!selectedId) return
|
|
80
|
-
const
|
|
94
|
+
const sceneNodes = useScene.getState().nodes
|
|
95
|
+
const node = sceneNodes[selectedId]
|
|
81
96
|
if (!node) return
|
|
82
97
|
sfxEmitter.emit('sfx:item-pick')
|
|
83
|
-
useEditor.getState().setMovingNode(node as any)
|
|
98
|
+
useEditor.getState().setMovingNode(resolveMoveActionNode(node, sceneNodes) as any)
|
|
84
99
|
clearSelection()
|
|
85
100
|
}, [selectedId, clearSelection])
|
|
86
101
|
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
23
23
|
import { guideEmitter } from '../../../lib/guide-events'
|
|
24
24
|
import { getGuideImageName } from '../../../lib/local-guide-image'
|
|
25
|
+
import { cn } from '../../../lib/utils'
|
|
25
26
|
import useEditor from '../../../store/use-editor'
|
|
26
27
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
27
28
|
import { PanelSection } from '../controls/panel-section'
|
|
@@ -60,6 +61,9 @@ export function ReferencePanel() {
|
|
|
60
61
|
? (s.nodes[selectedReferenceId as AnyNode['id']] as ReferenceNode | undefined)
|
|
61
62
|
: undefined,
|
|
62
63
|
)
|
|
64
|
+
const isScaleFlowActive = useEditor(
|
|
65
|
+
(s) => s.referenceScaleActiveGuideId !== null && s.referenceScaleActiveGuideId === node?.id,
|
|
66
|
+
)
|
|
63
67
|
|
|
64
68
|
const handleUpdate = useCallback(
|
|
65
69
|
(updates: Partial<ReferenceNode>) => {
|
|
@@ -98,6 +102,9 @@ export function ReferencePanel() {
|
|
|
98
102
|
} as Partial<GuideNode>,
|
|
99
103
|
)
|
|
100
104
|
setGuideScaleReferenceVisible(selectedReferenceId, true)
|
|
105
|
+
// The new image starts uncalibrated — drop the calibration auto-lock
|
|
106
|
+
// so it can be resized/rotated right away.
|
|
107
|
+
setGuideLocked(selectedReferenceId, false)
|
|
101
108
|
} catch {
|
|
102
109
|
setReplaceError('Could not replace that image.')
|
|
103
110
|
} finally {
|
|
@@ -123,6 +130,13 @@ export function ReferencePanel() {
|
|
|
123
130
|
return
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
// The scale line is drawn on the 2D plan — starting from a 3D-only view
|
|
134
|
+
// would arm the flow invisibly inside the hidden floorplan panel.
|
|
135
|
+
const editor = useEditor.getState()
|
|
136
|
+
if (editor.viewMode === '3d') {
|
|
137
|
+
editor.setViewMode('2d')
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
guideEmitter.emit('guide:set-reference-scale', { guideId: node.id })
|
|
127
141
|
}, [node])
|
|
128
142
|
|
|
@@ -233,33 +247,54 @@ export function ReferencePanel() {
|
|
|
233
247
|
|
|
234
248
|
<PanelSection title="Reference Scale">
|
|
235
249
|
<div className="flex items-center gap-2 rounded-md border border-border/50 bg-background/40 px-2.5 py-2 text-sm">
|
|
236
|
-
<Ruler
|
|
250
|
+
<Ruler
|
|
251
|
+
className={cn(
|
|
252
|
+
'h-4 w-4 shrink-0',
|
|
253
|
+
node.scaleReference ? 'text-primary' : 'text-amber-600 dark:text-amber-400',
|
|
254
|
+
)}
|
|
255
|
+
/>
|
|
237
256
|
<span className="truncate text-muted-foreground">{scaleStatus}</span>
|
|
238
257
|
</div>
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
{!node.scaleReference && (
|
|
260
|
+
<p className="px-0.5 text-muted-foreground text-xs leading-snug">
|
|
261
|
+
{isScaleFlowActive
|
|
262
|
+
? 'Click both ends of a known distance on the plan, then type its real length.'
|
|
263
|
+
: 'Draw a line over a known dimension on the plan, then type its real length to scale the image exactly.'}
|
|
264
|
+
</p>
|
|
265
|
+
)}
|
|
247
266
|
|
|
248
267
|
<ActionGroup>
|
|
249
268
|
<ActionButton
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
label="Clear Scale"
|
|
260
|
-
onClick={() => handleUpdate({ scaleReference: null } as Partial<GuideNode>)}
|
|
269
|
+
className={cn(
|
|
270
|
+
!node.scaleReference &&
|
|
271
|
+
!isScaleFlowActive &&
|
|
272
|
+
'border-primary/50 bg-primary/15 text-primary hover:bg-primary/25 active:bg-primary/25',
|
|
273
|
+
)}
|
|
274
|
+
label={
|
|
275
|
+
isScaleFlowActive ? 'Cancel' : node.scaleReference ? 'Edit Scale' : 'Set Scale'
|
|
276
|
+
}
|
|
277
|
+
onClick={isScaleFlowActive ? handleCancelScale : handleStartScale}
|
|
261
278
|
/>
|
|
262
279
|
</ActionGroup>
|
|
280
|
+
|
|
281
|
+
{node.scaleReference && (
|
|
282
|
+
<ActionGroup>
|
|
283
|
+
<ActionButton
|
|
284
|
+
label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
|
|
285
|
+
onClick={() => setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)}
|
|
286
|
+
/>
|
|
287
|
+
<ActionButton
|
|
288
|
+
label="Clear Scale"
|
|
289
|
+
onClick={() => {
|
|
290
|
+
handleUpdate({ scaleReference: null } as Partial<GuideNode>)
|
|
291
|
+
// Calibrating auto-locked the guide; clearing the scale
|
|
292
|
+
// returns it to a freely-editable reference.
|
|
293
|
+
setGuideLocked(node.id, false)
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
</ActionGroup>
|
|
297
|
+
)}
|
|
263
298
|
</PanelSection>
|
|
264
299
|
|
|
265
300
|
<PanelSection title="Quick Actions">
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { formatSelectionBreakdown } from './selection-breakdown'
|
|
3
|
+
|
|
4
|
+
describe('formatSelectionBreakdown', () => {
|
|
5
|
+
test('counts per type in first-appearance order, pluralizing with +s', () => {
|
|
6
|
+
expect(formatSelectionBreakdown(['slab', 'stair', 'fence', 'fence'])).toBe(
|
|
7
|
+
'1 slab · 1 stair · 2 fences',
|
|
8
|
+
)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('humanizes hyphenated kinds', () => {
|
|
12
|
+
expect(formatSelectionBreakdown(['roof-segment', 'roof-segment', 'wall'])).toBe(
|
|
13
|
+
'2 roof segments · 1 wall',
|
|
14
|
+
)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('skips missing nodes', () => {
|
|
18
|
+
expect(formatSelectionBreakdown(['wall', undefined, null])).toBe('1 wall')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('empty selection formats to an empty string', () => {
|
|
22
|
+
expect(formatSelectionBreakdown([])).toBe('')
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "1 slab · 1 stair · 2 fences" — one entry per node type in first-appearance
|
|
3
|
+
* order so the line stays stable while shift-clicking. Labels derive from the
|
|
4
|
+
* type id ('roof-segment' → 'roof segment'); pluralization is a simple +s
|
|
5
|
+
* (the codebase has no pluralize helper and no current kind needs one).
|
|
6
|
+
* Missing nodes (stale ids) are skipped.
|
|
7
|
+
*/
|
|
8
|
+
export function formatSelectionBreakdown(types: Array<string | null | undefined>): string {
|
|
9
|
+
const counts = new Map<string, number>()
|
|
10
|
+
for (const type of types) {
|
|
11
|
+
if (!type) continue
|
|
12
|
+
counts.set(type, (counts.get(type) ?? 0) + 1)
|
|
13
|
+
}
|
|
14
|
+
const parts: string[] = []
|
|
15
|
+
for (const [type, count] of counts) {
|
|
16
|
+
const label = type.replace(/-/g, ' ')
|
|
17
|
+
parts.push(`${count} ${count === 1 ? label : `${label}s`}`)
|
|
18
|
+
}
|
|
19
|
+
return parts.join(' · ')
|
|
20
|
+
}
|
|
@@ -22,6 +22,17 @@ const MOUSE_SHORTCUTS = {
|
|
|
22
22
|
},
|
|
23
23
|
} as const
|
|
24
24
|
|
|
25
|
+
// The platform-agnostic command modifier. Both Cmd and Ctrl bind the action; we
|
|
26
|
+
// render the symbol for the *current* device so the hint reads native (⌘ on Mac,
|
|
27
|
+
// Ctrl elsewhere) without implying only one of them works.
|
|
28
|
+
const COMMAND_VALUES = new Set(['Cmd/Ctrl', 'Cmd', 'Command', 'Meta'])
|
|
29
|
+
|
|
30
|
+
// Resolved once on the client at module load — the editor HUD is client-only, so
|
|
31
|
+
// there's no server render to mismatch against. `navigator.platform` is enough
|
|
32
|
+
// here and matches the detection used elsewhere (floorplan rotate hint).
|
|
33
|
+
const IS_MAC =
|
|
34
|
+
typeof navigator !== 'undefined' && navigator.platform.toUpperCase().includes('MAC')
|
|
35
|
+
|
|
25
36
|
type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
|
|
26
37
|
value: string
|
|
27
38
|
displayValue?: string
|
|
@@ -30,16 +41,23 @@ type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
|
|
|
30
41
|
function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTokenProps) {
|
|
31
42
|
const mouseShortcut =
|
|
32
43
|
value in MOUSE_SHORTCUTS ? MOUSE_SHORTCUTS[value as keyof typeof MOUSE_SHORTCUTS] : null
|
|
44
|
+
const isCommand = COMMAND_VALUES.has(value)
|
|
45
|
+
const isShift = value === 'Shift'
|
|
46
|
+
const commandDisplay = IS_MAC ? '⌘' : 'Ctrl'
|
|
47
|
+
const commandLabel = IS_MAC ? 'Command' : 'Control'
|
|
33
48
|
|
|
34
49
|
return (
|
|
35
50
|
<kbd
|
|
36
|
-
aria-label={
|
|
51
|
+
aria-label={
|
|
52
|
+
mouseShortcut?.label ??
|
|
53
|
+
(isCommand ? commandLabel : isShift ? 'Shift' : (displayValue ?? value))
|
|
54
|
+
}
|
|
37
55
|
className={cn(
|
|
38
56
|
'inline-flex h-6 items-center rounded border border-border bg-muted px-2 font-medium font-mono text-[11px] text-muted-foreground',
|
|
39
|
-
mouseShortcut && 'justify-center px-1.5',
|
|
57
|
+
(mouseShortcut || isShift) && 'justify-center px-1.5',
|
|
40
58
|
className,
|
|
41
59
|
)}
|
|
42
|
-
title={mouseShortcut?.label ?? value}
|
|
60
|
+
title={mouseShortcut?.label ?? (isCommand ? commandLabel : isShift ? 'Shift' : value)}
|
|
43
61
|
{...props}
|
|
44
62
|
>
|
|
45
63
|
{mouseShortcut ? (
|
|
@@ -54,6 +72,24 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
|
|
|
54
72
|
/>
|
|
55
73
|
<span className="sr-only">{mouseShortcut.label}</span>
|
|
56
74
|
</>
|
|
75
|
+
) : isShift ? (
|
|
76
|
+
// Icon rather than the ⇧ text glyph — the font renders the glyph's
|
|
77
|
+
// crossbar too high to read as the shift key symbol.
|
|
78
|
+
<>
|
|
79
|
+
<Icon
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
className="shrink-0"
|
|
82
|
+
color="currentColor"
|
|
83
|
+
height={13}
|
|
84
|
+
icon="ph:arrow-fat-up"
|
|
85
|
+
width={13}
|
|
86
|
+
/>
|
|
87
|
+
<span className="sr-only">Shift</span>
|
|
88
|
+
</>
|
|
89
|
+
) : isCommand ? (
|
|
90
|
+
// The ⌘ glyph reads small next to letters at the same font size, so bump
|
|
91
|
+
// it up a touch on Mac. "Ctrl" stays at the token's normal size.
|
|
92
|
+
<span className={IS_MAC ? 'text-[13px] leading-none' : undefined}>{commandDisplay}</span>
|
|
57
93
|
) : (
|
|
58
94
|
(displayValue ?? value)
|
|
59
95
|
)}
|
|
@@ -16,6 +16,7 @@ import useEditor from './../../../store/use-editor'
|
|
|
16
16
|
import { type ExtraPanel, IconRail } from './icon-rail'
|
|
17
17
|
import { SettingsPanel, type SettingsPanelProps } from './panels/settings-panel'
|
|
18
18
|
import { SitePanel, type SitePanelProps } from './panels/site-panel'
|
|
19
|
+
import { useHostPanels } from './use-plugin-panels'
|
|
19
20
|
|
|
20
21
|
interface AppSidebarProps {
|
|
21
22
|
appMenuButton?: ReactNode
|
|
@@ -31,9 +32,12 @@ export function AppSidebar({
|
|
|
31
32
|
sidebarTop,
|
|
32
33
|
settingsPanelProps,
|
|
33
34
|
sitePanelProps,
|
|
34
|
-
extraPanels,
|
|
35
|
+
extraPanels: hostExtraPanels,
|
|
35
36
|
commandPaletteEmptyAction,
|
|
36
37
|
}: AppSidebarProps) {
|
|
38
|
+
// Host-provided panels merged with registered host panels — the icon rail and
|
|
39
|
+
// content area treat both identically.
|
|
40
|
+
const extraPanels = useHostPanels(hostExtraPanels)
|
|
37
41
|
const activePanel = useEditor((s) => s.activeSidebarPanel)
|
|
38
42
|
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
39
43
|
const hasActivePanel =
|