@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,116 @@
|
|
|
1
|
+
import { type Kind, parseQuantity, quantity } from '@pascal-app/lingo'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Natural-language measurement parsing for editor property fields, backed by
|
|
5
|
+
* `@pascal-app/lingo`. Lets a user type `6ft`, `180cm`, `1m80`, `5'11"`, `45°`
|
|
6
|
+
* or `1.57rad` into any measurement field and have it canonicalized to the
|
|
7
|
+
* unit the field stores its value in — independent of the metric/imperial
|
|
8
|
+
* display toggle.
|
|
9
|
+
*
|
|
10
|
+
* The editor stores linear values in meters and angular values in radians (a
|
|
11
|
+
* few fields store inches or degrees); lingo's `length` base is meters and its
|
|
12
|
+
* `angle` base is radians, so a field's `unit` prop already names the stored /
|
|
13
|
+
* canonical unit and doubles as the parse target.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface LingoUnitSpec {
|
|
17
|
+
kind: Kind
|
|
18
|
+
/** Unit id the field's stored `value` is expressed in — the parse target. */
|
|
19
|
+
unitId: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Map an editor field `unit` prop to a lingo kind + canonical unit. Returns
|
|
24
|
+
* `null` for units we deliberately do NOT natural-language-parse (plain
|
|
25
|
+
* numbers, percentages, angular rates, counts) so those keep exact
|
|
26
|
+
* `Number.parseFloat` behavior.
|
|
27
|
+
*/
|
|
28
|
+
export function lingoUnitSpec(unit: string | undefined): LingoUnitSpec | null {
|
|
29
|
+
switch (unit) {
|
|
30
|
+
case 'm':
|
|
31
|
+
case 'cm':
|
|
32
|
+
case 'mm':
|
|
33
|
+
case 'in':
|
|
34
|
+
case 'ft':
|
|
35
|
+
return { kind: 'length', unitId: unit }
|
|
36
|
+
case '°':
|
|
37
|
+
case 'deg':
|
|
38
|
+
case 'degrees':
|
|
39
|
+
return { kind: 'angle', unitId: 'deg' }
|
|
40
|
+
case 'rad':
|
|
41
|
+
case 'radians':
|
|
42
|
+
return { kind: 'angle', unitId: 'rad' }
|
|
43
|
+
default:
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ParseMeasurementOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Implied unit for a BARE number (e.g. `6` → `6 ft`). Defaults to the field's
|
|
51
|
+
* own `unitId`. A typed unit (`180cm`) is always honored regardless.
|
|
52
|
+
*/
|
|
53
|
+
bareUnit?: string
|
|
54
|
+
/** Disambiguates gal/ton/cup families; harmless for length/angle. */
|
|
55
|
+
system?: 'metric' | 'us' | 'imperial'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse typed field text into the field's stored numeric unit. Returns `null`
|
|
60
|
+
* when the text can't be read as a quantity, so the caller can fall back to a
|
|
61
|
+
* plain number parse or revert to the previous value.
|
|
62
|
+
*/
|
|
63
|
+
export function parseMeasurement(
|
|
64
|
+
raw: string,
|
|
65
|
+
spec: LingoUnitSpec,
|
|
66
|
+
options: ParseMeasurementOptions = {},
|
|
67
|
+
): number | null {
|
|
68
|
+
const result = parseQuantity(raw, {
|
|
69
|
+
kind: spec.kind,
|
|
70
|
+
unit: options.bareUnit ?? spec.unitId,
|
|
71
|
+
system: options.system,
|
|
72
|
+
strictness: 'forgiving',
|
|
73
|
+
})
|
|
74
|
+
if (!result.ok) return null
|
|
75
|
+
const value = result.quantity.to(spec.unitId).value
|
|
76
|
+
return Number.isFinite(value) ? value : null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface MeasurementHintOptions extends ParseMeasurementOptions {
|
|
80
|
+
/** Unit id the preview is rendered in (the field's displayed unit). */
|
|
81
|
+
displayUnit?: string
|
|
82
|
+
/** Max fraction digits in the preview. */
|
|
83
|
+
precision?: number
|
|
84
|
+
/**
|
|
85
|
+
* Clamp applied to the parsed stored value before previewing, so the hint
|
|
86
|
+
* reflects what a commit would actually store on a bounded field.
|
|
87
|
+
*/
|
|
88
|
+
clamp?: (stored: number) => number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const PLAIN_NUMBER = /^[+-]?\d*\.?\d*$/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A faint "= 1.83 m" preview of the value currently being typed, shown only
|
|
95
|
+
* when the user typed something beyond a plain decimal in the field's own unit
|
|
96
|
+
* (an explicit unit, a compound like `1m80`, or a number word) and it parses.
|
|
97
|
+
* Returns `null` when there's nothing useful to preview.
|
|
98
|
+
*/
|
|
99
|
+
export function measurementHint(
|
|
100
|
+
raw: string,
|
|
101
|
+
spec: LingoUnitSpec,
|
|
102
|
+
options: MeasurementHintOptions = {},
|
|
103
|
+
): string | null {
|
|
104
|
+
const trimmed = raw.trim()
|
|
105
|
+
if (!trimmed || PLAIN_NUMBER.test(trimmed)) return null
|
|
106
|
+
|
|
107
|
+
const parsed = parseMeasurement(trimmed, spec, options)
|
|
108
|
+
if (parsed === null) return null
|
|
109
|
+
const stored = options.clamp ? options.clamp(parsed) : parsed
|
|
110
|
+
|
|
111
|
+
const displayUnit = options.displayUnit ?? spec.unitId
|
|
112
|
+
const precision = options.precision ?? 2
|
|
113
|
+
const q = quantity(stored, spec.unitId)
|
|
114
|
+
const shown = displayUnit === spec.unitId ? q : q.to(displayUnit)
|
|
115
|
+
return `= ${shown.format({ precision })}`
|
|
116
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
buildMeasurementAngleArcPoints,
|
|
4
|
+
cubicMetersToVolumeUnit,
|
|
5
|
+
formatAreaLabel,
|
|
6
|
+
formatLinearMeasurement,
|
|
7
|
+
formatVolumeLabel,
|
|
8
|
+
getAreaUnitLabel,
|
|
9
|
+
getLinearUnitLabel,
|
|
10
|
+
getVolumeUnitLabel,
|
|
11
|
+
linearControlValueToMeters,
|
|
12
|
+
linearUnitToMeters,
|
|
13
|
+
MEASUREMENT_ACTIVE_COLOR,
|
|
14
|
+
MEASUREMENT_DANGLING_COLOR,
|
|
15
|
+
MEASUREMENT_FLOORPLAN_COLOR,
|
|
16
|
+
MEASUREMENT_PERSISTENT_COLOR,
|
|
17
|
+
measurementFloorplanPresentationColor,
|
|
18
|
+
measurementPresentationColor,
|
|
19
|
+
metersToLinearUnit,
|
|
20
|
+
squareMetersToAreaUnit,
|
|
21
|
+
} from './measurements'
|
|
22
|
+
|
|
23
|
+
describe('measurement presentation', () => {
|
|
24
|
+
test('uses black at rest, indigo while active, and red for dangling references', () => {
|
|
25
|
+
expect(measurementPresentationColor(false, false)).toBe(MEASUREMENT_PERSISTENT_COLOR)
|
|
26
|
+
expect(measurementPresentationColor(false, true)).toBe(MEASUREMENT_ACTIVE_COLOR)
|
|
27
|
+
expect(measurementPresentationColor(true, false)).toBe(MEASUREMENT_DANGLING_COLOR)
|
|
28
|
+
expect(measurementPresentationColor(true, true)).toBe(MEASUREMENT_DANGLING_COLOR)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('uses an indigo analysis color for resting 2D measurements', () => {
|
|
32
|
+
expect(measurementFloorplanPresentationColor(false, false)).toBe(MEASUREMENT_FLOORPLAN_COLOR)
|
|
33
|
+
expect(measurementFloorplanPresentationColor(false, true)).toBe(MEASUREMENT_ACTIVE_COLOR)
|
|
34
|
+
expect(measurementFloorplanPresentationColor(true, false)).toBe(MEASUREMENT_DANGLING_COLOR)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('angle arc presentation', () => {
|
|
39
|
+
test('samples the smaller angle from the first ray to the second', () => {
|
|
40
|
+
const arc = buildMeasurementAngleArcPoints([1, 0, 0], [0, 0, 0], [0, 0, 1], {
|
|
41
|
+
radius: 0.25,
|
|
42
|
+
sampleCount: 8,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(arc).toHaveLength(9)
|
|
46
|
+
expect(arc[0]?.[0]).toBeCloseTo(0.25)
|
|
47
|
+
expect(arc[0]?.[2]).toBeCloseTo(0)
|
|
48
|
+
expect(arc.at(-1)?.[0]).toBeCloseTo(0)
|
|
49
|
+
expect(arc.at(-1)?.[2]).toBeCloseTo(0.25)
|
|
50
|
+
expect(arc[4]?.[0]).toBeGreaterThan(0)
|
|
51
|
+
expect(arc[4]?.[2]).toBeGreaterThan(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('keeps a constant radius on an arbitrary 3D angle plane', () => {
|
|
55
|
+
const arc = buildMeasurementAngleArcPoints([1, 0, 0], [0, 0, 0], [0, 1, 0], {
|
|
56
|
+
radius: 0.3,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
expect(arc.length).toBeGreaterThan(4)
|
|
60
|
+
for (const point of arc) expect(Math.hypot(...point)).toBeCloseTo(0.3)
|
|
61
|
+
expect(arc.at(-1)?.[1]).toBeCloseTo(0.3)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('omits an arc when either ray is degenerate', () => {
|
|
65
|
+
expect(buildMeasurementAngleArcPoints([0, 0, 0], [0, 0, 0], [1, 0, 0])).toEqual([])
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('linear measurements', () => {
|
|
70
|
+
test('formats metric measurements in meters', () => {
|
|
71
|
+
expect(formatLinearMeasurement(3, 'metric')).toBe('3m')
|
|
72
|
+
expect(formatLinearMeasurement(3.456, 'metric')).toBe('3.46m')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('formats metric measurements in whole millimeters', () => {
|
|
76
|
+
expect(formatLinearMeasurement(3.456, 'metric', 'millimeters')).toBe('3456mm')
|
|
77
|
+
expect(formatLinearMeasurement(-0.1524, 'metric', 'millimeters')).toBe('-152mm')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('formats imperial measurements as feet and inches', () => {
|
|
81
|
+
expect(formatLinearMeasurement(3.048, 'imperial')).toBe(`10'0"`)
|
|
82
|
+
expect(formatLinearMeasurement(3.2004, 'imperial')).toBe(`10'6"`)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('carries rounded 12 inches into the next foot', () => {
|
|
86
|
+
expect(formatLinearMeasurement(3.047, 'imperial')).toBe(`10'0"`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('returns a placeholder for non-finite measurements', () => {
|
|
90
|
+
expect(formatLinearMeasurement(NaN, 'imperial')).toBe('--')
|
|
91
|
+
expect(formatLinearMeasurement(Infinity, 'imperial')).toBe('--')
|
|
92
|
+
expect(formatLinearMeasurement(NaN, 'metric')).toBe('--')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('formats zero measurements', () => {
|
|
96
|
+
expect(formatLinearMeasurement(0, 'imperial')).toBe(`0'0"`)
|
|
97
|
+
expect(formatLinearMeasurement(0, 'metric')).toBe('0m')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('formats sub-foot imperial measurements', () => {
|
|
101
|
+
expect(formatLinearMeasurement(0.1524, 'imperial')).toBe(`0'6"`)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('formats negative measurements with a sign', () => {
|
|
105
|
+
expect(formatLinearMeasurement(-0.1524, 'imperial')).toBe(`-0'6"`)
|
|
106
|
+
expect(formatLinearMeasurement(-0.1524, 'metric')).toBe('-0.15m')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('converts between meters and the active linear unit', () => {
|
|
110
|
+
expect(metersToLinearUnit(0, 'imperial')).toBe(0)
|
|
111
|
+
expect(linearUnitToMeters(0, 'imperial')).toBe(0)
|
|
112
|
+
|
|
113
|
+
expect(metersToLinearUnit(1, 'metric')).toBe(1)
|
|
114
|
+
expect(linearUnitToMeters(1, 'metric')).toBe(1)
|
|
115
|
+
|
|
116
|
+
expect(metersToLinearUnit(0.3048, 'imperial')).toBeCloseTo(1)
|
|
117
|
+
expect(linearUnitToMeters(1, 'imperial')).toBeCloseTo(0.3048)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('converts numeric control input back to meters for wall panel edits', () => {
|
|
121
|
+
expect(linearControlValueToMeters(10, 'imperial')).toBeCloseTo(3.048)
|
|
122
|
+
expect(linearControlValueToMeters(0.5, 'imperial')).toBeCloseTo(0.1524)
|
|
123
|
+
expect(linearControlValueToMeters(-1, 'imperial')).toBeCloseTo(-0.3048)
|
|
124
|
+
expect(linearControlValueToMeters(3.5, 'metric')).toBe(3.5)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('clamps numeric control input after converting to meters', () => {
|
|
128
|
+
expect(linearControlValueToMeters(0.1, 'imperial', { minMeters: 0.1 })).toBe(0.1)
|
|
129
|
+
expect(linearControlValueToMeters(0.3, 'imperial', { minMeters: 0.1 })).toBe(0.1)
|
|
130
|
+
expect(linearControlValueToMeters(19.7, 'imperial', { maxMeters: 6 })).toBe(6)
|
|
131
|
+
expect(linearControlValueToMeters(0.2, 'metric', { minMeters: 0.1 })).toBe(0.2)
|
|
132
|
+
expect(linearControlValueToMeters(0.2, 'metric', { maxMeters: 0.15 })).toBe(0.15)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('returns the display label for numeric controls', () => {
|
|
136
|
+
expect(getLinearUnitLabel('metric')).toBe('m')
|
|
137
|
+
expect(getLinearUnitLabel('imperial')).toBe('ft')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('area measurements', () => {
|
|
142
|
+
test('converts square meters to the active area unit', () => {
|
|
143
|
+
expect(squareMetersToAreaUnit(0, 'imperial')).toBe(0)
|
|
144
|
+
expect(squareMetersToAreaUnit(12.5, 'metric')).toBe(12.5)
|
|
145
|
+
expect(squareMetersToAreaUnit(1, 'imperial')).toBeCloseTo(10.7639)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('returns the display label for area readouts', () => {
|
|
149
|
+
expect(getAreaUnitLabel('metric')).toBe('m²')
|
|
150
|
+
expect(getAreaUnitLabel('imperial')).toBe('ft²')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('formats an area label with value and unit', () => {
|
|
154
|
+
expect(formatAreaLabel(12.34, 'metric')).toBe('12.3m²')
|
|
155
|
+
expect(formatAreaLabel(1, 'imperial')).toBe('10.8ft²')
|
|
156
|
+
expect(formatAreaLabel(12.34, 'metric', 2)).toBe('12.34m²')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('returns a placeholder for non-finite areas', () => {
|
|
160
|
+
expect(formatAreaLabel(NaN, 'metric')).toBe('--')
|
|
161
|
+
expect(formatAreaLabel(Infinity, 'imperial')).toBe('--')
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('volume measurements', () => {
|
|
166
|
+
test('converts cubic meters to the active volume unit', () => {
|
|
167
|
+
expect(cubicMetersToVolumeUnit(0, 'imperial')).toBe(0)
|
|
168
|
+
expect(cubicMetersToVolumeUnit(12.5, 'metric')).toBe(12.5)
|
|
169
|
+
expect(cubicMetersToVolumeUnit(1, 'imperial')).toBeCloseTo(35.3147)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('returns the display label for volume readouts', () => {
|
|
173
|
+
expect(getVolumeUnitLabel('metric')).toBe('m³')
|
|
174
|
+
expect(getVolumeUnitLabel('imperial')).toBe('ft³')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('formats a volume label with value and unit', () => {
|
|
178
|
+
expect(formatVolumeLabel(12.34, 'metric')).toBe('12.3m³')
|
|
179
|
+
expect(formatVolumeLabel(1, 'imperial')).toBe('35.3ft³')
|
|
180
|
+
expect(formatVolumeLabel(12.34, 'metric', 2)).toBe('12.34m³')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('returns a placeholder for non-finite volumes', () => {
|
|
184
|
+
expect(formatVolumeLabel(NaN, 'metric')).toBe('--')
|
|
185
|
+
expect(formatVolumeLabel(Infinity, 'imperial')).toBe('--')
|
|
186
|
+
expect(formatVolumeLabel(Number.NEGATIVE_INFINITY, 'metric')).toBe('--')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { MeasurementPoint } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export type LinearUnit = 'metric' | 'imperial'
|
|
4
|
+
export type MetricNotation = 'meters' | 'millimeters'
|
|
5
|
+
|
|
6
|
+
export const MEASUREMENT_ACTIVE_COLOR = '#6366f1'
|
|
7
|
+
export const MEASUREMENT_DANGLING_COLOR = '#dc2626'
|
|
8
|
+
export const MEASUREMENT_FLOORPLAN_COLOR = '#4f46e5'
|
|
9
|
+
export const MEASUREMENT_PERSISTENT_COLOR = '#111827'
|
|
10
|
+
|
|
11
|
+
export function measurementPresentationColor(dangling: boolean, active: boolean): string {
|
|
12
|
+
if (dangling) return MEASUREMENT_DANGLING_COLOR
|
|
13
|
+
return active ? MEASUREMENT_ACTIVE_COLOR : MEASUREMENT_PERSISTENT_COLOR
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function measurementFloorplanPresentationColor(dangling: boolean, active: boolean): string {
|
|
17
|
+
if (dangling) return MEASUREMENT_DANGLING_COLOR
|
|
18
|
+
return active ? MEASUREMENT_ACTIVE_COLOR : MEASUREMENT_FLOORPLAN_COLOR
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type MeasurementAngleArcOptions = {
|
|
22
|
+
radius?: number
|
|
23
|
+
sampleCount?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ANGLE_ARC_EPSILON = 1e-9
|
|
27
|
+
|
|
28
|
+
const subtractPoint = (point: MeasurementPoint, origin: MeasurementPoint): MeasurementPoint => [
|
|
29
|
+
point[0] - origin[0],
|
|
30
|
+
point[1] - origin[1],
|
|
31
|
+
point[2] - origin[2],
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const pointLength = (point: MeasurementPoint): number => Math.hypot(...point)
|
|
35
|
+
|
|
36
|
+
const scalePoint = (point: MeasurementPoint, scale: number): MeasurementPoint => [
|
|
37
|
+
point[0] * scale,
|
|
38
|
+
point[1] * scale,
|
|
39
|
+
point[2] * scale,
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const crossPoint = (first: MeasurementPoint, second: MeasurementPoint): MeasurementPoint => [
|
|
43
|
+
first[1] * second[2] - first[2] * second[1],
|
|
44
|
+
first[2] * second[0] - first[0] * second[2],
|
|
45
|
+
first[0] * second[1] - first[1] * second[0],
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const dotPoint = (first: MeasurementPoint, second: MeasurementPoint): number =>
|
|
49
|
+
first[0] * second[0] + first[1] * second[1] + first[2] * second[2]
|
|
50
|
+
|
|
51
|
+
export function buildMeasurementAngleArcPoints(
|
|
52
|
+
start: MeasurementPoint,
|
|
53
|
+
vertex: MeasurementPoint,
|
|
54
|
+
end: MeasurementPoint,
|
|
55
|
+
options: MeasurementAngleArcOptions = {},
|
|
56
|
+
): MeasurementPoint[] {
|
|
57
|
+
if (![...start, ...vertex, ...end].every(Number.isFinite)) return []
|
|
58
|
+
|
|
59
|
+
const startVector = subtractPoint(start, vertex)
|
|
60
|
+
const endVector = subtractPoint(end, vertex)
|
|
61
|
+
const startLength = pointLength(startVector)
|
|
62
|
+
const endLength = pointLength(endVector)
|
|
63
|
+
if (startLength <= ANGLE_ARC_EPSILON || endLength <= ANGLE_ARC_EPSILON) return []
|
|
64
|
+
|
|
65
|
+
const startDirection = scalePoint(startVector, 1 / startLength)
|
|
66
|
+
const endDirection = scalePoint(endVector, 1 / endLength)
|
|
67
|
+
const cosine = Math.max(-1, Math.min(1, dotPoint(startDirection, endDirection)))
|
|
68
|
+
const angle = Math.acos(cosine)
|
|
69
|
+
if (angle <= 1e-4) return []
|
|
70
|
+
|
|
71
|
+
let normal = crossPoint(startDirection, endDirection)
|
|
72
|
+
let normalLength = pointLength(normal)
|
|
73
|
+
if (normalLength <= ANGLE_ARC_EPSILON) {
|
|
74
|
+
const reference: MeasurementPoint = Math.abs(startDirection[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0]
|
|
75
|
+
normal = crossPoint(startDirection, reference)
|
|
76
|
+
normalLength = pointLength(normal)
|
|
77
|
+
}
|
|
78
|
+
if (normalLength <= ANGLE_ARC_EPSILON) return []
|
|
79
|
+
normal = scalePoint(normal, 1 / normalLength)
|
|
80
|
+
const tangent = crossPoint(normal, startDirection)
|
|
81
|
+
|
|
82
|
+
const shortestSide = Math.min(startLength, endLength)
|
|
83
|
+
const preferredRadius = options.radius ?? Math.min(Math.max(shortestSide * 0.28, 0.08), 0.75)
|
|
84
|
+
const radius = Math.min(Math.max(preferredRadius, 0), shortestSide * 0.45)
|
|
85
|
+
if (radius <= ANGLE_ARC_EPSILON) return []
|
|
86
|
+
|
|
87
|
+
const sampleCount = Math.min(
|
|
88
|
+
64,
|
|
89
|
+
Math.max(4, Math.round(options.sampleCount ?? Math.max(8, (angle / Math.PI) * 32))),
|
|
90
|
+
)
|
|
91
|
+
return Array.from({ length: sampleCount + 1 }, (_, index) => {
|
|
92
|
+
const sampleAngle = angle * (index / sampleCount)
|
|
93
|
+
const direction: MeasurementPoint = [
|
|
94
|
+
startDirection[0] * Math.cos(sampleAngle) + tangent[0] * Math.sin(sampleAngle),
|
|
95
|
+
startDirection[1] * Math.cos(sampleAngle) + tangent[1] * Math.sin(sampleAngle),
|
|
96
|
+
startDirection[2] * Math.cos(sampleAngle) + tangent[2] * Math.sin(sampleAngle),
|
|
97
|
+
]
|
|
98
|
+
return [
|
|
99
|
+
vertex[0] + direction[0] * radius,
|
|
100
|
+
vertex[1] + direction[1] * radius,
|
|
101
|
+
vertex[2] + direction[2] * radius,
|
|
102
|
+
]
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const METERS_PER_FOOT = 0.3048
|
|
107
|
+
const FEET_PER_METER = 1 / METERS_PER_FOOT
|
|
108
|
+
|
|
109
|
+
type LinearControlValueOptions = {
|
|
110
|
+
minMeters?: number
|
|
111
|
+
maxMeters?: number
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function metersToLinearUnit(meters: number, unit: LinearUnit): number {
|
|
115
|
+
return unit === 'imperial' ? meters * FEET_PER_METER : meters
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function linearUnitToMeters(value: number, unit: LinearUnit): number {
|
|
119
|
+
return unit === 'imperial' ? value * METERS_PER_FOOT : value
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function linearControlValueToMeters(
|
|
123
|
+
value: number,
|
|
124
|
+
unit: LinearUnit,
|
|
125
|
+
options: LinearControlValueOptions = {},
|
|
126
|
+
): number {
|
|
127
|
+
const meters = linearUnitToMeters(value, unit)
|
|
128
|
+
const minMeters = options.minMeters ?? Number.NEGATIVE_INFINITY
|
|
129
|
+
const maxMeters = options.maxMeters ?? Number.POSITIVE_INFINITY
|
|
130
|
+
|
|
131
|
+
return Math.min(Math.max(meters, minMeters), maxMeters)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getLinearUnitLabel(unit: LinearUnit): string {
|
|
135
|
+
return unit === 'imperial' ? 'ft' : 'm'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const SQUARE_FEET_PER_SQUARE_METER = FEET_PER_METER * FEET_PER_METER
|
|
139
|
+
const CUBIC_FEET_PER_CUBIC_METER = SQUARE_FEET_PER_SQUARE_METER * FEET_PER_METER
|
|
140
|
+
|
|
141
|
+
export function squareMetersToAreaUnit(squareMeters: number, unit: LinearUnit): number {
|
|
142
|
+
return unit === 'imperial' ? squareMeters * SQUARE_FEET_PER_SQUARE_METER : squareMeters
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getAreaUnitLabel(unit: LinearUnit): string {
|
|
146
|
+
return unit === 'imperial' ? 'ft²' : 'm²'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatAreaLabel(
|
|
150
|
+
squareMeters: number,
|
|
151
|
+
unit: LinearUnit,
|
|
152
|
+
fractionDigits = 1,
|
|
153
|
+
): string {
|
|
154
|
+
if (!Number.isFinite(squareMeters)) return '--'
|
|
155
|
+
|
|
156
|
+
return `${squareMetersToAreaUnit(squareMeters, unit).toFixed(fractionDigits)}${getAreaUnitLabel(unit)}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function cubicMetersToVolumeUnit(cubicMeters: number, unit: LinearUnit): number {
|
|
160
|
+
return unit === 'imperial' ? cubicMeters * CUBIC_FEET_PER_CUBIC_METER : cubicMeters
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getVolumeUnitLabel(unit: LinearUnit): string {
|
|
164
|
+
return unit === 'imperial' ? 'ft³' : 'm³'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function formatVolumeLabel(
|
|
168
|
+
cubicMeters: number,
|
|
169
|
+
unit: LinearUnit,
|
|
170
|
+
fractionDigits = 1,
|
|
171
|
+
): string {
|
|
172
|
+
if (!Number.isFinite(cubicMeters)) return '--'
|
|
173
|
+
|
|
174
|
+
return `${cubicMetersToVolumeUnit(cubicMeters, unit).toFixed(fractionDigits)}${getVolumeUnitLabel(unit)}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function formatLinearMeasurement(
|
|
178
|
+
meters: number,
|
|
179
|
+
unit: LinearUnit,
|
|
180
|
+
metricNotation: MetricNotation = 'meters',
|
|
181
|
+
): string {
|
|
182
|
+
if (!Number.isFinite(meters)) return '--'
|
|
183
|
+
|
|
184
|
+
const absoluteMeters = Math.abs(meters)
|
|
185
|
+
|
|
186
|
+
if (unit === 'imperial') {
|
|
187
|
+
const feet = metersToLinearUnit(absoluteMeters, unit)
|
|
188
|
+
let wholeFeet = Math.floor(feet)
|
|
189
|
+
let inches = Math.round((feet - wholeFeet) * 12)
|
|
190
|
+
if (inches === 12) {
|
|
191
|
+
wholeFeet += 1
|
|
192
|
+
inches = 0
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const sign = meters < 0 && (wholeFeet !== 0 || inches !== 0) ? '-' : ''
|
|
196
|
+
|
|
197
|
+
return `${sign}${wholeFeet}'${inches}"`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (metricNotation === 'millimeters') {
|
|
201
|
+
const roundedMillimeters = Math.round(absoluteMeters * 1000)
|
|
202
|
+
const sign = meters < 0 && roundedMillimeters !== 0 ? '-' : ''
|
|
203
|
+
return `${sign}${roundedMillimeters}mm`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const roundedMeters = Number.parseFloat(absoluteMeters.toFixed(2))
|
|
207
|
+
const sign = meters < 0 && roundedMeters !== 0 ? '-' : ''
|
|
208
|
+
|
|
209
|
+
return `${sign}${roundedMeters}m`
|
|
210
|
+
}
|