@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
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { useScene } from '@pascal-app/core'
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
5
|
+
import {
|
|
6
|
+
lingoUnitSpec,
|
|
7
|
+
measurementHint,
|
|
8
|
+
parseMeasurement,
|
|
9
|
+
} from '../../../lib/measurement-parser'
|
|
10
|
+
import { useLinearDisplay } from '../../../lib/use-linear-display'
|
|
5
11
|
import { cn } from '../../../lib/utils'
|
|
6
12
|
|
|
7
13
|
interface SliderControlProps {
|
|
@@ -59,10 +65,17 @@ export function SliderControl({
|
|
|
59
65
|
unit = '',
|
|
60
66
|
restoreOnCommit = true,
|
|
61
67
|
}: SliderControlProps) {
|
|
68
|
+
// Display/storage conversion so the value honors the metric/imperial toggle.
|
|
69
|
+
// `value`, `onChange`, `onCommit`, `min`/`max`/`clamp` are always in the
|
|
70
|
+
// stored unit (meters for `unit === 'm'`); the step, drag deltas, text field
|
|
71
|
+
// and rendered number are in the DISPLAY unit (feet when imperial). For
|
|
72
|
+
// metric and non-length units these conversions are the identity.
|
|
73
|
+
const { isImperial, displayUnit, toDisplay, toStored } = useLinearDisplay(unit, precision)
|
|
74
|
+
|
|
62
75
|
const [isEditing, setIsEditing] = useState(false)
|
|
63
76
|
const [isDragging, setIsDragging] = useState(false)
|
|
64
77
|
const [isHovered, setIsHovered] = useState(false)
|
|
65
|
-
const [inputValue, setInputValue] = useState(value.toFixed(precision))
|
|
78
|
+
const [inputValue, setInputValue] = useState(toDisplay(value).toFixed(precision))
|
|
66
79
|
|
|
67
80
|
const dragRef = useRef<{
|
|
68
81
|
// Original value at drag start — preserved across modifier re-anchors so
|
|
@@ -80,12 +93,23 @@ export function SliderControl({
|
|
|
80
93
|
valueRef.current = value
|
|
81
94
|
|
|
82
95
|
const clamp = useCallback((val: number) => Math.min(Math.max(val, min), max), [min, max])
|
|
96
|
+
// Apply a signed display-unit delta to a stored value, rounding in the
|
|
97
|
+
// display unit and clamping in the stored unit.
|
|
98
|
+
const applyDisplayDelta = useCallback(
|
|
99
|
+
(storedValue: number, displayDelta: number, displayStep: number) =>
|
|
100
|
+
clamp(
|
|
101
|
+
toStored(
|
|
102
|
+
Number.parseFloat((toDisplay(storedValue) + displayDelta).toFixed(stepPrecision(displayStep))),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
[clamp, toDisplay, toStored],
|
|
106
|
+
)
|
|
83
107
|
|
|
84
108
|
useEffect(() => {
|
|
85
109
|
if (!isEditing) {
|
|
86
|
-
setInputValue(value.toFixed(precision))
|
|
110
|
+
setInputValue(toDisplay(value).toFixed(precision))
|
|
87
111
|
}
|
|
88
|
-
}, [value, precision, isEditing])
|
|
112
|
+
}, [value, precision, isEditing, toDisplay])
|
|
89
113
|
|
|
90
114
|
// Wheel support on the label
|
|
91
115
|
useEffect(() => {
|
|
@@ -96,14 +120,13 @@ export function SliderControl({
|
|
|
96
120
|
e.preventDefault()
|
|
97
121
|
const direction = e.deltaY < 0 ? 1 : -1
|
|
98
122
|
const s = getAdjustedStep(step, e)
|
|
99
|
-
const
|
|
100
|
-
const final = Number.parseFloat(newValue.toFixed(stepPrecision(s)))
|
|
123
|
+
const final = applyDisplayDelta(valueRef.current, direction * s, s)
|
|
101
124
|
if (final !== valueRef.current) onChange(final)
|
|
102
125
|
onCommit?.(final)
|
|
103
126
|
}
|
|
104
127
|
el.addEventListener('wheel', handleWheel, { passive: false })
|
|
105
128
|
return () => el.removeEventListener('wheel', handleWheel)
|
|
106
|
-
}, [isEditing, step,
|
|
129
|
+
}, [isEditing, step, applyDisplayDelta, onChange, onCommit])
|
|
107
130
|
|
|
108
131
|
// Arrow key support while hovered
|
|
109
132
|
useEffect(() => {
|
|
@@ -115,15 +138,14 @@ export function SliderControl({
|
|
|
115
138
|
if (direction !== 0) {
|
|
116
139
|
e.preventDefault()
|
|
117
140
|
const s = getAdjustedStep(step, e)
|
|
118
|
-
const
|
|
119
|
-
const final = Number.parseFloat(newValue.toFixed(stepPrecision(s)))
|
|
141
|
+
const final = applyDisplayDelta(valueRef.current, direction * s, s)
|
|
120
142
|
if (final !== valueRef.current) onChange(final)
|
|
121
143
|
onCommit?.(final)
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
146
|
window.addEventListener('keydown', handleKeyDown)
|
|
125
147
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
126
|
-
}, [isHovered, isEditing, step,
|
|
148
|
+
}, [isHovered, isEditing, step, applyDisplayDelta, onChange, onCommit])
|
|
127
149
|
|
|
128
150
|
const handleLabelPointerDown = useCallback(
|
|
129
151
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
@@ -160,15 +182,13 @@ export function SliderControl({
|
|
|
160
182
|
const dx = e.clientX - anchorX
|
|
161
183
|
const s = step * multiplier
|
|
162
184
|
// 4 px per step at default sensitivity
|
|
163
|
-
const newValue =
|
|
164
|
-
Number.parseFloat((anchorValue + (dx / 4) * s).toFixed(stepPrecision(s))),
|
|
165
|
-
)
|
|
185
|
+
const newValue = applyDisplayDelta(anchorValue, (dx / 4) * s, s)
|
|
166
186
|
if (newValue !== valueRef.current) {
|
|
167
187
|
valueRef.current = newValue
|
|
168
188
|
onChange(newValue)
|
|
169
189
|
}
|
|
170
190
|
},
|
|
171
|
-
[step,
|
|
191
|
+
[step, applyDisplayDelta, onChange],
|
|
172
192
|
)
|
|
173
193
|
|
|
174
194
|
const handleLabelPointerUp = useCallback(
|
|
@@ -195,49 +215,65 @@ export function SliderControl({
|
|
|
195
215
|
|
|
196
216
|
const handleValueClick = useCallback(() => {
|
|
197
217
|
setIsEditing(true)
|
|
198
|
-
setInputValue(value.toFixed(precision))
|
|
199
|
-
}, [value, precision])
|
|
218
|
+
setInputValue(toDisplay(value).toFixed(precision))
|
|
219
|
+
}, [value, precision, toDisplay])
|
|
200
220
|
|
|
201
221
|
const submitValue = useCallback(() => {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
const spec = lingoUnitSpec(unit)
|
|
223
|
+
let stored = spec
|
|
224
|
+
? parseMeasurement(inputValue, spec, {
|
|
225
|
+
bareUnit: isImperial ? 'ft' : spec.unitId,
|
|
226
|
+
system: isImperial ? 'us' : 'metric',
|
|
227
|
+
})
|
|
228
|
+
: null
|
|
229
|
+
if (stored === null) {
|
|
230
|
+
// Fallback: a bare number typed in the DISPLAY unit → convert to stored.
|
|
231
|
+
const numValue = Number.parseFloat(inputValue)
|
|
232
|
+
stored = Number.isFinite(numValue) ? toStored(numValue) : null
|
|
233
|
+
}
|
|
234
|
+
if (stored === null) {
|
|
235
|
+
setInputValue(toDisplay(value).toFixed(precision))
|
|
205
236
|
} else {
|
|
206
|
-
const nextValue = clamp(Number.parseFloat(
|
|
237
|
+
const nextValue = clamp(toStored(Number.parseFloat(toDisplay(stored).toFixed(precision))))
|
|
207
238
|
onChange(nextValue)
|
|
208
239
|
onCommit?.(nextValue)
|
|
209
240
|
}
|
|
210
241
|
setIsEditing(false)
|
|
211
|
-
}, [inputValue, onChange, onCommit, clamp, precision, value])
|
|
242
|
+
}, [inputValue, unit, isImperial, onChange, onCommit, clamp, precision, value, toDisplay, toStored])
|
|
243
|
+
|
|
244
|
+
const spec = lingoUnitSpec(unit)
|
|
245
|
+
const hint =
|
|
246
|
+
isEditing && spec
|
|
247
|
+
? measurementHint(inputValue, spec, {
|
|
248
|
+
bareUnit: isImperial ? 'ft' : spec.unitId,
|
|
249
|
+
system: isImperial ? 'us' : 'metric',
|
|
250
|
+
displayUnit: isImperial ? 'ft' : spec.unitId,
|
|
251
|
+
precision,
|
|
252
|
+
clamp,
|
|
253
|
+
})
|
|
254
|
+
: null
|
|
212
255
|
|
|
213
256
|
const handleInputKeyDown = useCallback(
|
|
214
257
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
215
258
|
if (e.key === 'Enter') {
|
|
216
259
|
submitValue()
|
|
217
260
|
} else if (e.key === 'Escape') {
|
|
218
|
-
setInputValue(value.toFixed(precision))
|
|
261
|
+
setInputValue(toDisplay(value).toFixed(precision))
|
|
219
262
|
setIsEditing(false)
|
|
220
|
-
} else if (e.key === 'ArrowUp') {
|
|
263
|
+
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
221
264
|
e.preventDefault()
|
|
265
|
+
const direction = e.key === 'ArrowUp' ? 1 : -1
|
|
222
266
|
const adjustedStep = getAdjustedStep(step, e)
|
|
223
|
-
const newV =
|
|
224
|
-
Number.parseFloat((value + adjustedStep).toFixed(stepPrecision(adjustedStep))),
|
|
225
|
-
)
|
|
267
|
+
const newV = applyDisplayDelta(value, direction * adjustedStep, adjustedStep)
|
|
226
268
|
onChange(newV)
|
|
227
|
-
setInputValue(newV.toFixed(precision))
|
|
228
|
-
} else if (e.key === 'ArrowDown') {
|
|
229
|
-
e.preventDefault()
|
|
230
|
-
const adjustedStep = getAdjustedStep(step, e)
|
|
231
|
-
const newV = clamp(
|
|
232
|
-
Number.parseFloat((value - adjustedStep).toFixed(stepPrecision(adjustedStep))),
|
|
233
|
-
)
|
|
234
|
-
onChange(newV)
|
|
235
|
-
setInputValue(newV.toFixed(precision))
|
|
269
|
+
setInputValue(toDisplay(newV).toFixed(precision))
|
|
236
270
|
}
|
|
237
271
|
},
|
|
238
|
-
[submitValue, value, precision, step,
|
|
272
|
+
[submitValue, value, precision, step, applyDisplayDelta, onChange, toDisplay],
|
|
239
273
|
)
|
|
240
274
|
|
|
275
|
+
const displayValue = toDisplay(value)
|
|
276
|
+
|
|
241
277
|
return (
|
|
242
278
|
<div
|
|
243
279
|
className={cn(
|
|
@@ -279,6 +315,11 @@ export function SliderControl({
|
|
|
279
315
|
<div className="flex items-center text-xs">
|
|
280
316
|
{isEditing ? (
|
|
281
317
|
<>
|
|
318
|
+
{hint && (
|
|
319
|
+
<span className="mr-1 shrink-0 whitespace-nowrap text-[10px] text-muted-foreground/50 tabular-nums">
|
|
320
|
+
{hint}
|
|
321
|
+
</span>
|
|
322
|
+
)}
|
|
282
323
|
<input
|
|
283
324
|
autoFocus
|
|
284
325
|
className="w-14 bg-transparent p-0 text-right font-mono text-foreground outline-none selection:bg-primary/30"
|
|
@@ -288,7 +329,16 @@ export function SliderControl({
|
|
|
288
329
|
type="text"
|
|
289
330
|
value={inputValue}
|
|
290
331
|
/>
|
|
291
|
-
{
|
|
332
|
+
{displayUnit && (
|
|
333
|
+
<span
|
|
334
|
+
className={cn(
|
|
335
|
+
'ml-[1px] transition-opacity duration-150',
|
|
336
|
+
hint ? 'opacity-0' : 'text-muted-foreground/40',
|
|
337
|
+
)}
|
|
338
|
+
>
|
|
339
|
+
{displayUnit}
|
|
340
|
+
</span>
|
|
341
|
+
)}
|
|
292
342
|
</>
|
|
293
343
|
) : (
|
|
294
344
|
<div
|
|
@@ -296,9 +346,9 @@ export function SliderControl({
|
|
|
296
346
|
onClick={handleValueClick}
|
|
297
347
|
>
|
|
298
348
|
<span className="font-mono tabular-nums tracking-tight" suppressHydrationWarning>
|
|
299
|
-
{Number(
|
|
349
|
+
{Number(displayValue.toFixed(precision)).toFixed(precision)}
|
|
300
350
|
</span>
|
|
301
|
-
{
|
|
351
|
+
{displayUnit && <span className="ml-[1px] text-muted-foreground">{displayUnit}</span>}
|
|
302
352
|
</div>
|
|
303
353
|
)}
|
|
304
354
|
</div>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
RAISE_METRES_PER_STROKE,
|
|
6
|
+
type SiteNode,
|
|
7
|
+
type TerrainVerb,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { Mountain, Pipette } from 'lucide-react'
|
|
11
|
+
import { brushRadiusRange, flattenSite, resetSiteTerrain } from '../../../lib/terrain-sculpt'
|
|
12
|
+
import useEditor from '../../../store/use-editor'
|
|
13
|
+
import { Button } from '../primitives/button'
|
|
14
|
+
import { SegmentedControl } from './segmented-control'
|
|
15
|
+
import { SliderControl } from './slider-control'
|
|
16
|
+
|
|
17
|
+
const VERB_OPTIONS: Array<{ value: TerrainVerb; iconSrc: string; hint: string }> = [
|
|
18
|
+
{ value: 'raise', iconSrc: '/icons/terrain-raise.webp', hint: 'Raise' },
|
|
19
|
+
{ value: 'lower', iconSrc: '/icons/terrain-lower.webp', hint: 'Lower' },
|
|
20
|
+
{ value: 'flatten', iconSrc: '/icons/terrain-flatten.webp', hint: 'Flatten' },
|
|
21
|
+
{ value: 'smooth', iconSrc: '/icons/terrain-smooth.webp', hint: 'Smooth' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const VERB_HINTS: Record<TerrainVerb, string> = {
|
|
25
|
+
raise: `Drag to raise the ground. One pass moves it up to ${RAISE_METRES_PER_STROKE} m — release and drag again to go further.`,
|
|
26
|
+
lower: `Drag to lower the ground. One pass moves it down to ${RAISE_METRES_PER_STROKE} m.`,
|
|
27
|
+
flatten: 'Drag to level the ground toward the target height. It never overshoots.',
|
|
28
|
+
smooth: 'Drag to soften slopes and remove ridges. Flat ground stays flat.',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sculpt controls for terrain mode — verb, brush, and the two lot-wide actions.
|
|
33
|
+
*
|
|
34
|
+
* A panel rather than a floating HUD because the brush settings are the sort of
|
|
35
|
+
* thing a user adjusts between strokes and then leaves alone, and because
|
|
36
|
+
* sculpting already owns the whole viewport pointer: putting controls over the
|
|
37
|
+
* canvas would put them over the surface being sculpted.
|
|
38
|
+
*
|
|
39
|
+
* Embedders mount this wherever their sculpt controls belong (the community
|
|
40
|
+
* editor puts it in the Build sidebar while sculpt mode is active), exactly like
|
|
41
|
+
* `MaterialPaintPanel`.
|
|
42
|
+
*/
|
|
43
|
+
export function TerrainSculptPanel() {
|
|
44
|
+
const verb = useEditor((state) => state.terrainVerb)
|
|
45
|
+
const setTerrainVerb = useEditor((state) => state.setTerrainVerb)
|
|
46
|
+
const brush = useEditor((state) => state.terrainBrush)
|
|
47
|
+
const setTerrainBrush = useEditor((state) => state.setTerrainBrush)
|
|
48
|
+
const flattenTarget = useEditor((state) => state.terrainFlattenTarget)
|
|
49
|
+
const setTerrainFlattenTarget = useEditor((state) => state.setTerrainFlattenTarget)
|
|
50
|
+
const sampling = useEditor((state) => state.terrainSampling)
|
|
51
|
+
const setTerrainSampling = useEditor((state) => state.setTerrainSampling)
|
|
52
|
+
|
|
53
|
+
const nodes = useScene((state) => state.nodes)
|
|
54
|
+
const rootNodeIds = useScene((state) => state.rootNodeIds)
|
|
55
|
+
const siteId = rootNodeIds[0]
|
|
56
|
+
const siteNode = siteId ? nodes[siteId as AnyNodeId] : undefined
|
|
57
|
+
const site = siteNode?.type === 'site' ? (siteNode as SiteNode) : null
|
|
58
|
+
const [minRadius, maxRadius] = brushRadiusRange(site)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-col gap-3">
|
|
62
|
+
<div className="flex flex-col gap-1.5">
|
|
63
|
+
<SegmentedControl
|
|
64
|
+
className="h-14"
|
|
65
|
+
onChange={(next) => setTerrainVerb(next)}
|
|
66
|
+
options={VERB_OPTIONS.map(({ value, iconSrc, hint }) => ({
|
|
67
|
+
value,
|
|
68
|
+
label: (
|
|
69
|
+
<span className="flex flex-col items-center gap-0.5">
|
|
70
|
+
<img
|
|
71
|
+
alt=""
|
|
72
|
+
aria-hidden
|
|
73
|
+
className="size-7 object-contain"
|
|
74
|
+
draggable={false}
|
|
75
|
+
height={28}
|
|
76
|
+
src={iconSrc}
|
|
77
|
+
width={28}
|
|
78
|
+
/>
|
|
79
|
+
<span className="text-[9px] leading-none">{hint}</span>
|
|
80
|
+
</span>
|
|
81
|
+
),
|
|
82
|
+
}))}
|
|
83
|
+
value={verb}
|
|
84
|
+
/>
|
|
85
|
+
<p className="px-0.5 text-muted-foreground text-xs">{VERB_HINTS[verb]}</p>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="flex flex-col gap-1.5">
|
|
89
|
+
{/*
|
|
90
|
+
Range from `brushRadiusRange`, shared with the `[`/`]` keys. The low end
|
|
91
|
+
is not a preference: it tracks the field's sample spacing, and a brush
|
|
92
|
+
under it lands between samples and paints nothing at all.
|
|
93
|
+
*/}
|
|
94
|
+
<SliderControl
|
|
95
|
+
label="Size"
|
|
96
|
+
max={maxRadius}
|
|
97
|
+
min={minRadius}
|
|
98
|
+
onChange={(radius) => setTerrainBrush({ radius })}
|
|
99
|
+
precision={1}
|
|
100
|
+
step={0.5}
|
|
101
|
+
unit="m"
|
|
102
|
+
value={brush.radius}
|
|
103
|
+
/>
|
|
104
|
+
<SliderControl
|
|
105
|
+
label="Strength"
|
|
106
|
+
max={1}
|
|
107
|
+
min={0.05}
|
|
108
|
+
onChange={(strength) => setTerrainBrush({ strength })}
|
|
109
|
+
precision={2}
|
|
110
|
+
step={0.05}
|
|
111
|
+
value={brush.strength}
|
|
112
|
+
/>
|
|
113
|
+
<SliderControl
|
|
114
|
+
label="Softness"
|
|
115
|
+
max={1}
|
|
116
|
+
min={0}
|
|
117
|
+
onChange={(falloff) => setTerrainBrush({ falloff })}
|
|
118
|
+
precision={2}
|
|
119
|
+
step={0.05}
|
|
120
|
+
value={brush.falloff}
|
|
121
|
+
/>
|
|
122
|
+
<SegmentedControl
|
|
123
|
+
onChange={(shape) => setTerrainBrush({ shape })}
|
|
124
|
+
options={[
|
|
125
|
+
{ value: 'round', label: 'Round' },
|
|
126
|
+
{ value: 'square', label: 'Square' },
|
|
127
|
+
]}
|
|
128
|
+
value={brush.shape}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{verb === 'flatten' && (
|
|
133
|
+
<div className="flex flex-col gap-1.5 border-border/60 border-t pt-3">
|
|
134
|
+
<div className="flex items-center gap-2">
|
|
135
|
+
<div className="min-w-0 flex-1">
|
|
136
|
+
<SliderControl
|
|
137
|
+
label="Target"
|
|
138
|
+
max={50}
|
|
139
|
+
min={-50}
|
|
140
|
+
onChange={setTerrainFlattenTarget}
|
|
141
|
+
precision={2}
|
|
142
|
+
step={0.1}
|
|
143
|
+
unit="m"
|
|
144
|
+
value={flattenTarget ?? 0}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<Button
|
|
148
|
+
aria-label="Pick target height from the ground"
|
|
149
|
+
aria-pressed={sampling}
|
|
150
|
+
onClick={() => setTerrainSampling(!sampling)}
|
|
151
|
+
size="icon-sm"
|
|
152
|
+
type="button"
|
|
153
|
+
variant={sampling ? 'default' : 'outline'}
|
|
154
|
+
>
|
|
155
|
+
<Pipette />
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
<p className="px-0.5 text-muted-foreground text-xs">
|
|
159
|
+
{sampling
|
|
160
|
+
? 'Click the ground to pick its height as the target.'
|
|
161
|
+
: flattenTarget === null
|
|
162
|
+
? 'No target yet — the first click samples the ground under it.'
|
|
163
|
+
: 'Every flatten stroke levels toward this height.'}
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<div className="flex items-center gap-2 border-border/60 border-t pt-3">
|
|
169
|
+
<Button
|
|
170
|
+
className="flex-1"
|
|
171
|
+
disabled={!site}
|
|
172
|
+
onClick={() => site && flattenSite(site, flattenTarget ?? 0)}
|
|
173
|
+
size="sm"
|
|
174
|
+
type="button"
|
|
175
|
+
variant="outline"
|
|
176
|
+
>
|
|
177
|
+
<Mountain />
|
|
178
|
+
Level lot
|
|
179
|
+
</Button>
|
|
180
|
+
<Button
|
|
181
|
+
className="flex-1"
|
|
182
|
+
disabled={!site?.terrain}
|
|
183
|
+
onClick={() => site && resetSiteTerrain(site)}
|
|
184
|
+
size="sm"
|
|
185
|
+
type="button"
|
|
186
|
+
variant="outline"
|
|
187
|
+
>
|
|
188
|
+
Clear terrain
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
type AnyNode,
|
|
23
23
|
type AnyNodeId,
|
|
24
24
|
type BuildingNode,
|
|
25
|
+
DEFAULT_LEVEL_HEIGHT,
|
|
26
|
+
getStoredLevelHeight,
|
|
25
27
|
LevelNode,
|
|
26
28
|
useScene,
|
|
27
29
|
} from '@pascal-app/core'
|
|
@@ -34,22 +36,19 @@ import {
|
|
|
34
36
|
useEffect,
|
|
35
37
|
useRef,
|
|
36
38
|
useState,
|
|
37
|
-
useSyncExternalStore,
|
|
38
39
|
} from 'react'
|
|
39
40
|
import { useShallow } from 'zustand/react/shallow'
|
|
41
|
+
import { pasteSelectionAndPickUp } from '../editor/group-actions'
|
|
40
42
|
import {
|
|
41
43
|
buildLevelDuplicateCreateOps,
|
|
42
44
|
type LevelDuplicatePreset,
|
|
43
45
|
} from '../../lib/level-duplication'
|
|
44
|
-
import { getDefaultLevelName, getLevelDisplayName } from '
|
|
46
|
+
import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
|
|
45
47
|
import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
|
|
46
|
-
import {
|
|
47
|
-
getEditorClipboardSnapshot,
|
|
48
|
-
pasteEditorClipboardToLevel,
|
|
49
|
-
subscribeEditorClipboard,
|
|
50
|
-
} from '../../lib/scene-clipboard'
|
|
51
|
-
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
48
|
+
import { useLinearDisplay } from '../../lib/use-linear-display'
|
|
52
49
|
import { cn } from '../../lib/utils'
|
|
50
|
+
import { ActionButton } from './controls/action-button'
|
|
51
|
+
import { SliderControl } from './controls/slider-control'
|
|
53
52
|
import { LevelDuplicateDialog } from './level-duplicate-dialog'
|
|
54
53
|
import {
|
|
55
54
|
Dialog,
|
|
@@ -145,6 +144,26 @@ function LevelRow({
|
|
|
145
144
|
}) {
|
|
146
145
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
147
146
|
const [isEditing, setIsEditing] = useState(false)
|
|
147
|
+
const updateNode = useScene((s) => s.updateNode)
|
|
148
|
+
const { isImperial, toDisplay, displayUnit } = useLinearDisplay('m', 2)
|
|
149
|
+
|
|
150
|
+
const storeyHeight = getStoredLevelHeight(level)
|
|
151
|
+
// toFixed(2) + strip one trailing zero: "2.50" → "2.5", "2.75" stays.
|
|
152
|
+
const storeyHeightLabel = `${toDisplay(storeyHeight).toFixed(2).replace(/0$/, '')} ${displayUnit}`
|
|
153
|
+
|
|
154
|
+
// Clean preset values per display system; imperial stores exact meters
|
|
155
|
+
// for whole-foot storey heights.
|
|
156
|
+
const heightPresets = isImperial
|
|
157
|
+
? [
|
|
158
|
+
{ label: '8 ft', height: 2.4384 },
|
|
159
|
+
{ label: '9 ft', height: 2.7432 },
|
|
160
|
+
{ label: '10 ft', height: 3.048 },
|
|
161
|
+
]
|
|
162
|
+
: [
|
|
163
|
+
{ label: '2.5 m', height: 2.5 },
|
|
164
|
+
{ label: '3.0 m', height: 3.0 },
|
|
165
|
+
{ label: '3.5 m', height: 3.5 },
|
|
166
|
+
]
|
|
148
167
|
|
|
149
168
|
return (
|
|
150
169
|
<div className="group/level">
|
|
@@ -195,6 +214,48 @@ function LevelRow({
|
|
|
195
214
|
<span className="truncate">{getLevelDisplayName(level)}</span>
|
|
196
215
|
</button>
|
|
197
216
|
|
|
217
|
+
{/* Storey height badge — opens the height popover */}
|
|
218
|
+
<Popover>
|
|
219
|
+
<PopoverTrigger asChild>
|
|
220
|
+
<button
|
|
221
|
+
className="mr-0.5 shrink-0 whitespace-nowrap rounded px-1 py-0.5 font-mono text-[10px] text-muted-foreground/50 tabular-nums transition-colors hover:bg-white/5 hover:text-foreground"
|
|
222
|
+
onClick={(e) => e.stopPropagation()}
|
|
223
|
+
title="Level height"
|
|
224
|
+
type="button"
|
|
225
|
+
>
|
|
226
|
+
{storeyHeightLabel}
|
|
227
|
+
</button>
|
|
228
|
+
</PopoverTrigger>
|
|
229
|
+
<PopoverContent
|
|
230
|
+
align="start"
|
|
231
|
+
className="w-56 p-2"
|
|
232
|
+
onClick={(e) => e.stopPropagation()}
|
|
233
|
+
side="right"
|
|
234
|
+
sideOffset={8}
|
|
235
|
+
>
|
|
236
|
+
<SliderControl
|
|
237
|
+
label="Level height"
|
|
238
|
+
max={6}
|
|
239
|
+
min={1}
|
|
240
|
+
onChange={(v) => updateNode(level.id, { height: v })}
|
|
241
|
+
precision={3}
|
|
242
|
+
step={0.1}
|
|
243
|
+
unit="m"
|
|
244
|
+
value={Math.round(storeyHeight * 1000) / 1000}
|
|
245
|
+
/>
|
|
246
|
+
<div className="mt-1.5 grid grid-cols-3 gap-1.5">
|
|
247
|
+
{heightPresets.map((preset) => (
|
|
248
|
+
<ActionButton
|
|
249
|
+
className="h-7 px-2"
|
|
250
|
+
key={preset.label}
|
|
251
|
+
label={preset.label}
|
|
252
|
+
onClick={() => updateNode(level.id, { height: preset.height })}
|
|
253
|
+
/>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
</PopoverContent>
|
|
257
|
+
</Popover>
|
|
258
|
+
|
|
198
259
|
{/* Vertical three-dot menu — inside the pill */}
|
|
199
260
|
<Popover>
|
|
200
261
|
<PopoverTrigger asChild>
|
|
@@ -332,11 +393,6 @@ export function FloatingLevelSelector() {
|
|
|
332
393
|
|
|
333
394
|
const [deletingLevel, setDeletingLevel] = useState<LevelNode | null>(null)
|
|
334
395
|
const [draggingLevelId, setDraggingLevelId] = useState<string | null>(null)
|
|
335
|
-
const clipboardSnapshot = useSyncExternalStore(
|
|
336
|
-
subscribeEditorClipboard,
|
|
337
|
-
getEditorClipboardSnapshot,
|
|
338
|
-
getEditorClipboardSnapshot,
|
|
339
|
-
)
|
|
340
396
|
const sensors = useSensors(
|
|
341
397
|
useSensor(PointerSensor, {
|
|
342
398
|
activationConstraint: { distance: 4 },
|
|
@@ -371,6 +427,7 @@ export function FloatingLevelSelector() {
|
|
|
371
427
|
const maxLevel = levels.length > 0 ? Math.max(...levels.map((l) => l.level)) : -1
|
|
372
428
|
const newLevel = LevelNode.parse({
|
|
373
429
|
level: maxLevel + 1,
|
|
430
|
+
height: DEFAULT_LEVEL_HEIGHT,
|
|
374
431
|
children: [],
|
|
375
432
|
parentId: resolvedBuildingId,
|
|
376
433
|
})
|
|
@@ -383,6 +440,7 @@ export function FloatingLevelSelector() {
|
|
|
383
440
|
const minLevel = levels.length > 0 ? Math.min(...levels.map((l) => l.level)) : 1
|
|
384
441
|
const newLevel = LevelNode.parse({
|
|
385
442
|
level: minLevel - 1,
|
|
443
|
+
height: DEFAULT_LEVEL_HEIGHT,
|
|
386
444
|
children: [],
|
|
387
445
|
parentId: resolvedBuildingId,
|
|
388
446
|
})
|
|
@@ -409,6 +467,7 @@ export function FloatingLevelSelector() {
|
|
|
409
467
|
|
|
410
468
|
const newLevel = LevelNode.parse({
|
|
411
469
|
level: newLevelNumber,
|
|
470
|
+
height: DEFAULT_LEVEL_HEIGHT,
|
|
412
471
|
children: [],
|
|
413
472
|
parentId: resolvedBuildingId,
|
|
414
473
|
})
|
|
@@ -452,10 +511,7 @@ export function FloatingLevelSelector() {
|
|
|
452
511
|
)
|
|
453
512
|
|
|
454
513
|
const handlePasteToLevel = useCallback((level: LevelNode) => {
|
|
455
|
-
|
|
456
|
-
if (result?.pastedIds.length) {
|
|
457
|
-
sfxEmitter.emit('sfx:item-place')
|
|
458
|
-
}
|
|
514
|
+
void pasteSelectionAndPickUp(level.id)
|
|
459
515
|
}, [])
|
|
460
516
|
|
|
461
517
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
@@ -557,9 +613,7 @@ export function FloatingLevelSelector() {
|
|
|
557
613
|
isSelected={isSelected}
|
|
558
614
|
level={level}
|
|
559
615
|
onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
|
|
560
|
-
onPaste={
|
|
561
|
-
clipboardSnapshot ? () => handlePasteToLevel(level) : undefined
|
|
562
|
-
}
|
|
616
|
+
onPaste={() => handlePasteToLevel(level)}
|
|
563
617
|
onRequestDelete={() => setDeletingLevel(level)}
|
|
564
618
|
onSelect={() =>
|
|
565
619
|
setSelection(
|
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
2
2
|
|
|
3
3
|
interface BuildingHelperProps {
|
|
4
4
|
showRotate?: boolean
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
// Rotate is one hint with both keys (R / T) — never two separate
|
|
8
|
+
// counterclockwise / clockwise rows — to match every other placement helper.
|
|
7
9
|
export function BuildingHelper({ showRotate }: BuildingHelperProps) {
|
|
8
10
|
return (
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<div className="flex items-center gap-2 text-sm">
|
|
17
|
-
<ShortcutToken value="R" />
|
|
18
|
-
<span className="text-muted-foreground">Rotate counterclockwise</span>
|
|
19
|
-
</div>
|
|
20
|
-
<div className="flex items-center gap-2 text-sm">
|
|
21
|
-
<ShortcutToken value="T" />
|
|
22
|
-
<span className="text-muted-foreground">Rotate clockwise</span>
|
|
23
|
-
</div>
|
|
24
|
-
</>
|
|
25
|
-
)}
|
|
26
|
-
<div className="flex items-center gap-2 text-sm">
|
|
27
|
-
<ShortcutToken value="Esc" />
|
|
28
|
-
<span className="text-muted-foreground">Cancel</span>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
11
|
+
<ContextualHelperPanel
|
|
12
|
+
hints={[
|
|
13
|
+
{ keys: ['Left click'], label: 'Place building' },
|
|
14
|
+
...(showRotate ? [{ keys: ['R', 'T'], label: 'Rotate' }] : []),
|
|
15
|
+
{ keys: ['Esc'], label: 'Cancel' },
|
|
16
|
+
]}
|
|
17
|
+
/>
|
|
31
18
|
)
|
|
32
19
|
}
|