@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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// The authoritative description of "what the user is currently doing".
|
|
2
|
+
//
|
|
3
|
+
// Before this, that question was answered by re-deriving from 7+ independent
|
|
4
|
+
// `useEditor` flags (`mode`, `tool`, `movingNode`, `placementDragMode`,
|
|
5
|
+
// `activeHandleDrag`, `curvingWall`, `curvingFence`, `editingHole`,
|
|
6
|
+
// `movingWallEndpoint`, `movingFenceEndpoint`, …). Every overlay and pick site
|
|
7
|
+
// re-derived its behaviour from a different subset, so the flags could drift
|
|
8
|
+
// into illegal combinations (moving + curving at once; a stale `movingNode`
|
|
9
|
+
// after a drag ended). Collapsing them into one discriminated union makes those
|
|
10
|
+
// combinations unrepresentable: a scope is exactly one interaction at a time,
|
|
11
|
+
// and `idle` carries no interaction payload at all.
|
|
12
|
+
|
|
13
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
14
|
+
|
|
15
|
+
export type InteractionView = '2d' | '3d'
|
|
16
|
+
export type ReshapeDriver = 'tool' | 'floorplan'
|
|
17
|
+
|
|
18
|
+
// Endpoint/curve/hole/boundary edits are all "reshape the selected node" — one
|
|
19
|
+
// node, one in-flight reshape. Grouping them as sub-states of `reshaping`
|
|
20
|
+
// (rather than four sibling scopes) keeps the union small while still making
|
|
21
|
+
// "curving and hole-editing at once" unrepresentable.
|
|
22
|
+
export type ReshapeKind = 'curve' | 'hole' | 'endpoint' | 'boundary' | 'control-point' | 'tangent'
|
|
23
|
+
|
|
24
|
+
export type InteractionScope =
|
|
25
|
+
| { kind: 'idle' }
|
|
26
|
+
// Placing a fresh node (catalog/preset/build tool). `pressDrag` is the
|
|
27
|
+
// gizmo press-drag flavour (commit on release) vs click-to-place.
|
|
28
|
+
| {
|
|
29
|
+
kind: 'placing'
|
|
30
|
+
// The node being placed, carried inline: a fresh-placement / duplicate
|
|
31
|
+
// draft is not in the scene yet, so it cannot be recovered by id. Set once
|
|
32
|
+
// at `begin` and never mutated, so it is a stable reference for the gesture.
|
|
33
|
+
node: AnyNode
|
|
34
|
+
nodeId: string
|
|
35
|
+
nodeType: string
|
|
36
|
+
view: InteractionView
|
|
37
|
+
pressDrag: boolean
|
|
38
|
+
}
|
|
39
|
+
// Moving an existing node.
|
|
40
|
+
| { kind: 'moving'; node: AnyNode; nodeId: string; nodeType: string; view: InteractionView }
|
|
41
|
+
// Dragging a resize/translate/rotate handle of a selected node.
|
|
42
|
+
| { kind: 'handle-drag'; nodeId: string; handle: string }
|
|
43
|
+
// Click-to-click drafting of a polyline/polygon kind (wall/fence/slab/…).
|
|
44
|
+
| { kind: 'drafting'; tool: string }
|
|
45
|
+
// Reshaping a selected node's geometry (see ReshapeKind). `holeIndex` is set
|
|
46
|
+
// only for `reshape: 'hole'`; `endpoint` only for `reshape: 'endpoint'`.
|
|
47
|
+
| {
|
|
48
|
+
kind: 'reshaping'
|
|
49
|
+
nodeId: string
|
|
50
|
+
reshape: ReshapeKind
|
|
51
|
+
// Floorplan affordances own their commit and must not mount a second 3D reshape tool.
|
|
52
|
+
driver: ReshapeDriver
|
|
53
|
+
holeIndex?: number
|
|
54
|
+
endpoint?: 'start' | 'end'
|
|
55
|
+
index?: number
|
|
56
|
+
side?: 'in' | 'out'
|
|
57
|
+
}
|
|
58
|
+
// Marquee selection drag.
|
|
59
|
+
| { kind: 'box-select' }
|
|
60
|
+
// Material paint application.
|
|
61
|
+
| { kind: 'painting' }
|
|
62
|
+
// Terrain sculpting. Held for the whole time the sculpt tool is armed, not
|
|
63
|
+
// just while the pointer is down — the same lifetime as `painting`, and for
|
|
64
|
+
// the same reason: an *active* scope is what makes every other object's
|
|
65
|
+
// handles, the floating action menu, and the zone labels step back
|
|
66
|
+
// (`resolveOverlayPolicy`). A brush that only claimed the scope between
|
|
67
|
+
// pointer-down and -up would flicker all of that back on between dabs.
|
|
68
|
+
| { kind: 'sculpting' }
|
|
69
|
+
|
|
70
|
+
export type InteractionKind = InteractionScope['kind']
|
|
71
|
+
|
|
72
|
+
export type ActiveInteractionScope = Exclude<InteractionScope, { kind: 'idle' }>
|
|
73
|
+
|
|
74
|
+
export const IDLE_SCOPE: InteractionScope = { kind: 'idle' }
|
|
75
|
+
|
|
76
|
+
export function isIdle(scope: InteractionScope): scope is { kind: 'idle' } {
|
|
77
|
+
return scope.kind === 'idle'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isActive(scope: InteractionScope): scope is ActiveInteractionScope {
|
|
81
|
+
return scope.kind !== 'idle'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The node a scope is acting on, if any. Drafting/box-select/painting/idle
|
|
85
|
+
// target no single existing node.
|
|
86
|
+
export function scopeNodeId(scope: InteractionScope): string | null {
|
|
87
|
+
switch (scope.kind) {
|
|
88
|
+
case 'placing':
|
|
89
|
+
case 'moving':
|
|
90
|
+
case 'handle-drag':
|
|
91
|
+
case 'reshaping':
|
|
92
|
+
return scope.nodeId
|
|
93
|
+
default:
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The node a placing/moving scope is acting on, carried inline (see the
|
|
99
|
+
// `placing` variant comment). Null for every other scope. Replaces the legacy
|
|
100
|
+
// `useEditor.movingNode` flag: the node lives inside the discriminated union, so
|
|
101
|
+
// it cannot survive past the interaction's `end()`.
|
|
102
|
+
export function movingNodeOf(scope: InteractionScope): AnyNode | null {
|
|
103
|
+
return scope.kind === 'placing' || scope.kind === 'moving' ? scope.node : null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Selection/hover picking is only meaningful while idle. During any active
|
|
107
|
+
// interaction the pointer belongs to that interaction's body, not to selecting
|
|
108
|
+
// a different object — the picking choke point should not route a hover/click
|
|
109
|
+
// to selection while this is false.
|
|
110
|
+
export function selectionEnabled(scope: InteractionScope): boolean {
|
|
111
|
+
return scope.kind === 'idle'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Derived views of the scope that mirror the legacy `useEditor` flags they
|
|
115
|
+
// replaced. Each returns null unless that exact interaction is active, so a
|
|
116
|
+
// stale payload is unrepresentable: the value is a pure function of the single
|
|
117
|
+
// authoritative scope, not an independent flag that can drift out of sync.
|
|
118
|
+
|
|
119
|
+
// The legacy `activeHandleDrag` flag. `label` keeps the legacy field name so
|
|
120
|
+
// downstream `=== ROTATE_HANDLE_DRAG_LABEL` / `=== 'height'` checks are unchanged.
|
|
121
|
+
export function handleDragInfo(scope: InteractionScope): { nodeId: string; label: string } | null {
|
|
122
|
+
return scope.kind === 'handle-drag' ? { nodeId: scope.nodeId, label: scope.handle } : null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The legacy `editingHole` flag (`SurfaceHoleTarget`).
|
|
126
|
+
export function editingHoleInfo(
|
|
127
|
+
scope: InteractionScope,
|
|
128
|
+
): { nodeId: string; holeIndex: number } | null {
|
|
129
|
+
return scope.kind === 'reshaping' && scope.reshape === 'hole' && scope.holeIndex !== undefined
|
|
130
|
+
? { nodeId: scope.nodeId, holeIndex: scope.holeIndex }
|
|
131
|
+
: null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build the scope payload for a hole reshape, so producers don't re-spell the
|
|
135
|
+
// discriminator at every call site.
|
|
136
|
+
export function holeEditScope(target: {
|
|
137
|
+
nodeId: string
|
|
138
|
+
holeIndex: number
|
|
139
|
+
driver?: ReshapeDriver
|
|
140
|
+
}): ActiveInteractionScope {
|
|
141
|
+
return {
|
|
142
|
+
kind: 'reshaping',
|
|
143
|
+
nodeId: target.nodeId,
|
|
144
|
+
reshape: 'hole',
|
|
145
|
+
holeIndex: target.holeIndex,
|
|
146
|
+
driver: target.driver ?? 'tool',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// True while the selected node's geometry is being curved (legacy
|
|
151
|
+
// `curvingWall` / `curvingFence` — now one scope; the wall-vs-fence kind is
|
|
152
|
+
// recovered from the reshaped node's type, looked up from the scene by nodeId).
|
|
153
|
+
export function isCurveReshape(scope: InteractionScope): boolean {
|
|
154
|
+
return scope.kind === 'reshaping' && scope.reshape === 'curve'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function isToolDrivenReshape(scope: InteractionScope): boolean {
|
|
158
|
+
return scope.kind === 'reshaping' && scope.driver === 'tool'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function isFloorplanDrivenReshape(scope: InteractionScope): boolean {
|
|
162
|
+
return scope.kind === 'reshaping' && scope.driver === 'floorplan'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The legacy `movingWallEndpoint` / `movingFenceEndpoint` flags minus the node
|
|
166
|
+
// itself (consumers fetch the node from the scene by `nodeId`; it is stable for
|
|
167
|
+
// the duration of the drag).
|
|
168
|
+
export function endpointReshapeInfo(
|
|
169
|
+
scope: InteractionScope,
|
|
170
|
+
): { nodeId: string; endpoint: 'start' | 'end' } | null {
|
|
171
|
+
return scope.kind === 'reshaping' && scope.reshape === 'endpoint' && scope.endpoint !== undefined
|
|
172
|
+
? { nodeId: scope.nodeId, endpoint: scope.endpoint }
|
|
173
|
+
: null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function controlPointReshapeInfo(
|
|
177
|
+
scope: InteractionScope,
|
|
178
|
+
): { nodeId: string; index: number } | null {
|
|
179
|
+
return scope.kind === 'reshaping' &&
|
|
180
|
+
scope.reshape === 'control-point' &&
|
|
181
|
+
scope.index !== undefined
|
|
182
|
+
? { nodeId: scope.nodeId, index: scope.index }
|
|
183
|
+
: null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function tangentReshapeInfo(
|
|
187
|
+
scope: InteractionScope,
|
|
188
|
+
): { nodeId: string; index: number; side: 'in' | 'out' } | null {
|
|
189
|
+
return scope.kind === 'reshaping' &&
|
|
190
|
+
scope.reshape === 'tangent' &&
|
|
191
|
+
scope.index !== undefined &&
|
|
192
|
+
scope.side !== undefined
|
|
193
|
+
? { nodeId: scope.nodeId, index: scope.index, side: scope.side }
|
|
194
|
+
: null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The id of the node being reshaped (any reshape kind), for the scene lookup
|
|
198
|
+
// that recovers the full node payload a few consumers still need.
|
|
199
|
+
export function reshapingNodeId(scope: InteractionScope): string | null {
|
|
200
|
+
return scope.kind === 'reshaping' ? scope.nodeId : null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Builders so producers don't re-spell the discriminator at every call site.
|
|
204
|
+
export function curveReshapeScope(
|
|
205
|
+
nodeId: string,
|
|
206
|
+
driver: ReshapeDriver = 'tool',
|
|
207
|
+
): ActiveInteractionScope {
|
|
208
|
+
return { kind: 'reshaping', nodeId, reshape: 'curve', driver }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function endpointReshapeScope(
|
|
212
|
+
nodeId: string,
|
|
213
|
+
endpoint: 'start' | 'end',
|
|
214
|
+
driver: ReshapeDriver = 'tool',
|
|
215
|
+
): ActiveInteractionScope {
|
|
216
|
+
return { kind: 'reshaping', nodeId, reshape: 'endpoint', endpoint, driver }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function controlPointReshapeScope(
|
|
220
|
+
nodeId: string,
|
|
221
|
+
index: number,
|
|
222
|
+
driver: ReshapeDriver = 'tool',
|
|
223
|
+
): ActiveInteractionScope {
|
|
224
|
+
return { kind: 'reshaping', nodeId, reshape: 'control-point', index, driver }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function tangentReshapeScope(
|
|
228
|
+
nodeId: string,
|
|
229
|
+
index: number,
|
|
230
|
+
side: 'in' | 'out',
|
|
231
|
+
driver: ReshapeDriver = 'tool',
|
|
232
|
+
): ActiveInteractionScope {
|
|
233
|
+
return { kind: 'reshaping', nodeId, reshape: 'tangent', index, side, driver }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Dragging a polygon vertex/edge (slab / ceiling boundary). Drives the snapping
|
|
237
|
+
// HUD (no-angle 'polygon' set) and keeps the idle select hints off-screen.
|
|
238
|
+
export function boundaryReshapeScope(
|
|
239
|
+
nodeId: string,
|
|
240
|
+
driver: ReshapeDriver = 'tool',
|
|
241
|
+
): ActiveInteractionScope {
|
|
242
|
+
return { kind: 'reshaping', nodeId, reshape: 'boundary', driver }
|
|
243
|
+
}
|
|
@@ -11,7 +11,7 @@ import { buildLevelDuplicateCreateOps } from './level-duplication'
|
|
|
11
11
|
|
|
12
12
|
describe('buildLevelDuplicateCreateOps', () => {
|
|
13
13
|
test('parents a duplicated bootstrap level back to its building', () => {
|
|
14
|
-
const level = LevelNode.parse({ level: 0, children: [] })
|
|
14
|
+
const level = LevelNode.parse({ level: 0, height: 3.25, children: [] })
|
|
15
15
|
const building = BuildingNode.parse({ children: [level.id] })
|
|
16
16
|
const wall = WallNode.parse({
|
|
17
17
|
parentId: level.id,
|
|
@@ -36,6 +36,7 @@ describe('buildLevelDuplicateCreateOps', () => {
|
|
|
36
36
|
|
|
37
37
|
expect(sourceLevel.parentId).toBeNull()
|
|
38
38
|
expect(levelCreateOp?.parentId).toBe(building.id)
|
|
39
|
+
expect(levelCreateOp?.node.type === 'level' ? levelCreateOp.node.height : undefined).toBe(3.25)
|
|
39
40
|
})
|
|
40
41
|
|
|
41
42
|
test('does not copy spawn points from the source level', () => {
|
|
@@ -5,10 +5,10 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
5
5
|
function getAdjacentLevelIdForDeletion(levelId: AnyNodeId): LevelNode['id'] | null {
|
|
6
6
|
const { nodes } = useScene.getState()
|
|
7
7
|
const level = nodes[levelId]
|
|
8
|
-
if (
|
|
8
|
+
if (level?.type !== 'level' || !level.parentId) return null
|
|
9
9
|
|
|
10
10
|
const building = nodes[level.parentId as AnyNodeId]
|
|
11
|
-
if (
|
|
11
|
+
if (building?.type !== 'building') return null
|
|
12
12
|
|
|
13
13
|
const siblingLevelIds = (building as BuildingNode).children.filter(
|
|
14
14
|
(childId): childId is LevelNode['id'] => nodes[childId as AnyNodeId]?.type === 'level',
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
getEffectiveRoofSurfaceMaterial,
|
|
14
14
|
getEffectiveSegmentSurfaceMaterial,
|
|
15
15
|
getEffectiveStairSurfaceMaterial,
|
|
16
|
-
getEffectiveWallSurfaceMaterial,
|
|
17
16
|
getLibraryMaterialIdFromRef,
|
|
18
17
|
type MaterialSchema,
|
|
19
18
|
type MaterialTarget,
|
|
@@ -26,28 +25,30 @@ import {
|
|
|
26
25
|
type SlabNode,
|
|
27
26
|
type StairNode,
|
|
28
27
|
type StairSurfaceMaterialRole,
|
|
29
|
-
type WallNode,
|
|
30
28
|
type WallSurfaceSide,
|
|
31
29
|
} from '@pascal-app/core'
|
|
32
30
|
|
|
33
|
-
export type PaintableMaterialTarget =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
export type PaintableMaterialTarget =
|
|
32
|
+
| Extract<
|
|
33
|
+
MaterialTarget,
|
|
34
|
+
| 'wall'
|
|
35
|
+
| 'roof'
|
|
36
|
+
| 'stair'
|
|
37
|
+
| 'fence'
|
|
38
|
+
| 'column'
|
|
39
|
+
| 'slab'
|
|
40
|
+
| 'ceiling'
|
|
41
|
+
| 'shelf'
|
|
42
|
+
| 'cabinet'
|
|
43
|
+
| 'chimney'
|
|
44
|
+
| 'dormer'
|
|
45
|
+
| 'box-vent'
|
|
46
|
+
| 'ridge-vent'
|
|
47
|
+
| 'turbine-vent'
|
|
48
|
+
| 'cupola'
|
|
49
|
+
| 'eyebrow-vent'
|
|
50
|
+
>
|
|
51
|
+
| 'item'
|
|
51
52
|
|
|
52
53
|
export type SingleSurfaceMaterialRole = 'surface'
|
|
53
54
|
|
|
@@ -76,32 +77,6 @@ export function getActivePaintMaterialLabel(material: ActivePaintMaterial | null
|
|
|
76
77
|
return getCatalogEntryForActivePaintMaterial(material)?.label ?? 'Custom'
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
export function buildWallSurfaceMaterialPatch(
|
|
80
|
-
node: WallNode,
|
|
81
|
-
targetSide: WallSurfaceSide,
|
|
82
|
-
material: MaterialSchema | undefined,
|
|
83
|
-
materialPreset: string | undefined,
|
|
84
|
-
): Partial<WallNode> {
|
|
85
|
-
const nextSurfaceMaterial = { material, materialPreset }
|
|
86
|
-
const nextInterior =
|
|
87
|
-
targetSide === 'interior'
|
|
88
|
-
? nextSurfaceMaterial
|
|
89
|
-
: getEffectiveWallSurfaceMaterial(node, 'interior')
|
|
90
|
-
const nextExterior =
|
|
91
|
-
targetSide === 'exterior'
|
|
92
|
-
? nextSurfaceMaterial
|
|
93
|
-
: getEffectiveWallSurfaceMaterial(node, 'exterior')
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
interiorMaterial: nextInterior.material,
|
|
97
|
-
interiorMaterialPreset: nextInterior.materialPreset,
|
|
98
|
-
exteriorMaterial: nextExterior.material,
|
|
99
|
-
exteriorMaterialPreset: nextExterior.materialPreset,
|
|
100
|
-
material: undefined,
|
|
101
|
-
materialPreset: undefined,
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
80
|
export function buildRoofSurfaceMaterialPatch(
|
|
106
81
|
node: RoofNode,
|
|
107
82
|
targetRole: RoofSurfaceMaterialRole,
|
|
@@ -179,6 +154,7 @@ export function buildResetSurfaceMaterialUpdates(
|
|
|
179
154
|
if (
|
|
180
155
|
key === 'material' ||
|
|
181
156
|
key === 'materialPreset' ||
|
|
157
|
+
key === 'slots' ||
|
|
182
158
|
key.endsWith('Material') ||
|
|
183
159
|
key.endsWith('MaterialPreset')
|
|
184
160
|
) {
|
|
@@ -276,6 +252,7 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|
|
276
252
|
| ChimneyMaterialRole
|
|
277
253
|
| DormerSurfaceMaterialRole
|
|
278
254
|
| SingleSurfaceMaterialRole
|
|
255
|
+
| string
|
|
279
256
|
} | null
|
|
280
257
|
}): ActivePaintMaterial | null {
|
|
281
258
|
const { nodes, selectedId, selectedMaterialTarget } = params
|
|
@@ -297,7 +274,7 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|
|
297
274
|
nodes,
|
|
298
275
|
})
|
|
299
276
|
if (surface) {
|
|
300
|
-
const sourceTarget = selectedNode.type as PaintableMaterialTarget
|
|
277
|
+
const sourceTarget = (paintCap.materialTarget ?? selectedNode.type) as PaintableMaterialTarget
|
|
301
278
|
return hasActivePaintMaterial({
|
|
302
279
|
material: surface.material,
|
|
303
280
|
materialPreset: surface.materialPreset,
|
|
@@ -375,11 +352,13 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|
|
375
352
|
// Wall / chimney / dormer flow through the registry-driven path
|
|
376
353
|
// at the top of this function.
|
|
377
354
|
|
|
355
|
+
// Slot-backed kinds resolve via the registry-driven `getEffectiveMaterial`
|
|
356
|
+
// path at the top of this function, including legacy inline-material
|
|
357
|
+
// fallbacks when their capability exposes one.
|
|
358
|
+
|
|
378
359
|
if (
|
|
379
360
|
(selectedNode.type === 'fence' ||
|
|
380
361
|
selectedNode.type === 'column' ||
|
|
381
|
-
selectedNode.type === 'slab' ||
|
|
382
|
-
selectedNode.type === 'ceiling' ||
|
|
383
362
|
selectedNode.type === 'shelf') &&
|
|
384
363
|
selectedMaterialTarget.role === 'surface'
|
|
385
364
|
) {
|
|
@@ -412,6 +391,10 @@ export function resolvePaintTargetFromSelection(params: {
|
|
|
412
391
|
const selectedNode = nodes[selectedId]
|
|
413
392
|
if (!selectedNode) return null
|
|
414
393
|
|
|
394
|
+
const registryPaintTarget = nodeRegistry.get(selectedNode.type)?.capabilities?.paint
|
|
395
|
+
?.materialTarget
|
|
396
|
+
if (registryPaintTarget) return registryPaintTarget as PaintableMaterialTarget
|
|
397
|
+
|
|
415
398
|
if (selectedNode.type === 'wall') {
|
|
416
399
|
return 'wall'
|
|
417
400
|
}
|
|
@@ -444,6 +427,10 @@ export function resolvePaintTargetFromSelection(params: {
|
|
|
444
427
|
return 'shelf'
|
|
445
428
|
}
|
|
446
429
|
|
|
430
|
+
if (selectedNode.type === 'item') {
|
|
431
|
+
return 'item'
|
|
432
|
+
}
|
|
433
|
+
|
|
447
434
|
if (selectedNode.type === 'chimney') {
|
|
448
435
|
return 'chimney'
|
|
449
436
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
CREATABLE_MEASUREMENT_KINDS,
|
|
4
|
+
DEFAULT_CREATABLE_MEASUREMENT_KIND,
|
|
5
|
+
normalizeCreatableMeasurementKind,
|
|
6
|
+
} from './measurement-kind'
|
|
7
|
+
|
|
8
|
+
describe('creatable measurement kinds', () => {
|
|
9
|
+
test('keeps the supported creation kinds', () => {
|
|
10
|
+
expect(CREATABLE_MEASUREMENT_KINDS).toEqual([
|
|
11
|
+
'distance',
|
|
12
|
+
'angle',
|
|
13
|
+
'area',
|
|
14
|
+
'perimeter',
|
|
15
|
+
'volume',
|
|
16
|
+
])
|
|
17
|
+
for (const kind of CREATABLE_MEASUREMENT_KINDS) {
|
|
18
|
+
expect(normalizeCreatableMeasurementKind(kind)).toBe(kind)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test.each([
|
|
23
|
+
'smart',
|
|
24
|
+
'unknown',
|
|
25
|
+
null,
|
|
26
|
+
undefined,
|
|
27
|
+
])('falls back from a non-creatable persisted value: %s', (value) => {
|
|
28
|
+
expect(normalizeCreatableMeasurementKind(value)).toBe(DEFAULT_CREATABLE_MEASUREMENT_KIND)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const CREATABLE_MEASUREMENT_KINDS = [
|
|
2
|
+
'distance',
|
|
3
|
+
'angle',
|
|
4
|
+
'area',
|
|
5
|
+
'perimeter',
|
|
6
|
+
'volume',
|
|
7
|
+
] as const
|
|
8
|
+
|
|
9
|
+
export type CreatableMeasurementKind = (typeof CREATABLE_MEASUREMENT_KINDS)[number]
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_CREATABLE_MEASUREMENT_KIND: CreatableMeasurementKind = 'distance'
|
|
12
|
+
|
|
13
|
+
export function isCreatableMeasurementKind(value: unknown): value is CreatableMeasurementKind {
|
|
14
|
+
return CREATABLE_MEASUREMENT_KINDS.includes(value as CreatableMeasurementKind)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeCreatableMeasurementKind(value: unknown): CreatableMeasurementKind {
|
|
18
|
+
return isCreatableMeasurementKind(value) ? value : DEFAULT_CREATABLE_MEASUREMENT_KIND
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { measurementCentroid, pointInPolygon2D } from '@pascal-app/core'
|
|
3
|
+
import { measurementPolygonLabelAnchor, triangulateMeasurementPolygon } from './measurement-label'
|
|
4
|
+
|
|
5
|
+
describe('measurementPolygonLabelAnchor', () => {
|
|
6
|
+
test('keeps a concave polygon label inside its visible fill', () => {
|
|
7
|
+
const base = [
|
|
8
|
+
[0, 0, 0],
|
|
9
|
+
[4, 0, 0],
|
|
10
|
+
[4, 0, 1],
|
|
11
|
+
[1, 0, 1],
|
|
12
|
+
[1, 0, 4],
|
|
13
|
+
[0, 0, 4],
|
|
14
|
+
] as [number, number, number][]
|
|
15
|
+
const centroid = measurementCentroid(base)
|
|
16
|
+
const anchor = measurementPolygonLabelAnchor(base)
|
|
17
|
+
|
|
18
|
+
expect(centroid).not.toBeNull()
|
|
19
|
+
expect(
|
|
20
|
+
pointInPolygon2D(
|
|
21
|
+
[centroid![0], centroid![2]],
|
|
22
|
+
base.map(([x, , z]) => [x, z]),
|
|
23
|
+
),
|
|
24
|
+
).toBe(false)
|
|
25
|
+
expect(anchor).not.toBeNull()
|
|
26
|
+
expect(
|
|
27
|
+
pointInPolygon2D(
|
|
28
|
+
[anchor![0], anchor![2]],
|
|
29
|
+
base.map(([x, , z]) => [x, z]),
|
|
30
|
+
),
|
|
31
|
+
).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('returns an on-plane anchor for a sloped polygon', () => {
|
|
35
|
+
const base = [
|
|
36
|
+
[0, 0, 0],
|
|
37
|
+
[4, 2, 0],
|
|
38
|
+
[4, 2, 4],
|
|
39
|
+
[0, 0, 4],
|
|
40
|
+
] as [number, number, number][]
|
|
41
|
+
const anchor = measurementPolygonLabelAnchor(base)
|
|
42
|
+
|
|
43
|
+
expect(triangulateMeasurementPolygon(base)).toHaveLength(2)
|
|
44
|
+
expect(anchor).not.toBeNull()
|
|
45
|
+
expect(anchor![1]).toBeCloseTo(anchor![0] / 2)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type MeasurementPoint, measurementCentroid, measurementNormal } from '@pascal-app/core'
|
|
2
|
+
import { ShapeUtils, Vector2, Vector3 } from 'three'
|
|
3
|
+
|
|
4
|
+
type MeasurementPolygonProjection = {
|
|
5
|
+
contour: Vector2[]
|
|
6
|
+
triangles: number[][]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function projectMeasurementPolygon(
|
|
10
|
+
points: readonly MeasurementPoint[],
|
|
11
|
+
): MeasurementPolygonProjection | null {
|
|
12
|
+
const normalValue = measurementNormal(points)
|
|
13
|
+
const originValue = points[0]
|
|
14
|
+
if (!(normalValue && originValue)) return null
|
|
15
|
+
|
|
16
|
+
const normal = new Vector3(...normalValue)
|
|
17
|
+
const reference = Math.abs(normal.y) < 0.9 ? new Vector3(0, 1, 0) : new Vector3(1, 0, 0)
|
|
18
|
+
const tangent = new Vector3().crossVectors(reference, normal).normalize()
|
|
19
|
+
const bitangent = new Vector3().crossVectors(normal, tangent).normalize()
|
|
20
|
+
const origin = new Vector3(...originValue)
|
|
21
|
+
const contour = points.map((point) => {
|
|
22
|
+
const relative = new Vector3(...point).sub(origin)
|
|
23
|
+
return new Vector2(relative.dot(tangent), relative.dot(bitangent))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return { contour, triangles: ShapeUtils.triangulateShape(contour, []) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function triangulateMeasurementPolygon(points: readonly MeasurementPoint[]): number[][] {
|
|
30
|
+
return projectMeasurementPolygon(points)?.triangles ?? []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function measurementPolygonLabelAnchor(
|
|
34
|
+
points: readonly MeasurementPoint[],
|
|
35
|
+
): MeasurementPoint | null {
|
|
36
|
+
const projection = projectMeasurementPolygon(points)
|
|
37
|
+
if (!projection || projection.triangles.length === 0) return measurementCentroid(points)
|
|
38
|
+
|
|
39
|
+
let largestTriangle: number[] | null = null
|
|
40
|
+
let largestArea = Number.NEGATIVE_INFINITY
|
|
41
|
+
for (const triangle of projection.triangles) {
|
|
42
|
+
const [firstIndex, secondIndex, thirdIndex] = triangle
|
|
43
|
+
const first = firstIndex === undefined ? null : projection.contour[firstIndex]
|
|
44
|
+
const second = secondIndex === undefined ? null : projection.contour[secondIndex]
|
|
45
|
+
const third = thirdIndex === undefined ? null : projection.contour[thirdIndex]
|
|
46
|
+
if (!(first && second && third)) continue
|
|
47
|
+
const area = Math.abs(
|
|
48
|
+
(second.x - first.x) * (third.y - first.y) - (second.y - first.y) * (third.x - first.x),
|
|
49
|
+
)
|
|
50
|
+
if (area > largestArea) {
|
|
51
|
+
largestArea = area
|
|
52
|
+
largestTriangle = triangle
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!largestTriangle) return measurementCentroid(points)
|
|
57
|
+
const [firstIndex, secondIndex, thirdIndex] = largestTriangle
|
|
58
|
+
const first = firstIndex === undefined ? null : points[firstIndex]
|
|
59
|
+
const second = secondIndex === undefined ? null : points[secondIndex]
|
|
60
|
+
const third = thirdIndex === undefined ? null : points[thirdIndex]
|
|
61
|
+
if (!(first && second && third)) return measurementCentroid(points)
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
(first[0] + second[0] + third[0]) / 3,
|
|
65
|
+
(first[1] + second[1] + third[1]) / 3,
|
|
66
|
+
(first[2] + second[2] + third[2]) / 3,
|
|
67
|
+
]
|
|
68
|
+
}
|