@pascal-app/editor 1.0.6 → 1.1.0
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 +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +110 -42
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +77 -68
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +31 -11
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +458 -140
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +23 -1
- package/src/components/editor/index.tsx +150 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +2 -3
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +4 -3
- package/src/components/editor/room-handles-2d.tsx +158 -7
- package/src/components/editor/room-handles.tsx +244 -54
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +33 -17
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +15 -7
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +47 -19
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
- package/src/components/ui/helpers/helper-manager.tsx +136 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
- package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +11 -10
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +9 -5
- package/src/components/ui/panels/panel-wrapper.tsx +63 -127
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +465 -66
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +22 -13
- package/src/components/ui/panels/room-panel.tsx +207 -100
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +62 -5
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15849 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +39331 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +39331 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +39331 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +39331 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +224 -45
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +101 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -2
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +15 -6
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +24 -6
- package/src/lib/measurements.ts +1 -203
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +28 -17
- package/src/lib/room-built-on.ts +21 -17
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +141 -1
- package/src/lib/room-handle-drag.ts +554 -58
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +38 -1
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +11 -5
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +21 -6
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +17 -6
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +44 -0
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +53 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-snap-indicator.ts +15 -2
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -100
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { type AnyNodeId,
|
|
2
|
+
import { type AnyNodeId, generateId, SiteNode, useScene } from '@pascal-app/core'
|
|
3
3
|
import { dropInLot } from './drop-in'
|
|
4
4
|
import type { ParcelResolveData } from './lot-patch'
|
|
5
5
|
import type { ParcelProvider } from './parcel-provider'
|
|
@@ -245,6 +245,64 @@ describe('dropInLot onto a site that already has a lot', () => {
|
|
|
245
245
|
useScene.setState({ nodes: {}, rootNodeIds: [], dirtyNodes: new Set() } as never)
|
|
246
246
|
})
|
|
247
247
|
|
|
248
|
+
test('a terrain response applies the parcel to the hydrated site and preserves its current settings', async () => {
|
|
249
|
+
let finishTerrain!: () => void
|
|
250
|
+
let startedTerrain!: () => void
|
|
251
|
+
const waiting = new Promise<void>((resolve) => {
|
|
252
|
+
finishTerrain = resolve
|
|
253
|
+
})
|
|
254
|
+
const started = new Promise<void>((resolve) => {
|
|
255
|
+
startedTerrain = resolve
|
|
256
|
+
})
|
|
257
|
+
const read = fakeProvider(null, 'sloped')
|
|
258
|
+
const provider: ParcelProvider = async (endpoint, request) => {
|
|
259
|
+
if (endpoint === 'elevation') {
|
|
260
|
+
startedTerrain()
|
|
261
|
+
await waiting
|
|
262
|
+
}
|
|
263
|
+
return read(endpoint, request)
|
|
264
|
+
}
|
|
265
|
+
const dropping = dropInLot(
|
|
266
|
+
{ address: GAINESVILLE },
|
|
267
|
+
{
|
|
268
|
+
dossier: false,
|
|
269
|
+
roads: false,
|
|
270
|
+
provider,
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
await started
|
|
274
|
+
expect(useScene.getState().rootNodeIds).toEqual([])
|
|
275
|
+
const hydrated = SiteNode.parse({
|
|
276
|
+
id: generateId('site'),
|
|
277
|
+
type: 'site',
|
|
278
|
+
name: 'My site',
|
|
279
|
+
polygon: { type: 'polygon', points: rect(30, 30) },
|
|
280
|
+
setbacks: { front: 8, side: 3, rear: 6 },
|
|
281
|
+
zone: 'Owner verified zone',
|
|
282
|
+
metadata: { ownerNote: 'Keep the driveway' },
|
|
283
|
+
})
|
|
284
|
+
useScene.setState({ nodes: { [hydrated.id]: hydrated }, rootNodeIds: [hydrated.id] } as never)
|
|
285
|
+
finishTerrain()
|
|
286
|
+
|
|
287
|
+
const result = await dropping
|
|
288
|
+
expect(result.ok).toBe(true)
|
|
289
|
+
expect(result.siteId).toBe(hydrated.id)
|
|
290
|
+
expect(useScene.getState().rootNodeIds).toEqual([hydrated.id])
|
|
291
|
+
const current = site(hydrated.id)
|
|
292
|
+
const points = current.polygon!.points
|
|
293
|
+
expect(Math.max(...points.map((p) => p[0])) - Math.min(...points.map((p) => p[0]))).toBe(25)
|
|
294
|
+
expect(Math.max(...points.map((p) => p[1])) - Math.min(...points.map((p) => p[1]))).toBe(32)
|
|
295
|
+
expect(current.parcel?.apn).toBe('06075-030-062')
|
|
296
|
+
expect(current.address?.city).toBe('Gainesville')
|
|
297
|
+
expect(current.terrain).toBeDefined()
|
|
298
|
+
expect(current.setbacks).toEqual(hydrated.setbacks)
|
|
299
|
+
expect(current.zone).toBe(hydrated.zone)
|
|
300
|
+
expect(current.metadata).toMatchObject({
|
|
301
|
+
ownerNote: 'Keep the driveway',
|
|
302
|
+
terrainSample: result.terrain,
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
248
306
|
test('the second lot replaces the first lot’s address, APN and county', async () => {
|
|
249
307
|
const provider = fakeProvider(null)
|
|
250
308
|
const first = await dropInLot({ address: LAKELAND }, { ...offline, provider })
|
package/src/lib/lot/drop-in.ts
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* 3. `sitePatchFromParcel` — the site node patch: ring, address,
|
|
10
10
|
* provenance, the street-facing front edge, planning-default setbacks
|
|
11
11
|
* when the site has none, north up.
|
|
12
|
-
* 4.
|
|
13
|
-
* has none) and a building that fell outside the new ring is
|
|
14
|
-
* re-centred on it — `building.position` only, never the walls.
|
|
15
|
-
* 5. `elevation` — USGS ground over the lot into the site's
|
|
12
|
+
* 4. `elevation` — USGS ground over the lot into the site's
|
|
16
13
|
* heightfield (`site.terrain`, terrain.ts; fail-soft, flat lots write
|
|
17
14
|
* nothing), so a foundation can read the hill.
|
|
15
|
+
* 5. The parcel and terrain are applied to the current site together
|
|
16
|
+
* (created at the root when the scene has none). A building outside
|
|
17
|
+
* the new ring is re-centred — `building.position` only, never the walls.
|
|
18
18
|
*
|
|
19
19
|
* Used by the Lot rail panel, the Generate panel (drop in, then generate)
|
|
20
20
|
* and the Site inspector's "Find parcel", so every path behaves the same.
|
|
@@ -271,71 +271,63 @@ export async function dropInLot(
|
|
|
271
271
|
roadsFailure = 'skipped'
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
if (!site) {
|
|
277
|
-
site = SiteNode.parse({ id: generateId('site'), type: 'site', name: 'Site' })
|
|
278
|
-
scene.createNodes([{ node: site as AnyNode }])
|
|
279
|
-
}
|
|
280
|
-
const computed = sitePatchFromParcel(site, input, data, roads, undefined, extras)
|
|
281
|
-
if (!computed)
|
|
274
|
+
const geometry = sitePatchFromParcel(null, input, data, roads, undefined, extras)
|
|
275
|
+
if (!geometry)
|
|
282
276
|
return {
|
|
283
277
|
ok: false,
|
|
284
278
|
error: 'parcel geometry was unusable',
|
|
285
279
|
message: 'The parcel geometry was unusable.',
|
|
286
280
|
}
|
|
287
|
-
// the surveyed contour lines ride the site with the patch (the site plan
|
|
288
|
-
// draws them over the heightfield's own); absent without 3DEP lines
|
|
289
|
-
const contours = dossier && dossierOrigin ? contourLinesFromDossier(dossier, dossierOrigin) : null
|
|
290
|
-
useScene
|
|
291
|
-
.getState()
|
|
292
|
-
.updateNode(
|
|
293
|
-
site.id as AnyNodeId,
|
|
294
|
-
{ ...computed.patch, terrainContours: contours ?? undefined } as Partial<AnyNode>,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
281
|
// The ground over the lot — fail-soft. A sloping lot writes the
|
|
298
282
|
// heightfield; a flat one (or a failed read) clears any terrain the
|
|
299
283
|
// previous lot left behind so the site never shows another parcel's hill.
|
|
300
284
|
let terrain: TerrainSampleSummary | null = null
|
|
301
285
|
let terrainFailure = ''
|
|
286
|
+
let terrainRead: TerrainSampleResult | null = null
|
|
302
287
|
if (options.terrain !== false) {
|
|
303
|
-
|
|
304
|
-
? await sampleLotTerrain(
|
|
288
|
+
terrainRead = data.originLngLat
|
|
289
|
+
? await sampleLotTerrain(geometry.patch.polygon?.points ?? [], data.originLngLat, {
|
|
305
290
|
provider,
|
|
306
291
|
...(options.terrainDeadlineMs ? { deadlineMs: options.terrainDeadlineMs } : {}),
|
|
307
292
|
})
|
|
308
293
|
: { ok: false, reason: 'no parcel origin' }
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
current &&
|
|
312
|
-
typeof current.metadata === 'object' &&
|
|
313
|
-
current.metadata !== null &&
|
|
314
|
-
!Array.isArray(current.metadata)
|
|
315
|
-
? (current.metadata as Record<string, unknown>)
|
|
316
|
-
: {}
|
|
317
|
-
if (read.ok && read.summary) {
|
|
318
|
-
terrain = read.summary
|
|
319
|
-
useScene.getState().updateNode(
|
|
320
|
-
site.id as AnyNodeId,
|
|
321
|
-
{
|
|
322
|
-
terrain: read.terrain,
|
|
323
|
-
metadata: { ...meta, terrainSample: read.summary },
|
|
324
|
-
} as unknown as Partial<AnyNode>,
|
|
325
|
-
)
|
|
326
|
-
} else {
|
|
327
|
-
terrainFailure = read.reason ?? 'no terrain'
|
|
328
|
-
useScene
|
|
329
|
-
.getState()
|
|
330
|
-
.updateNode(
|
|
331
|
-
site.id as AnyNodeId,
|
|
332
|
-
{ terrain: undefined, metadata: meta } as unknown as Partial<AnyNode>,
|
|
333
|
-
)
|
|
334
|
-
}
|
|
294
|
+
if (terrainRead.ok && terrainRead.summary) terrain = terrainRead.summary
|
|
295
|
+
else terrainFailure = terrainRead.reason ?? 'no terrain'
|
|
335
296
|
} else {
|
|
336
297
|
terrainFailure = 'skipped'
|
|
337
298
|
}
|
|
338
299
|
|
|
300
|
+
// Hydration or a person's edit can replace the site while the providers run.
|
|
301
|
+
// Read it after every await, preserve its current settings, and commit the
|
|
302
|
+
// parcel with its terrain so a late read cannot leave the starter polygon.
|
|
303
|
+
const scene = useScene.getState()
|
|
304
|
+
let site = findSiteNode(options.siteId)
|
|
305
|
+
const computed = sitePatchFromParcel(site, input, data, roads, undefined, extras)
|
|
306
|
+
if (!computed)
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error: 'parcel geometry was unusable',
|
|
310
|
+
message: 'The parcel geometry was unusable.',
|
|
311
|
+
}
|
|
312
|
+
const contours = dossier && dossierOrigin ? contourLinesFromDossier(dossier, dossierOrigin) : null
|
|
313
|
+
const patch: Partial<SiteNode> = {
|
|
314
|
+
...computed.patch,
|
|
315
|
+
terrainContours: contours ?? undefined,
|
|
316
|
+
}
|
|
317
|
+
if (terrainRead) {
|
|
318
|
+
const { terrainSample: _previous, ...metadata } = computed.patch.metadata as Record<
|
|
319
|
+
string,
|
|
320
|
+
unknown
|
|
321
|
+
>
|
|
322
|
+
patch.terrain = terrainRead.ok ? terrainRead.terrain : undefined
|
|
323
|
+
patch.metadata = terrain ? { ...metadata, terrainSample: terrain } : metadata
|
|
324
|
+
}
|
|
325
|
+
if (site) scene.updateNode(site.id as AnyNodeId, patch as Partial<AnyNode>)
|
|
326
|
+
else {
|
|
327
|
+
site = SiteNode.parse({ id: generateId('site'), type: 'site', name: 'Site', ...patch })
|
|
328
|
+
scene.createNodes([{ node: site as AnyNode }])
|
|
329
|
+
}
|
|
330
|
+
|
|
339
331
|
// A house the new lot leaves past its lines stands centred on the buildable area again, facing
|
|
340
332
|
// as it did.
|
|
341
333
|
let recentred = false
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MaterialSwatch } from '@pascal-app/core'
|
|
2
|
+
import type { CSSProperties } from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The CSS that paints a material swatch: the image over its colour, and a tinted
|
|
6
|
+
* albedo multiplied by the colour, as the 3D material tints its map.
|
|
7
|
+
*/
|
|
8
|
+
export function materialSwatchStyle({
|
|
9
|
+
color,
|
|
10
|
+
imageUrl,
|
|
11
|
+
tinted,
|
|
12
|
+
}: Pick<MaterialSwatch, 'color' | 'imageUrl'> & { tinted?: boolean }): CSSProperties {
|
|
13
|
+
return {
|
|
14
|
+
backgroundColor: color,
|
|
15
|
+
...(imageUrl
|
|
16
|
+
? {
|
|
17
|
+
backgroundImage: `url(${JSON.stringify(imageUrl)})`,
|
|
18
|
+
backgroundPosition: 'center',
|
|
19
|
+
backgroundSize: 'cover',
|
|
20
|
+
...(tinted ? { backgroundBlendMode: 'multiply' } : {}),
|
|
21
|
+
}
|
|
22
|
+
: {}),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import { measurementCentroid, pointInPolygon2D } from '@pascal-app/core'
|
|
3
|
-
import { measurementPolygonLabelAnchor, triangulateMeasurementPolygon } from '
|
|
3
|
+
import { measurementPolygonLabelAnchor, triangulateMeasurementPolygon } from '@pascal-app/viewer'
|
|
4
4
|
|
|
5
5
|
describe('measurementPolygonLabelAnchor', () => {
|
|
6
6
|
test('keeps a concave polygon label inside its visible fill', () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Must precede the lingo import: lingo clones its kind table with
|
|
2
2
|
// structuredClone while its module body evaluates. See the file's own comment.
|
|
3
3
|
import './structured-clone-fallback'
|
|
4
|
+
import { normalizeDecimalInput } from '@pascal-app/core'
|
|
4
5
|
import { type Kind, parseQuantity, quantity } from '@pascal-app/lingo'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -56,6 +57,8 @@ export interface ParseMeasurementOptions {
|
|
|
56
57
|
bareUnit?: string
|
|
57
58
|
/** Disambiguates gal/ton/cup families; harmless for length/angle. */
|
|
58
59
|
system?: 'metric' | 'us' | 'imperial'
|
|
60
|
+
/** Locale whose decimal separator is accepted in bare values and quantities. */
|
|
61
|
+
locale?: string
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
/**
|
|
@@ -68,7 +71,7 @@ export function parseMeasurement(
|
|
|
68
71
|
spec: LingoUnitSpec,
|
|
69
72
|
options: ParseMeasurementOptions = {},
|
|
70
73
|
): number | null {
|
|
71
|
-
const result = parseQuantity(raw, {
|
|
74
|
+
const result = parseQuantity(normalizeDecimalInput(raw, options.locale), {
|
|
72
75
|
kind: spec.kind,
|
|
73
76
|
unit: options.bareUnit ?? spec.unitId,
|
|
74
77
|
system: options.system,
|
|
@@ -105,9 +108,10 @@ export function measurementHint(
|
|
|
105
108
|
options: MeasurementHintOptions = {},
|
|
106
109
|
): string | null {
|
|
107
110
|
const trimmed = raw.trim()
|
|
108
|
-
|
|
111
|
+
const normalized = normalizeDecimalInput(trimmed, options.locale)
|
|
112
|
+
if (!normalized || PLAIN_NUMBER.test(normalized)) return null
|
|
109
113
|
|
|
110
|
-
const parsed = parseMeasurement(
|
|
114
|
+
const parsed = parseMeasurement(normalized, spec, options)
|
|
111
115
|
if (parsed === null) return null
|
|
112
116
|
const stored = options.clamp ? options.clamp(parsed) : parsed
|
|
113
117
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { measurementHint, parseMeasurement } from '../index'
|
|
3
2
|
import {
|
|
4
3
|
buildMeasurementAngleArcPoints,
|
|
5
4
|
cubicMetersToVolumeUnit,
|
|
@@ -11,15 +10,18 @@ import {
|
|
|
11
10
|
getVolumeUnitLabel,
|
|
12
11
|
linearControlValueToMeters,
|
|
13
12
|
linearUnitToMeters,
|
|
13
|
+
metersToLinearUnit,
|
|
14
|
+
parseLinearMeasurementInput,
|
|
15
|
+
squareMetersToAreaUnit,
|
|
16
|
+
} from '@pascal-app/core'
|
|
17
|
+
import {
|
|
14
18
|
MEASUREMENT_ACTIVE_COLOR,
|
|
15
19
|
MEASUREMENT_DANGLING_COLOR,
|
|
16
|
-
MEASUREMENT_FLOORPLAN_COLOR,
|
|
17
20
|
MEASUREMENT_PERSISTENT_COLOR,
|
|
18
|
-
measurementFloorplanPresentationColor,
|
|
19
21
|
measurementPresentationColor,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from './measurements'
|
|
22
|
+
} from '@pascal-app/viewer'
|
|
23
|
+
import { measurementHint, parseMeasurement } from '../index'
|
|
24
|
+
import { MEASUREMENT_FLOORPLAN_COLOR, measurementFloorplanPresentationColor } from './measurements'
|
|
23
25
|
|
|
24
26
|
describe('measurement presentation', () => {
|
|
25
27
|
test('uses black at rest, indigo while active, and red for dangling references', () => {
|
|
@@ -129,6 +131,11 @@ describe('linear measurements', () => {
|
|
|
129
131
|
expect(linearControlValueToMeters(3.5, 'metric')).toBe(3.5)
|
|
130
132
|
})
|
|
131
133
|
|
|
134
|
+
test('parses floor-plan measurement input with the scoped locale', () => {
|
|
135
|
+
expect(parseLinearMeasurementInput('2,75', 'metric', 'fr')).toBe(2.75)
|
|
136
|
+
expect(parseLinearMeasurementInput('2,75', 'metric', 'en')).toBeNull()
|
|
137
|
+
})
|
|
138
|
+
|
|
132
139
|
test('clamps numeric control input after converting to meters', () => {
|
|
133
140
|
expect(linearControlValueToMeters(0.1, 'imperial', { minMeters: 0.1 })).toBe(0.1)
|
|
134
141
|
expect(linearControlValueToMeters(0.3, 'imperial', { minMeters: 0.1 })).toBe(0.1)
|
|
@@ -212,4 +219,15 @@ describe('typed measurement fields', () => {
|
|
|
212
219
|
expect(measurementHint('180cm', length)).toBe('= 1.80 m')
|
|
213
220
|
expect(measurementHint('6', length)).toBeNull()
|
|
214
221
|
})
|
|
222
|
+
|
|
223
|
+
test('only treats the active locale decimal separator as a plain decimal', () => {
|
|
224
|
+
const length = { kind: 'length', unitId: 'm' } as const
|
|
225
|
+
|
|
226
|
+
expect(measurementHint('1,000', length, { locale: 'en' })).not.toBeNull()
|
|
227
|
+
expect(measurementHint('-1,000', length, { locale: 'en' })).not.toBeNull()
|
|
228
|
+
expect(measurementHint('1,5', length, { locale: 'en' })).not.toBeNull()
|
|
229
|
+
|
|
230
|
+
expect(measurementHint('1,5', length, { locale: 'fr' })).toBeNull()
|
|
231
|
+
expect(parseMeasurement('2,75 m', length, { locale: 'fr' })).toBe(2.75)
|
|
232
|
+
})
|
|
215
233
|
})
|
package/src/lib/measurements.ts
CHANGED
|
@@ -1,210 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { MEASUREMENT_ACTIVE_COLOR, MEASUREMENT_DANGLING_COLOR } from '@pascal-app/viewer'
|
|
2
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
3
|
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
4
|
|
|
16
5
|
export function measurementFloorplanPresentationColor(dangling: boolean, active: boolean): string {
|
|
17
6
|
if (dangling) return MEASUREMENT_DANGLING_COLOR
|
|
18
7
|
return active ? MEASUREMENT_ACTIVE_COLOR : MEASUREMENT_FLOORPLAN_COLOR
|
|
19
8
|
}
|
|
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
|
-
}
|
|
@@ -169,7 +169,9 @@ describe('mezzanine selection', () => {
|
|
|
169
169
|
expect(gesture(hostId, ROOM_MOVE_DRAG_LABEL)).toBeNull()
|
|
170
170
|
expect(gesture(mezzanineId, 'height')).toBeNull()
|
|
171
171
|
expect(mezzanineGesture(null, nodes())).toBeNull()
|
|
172
|
-
expect(MEZZANINE_GESTURE_HINTS.move[0]!.label).
|
|
172
|
+
expect(MEZZANINE_GESTURE_HINTS.move[0]!.label).toMatchObject({
|
|
173
|
+
message: 'Place inside the room',
|
|
174
|
+
})
|
|
173
175
|
})
|
|
174
176
|
|
|
175
177
|
test('the delete dialog speaks of the mezzanine, and of its items only when it has some', () => {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { beforeAll, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
isMoveLocked,
|
|
7
|
+
isNodeDuplicable,
|
|
8
|
+
nodeRegistry,
|
|
9
|
+
registerNode,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { z } from 'zod'
|
|
13
|
+
import { duplicateNodeAndPickUp } from '../components/editor/duplicate-node'
|
|
14
|
+
import { classifyParticipant } from '../components/editor/group-transform-shared'
|
|
15
|
+
import useEditor from '../store/use-editor'
|
|
16
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
17
|
+
import { canDirectMoveNode } from './direct-manipulation'
|
|
18
|
+
import { createEditorApi } from './editor-api'
|
|
19
|
+
import { registryMoveDisabled } from './node-action-movement'
|
|
20
|
+
import { copySelectedNodesToEditorClipboard } from './scene-clipboard'
|
|
21
|
+
|
|
22
|
+
// Mirrors a lean-to post: a kind-level 2D mover plus per-node move and duplicate locks.
|
|
23
|
+
const KIND = 'move-lock-test'
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
if (nodeRegistry.has(KIND)) return
|
|
27
|
+
registerNode({
|
|
28
|
+
kind: KIND,
|
|
29
|
+
schemaVersion: 1,
|
|
30
|
+
schema: z.object({ type: z.literal(KIND) }) as never,
|
|
31
|
+
category: 'structure',
|
|
32
|
+
defaults: () => ({}),
|
|
33
|
+
capabilities: {
|
|
34
|
+
movable: {
|
|
35
|
+
axes: ['x', 'z'],
|
|
36
|
+
gridSnap: true,
|
|
37
|
+
override: ({ node }) =>
|
|
38
|
+
(node as { metadata?: { locked?: boolean } }).metadata?.locked
|
|
39
|
+
? { axes: [], gridSnap: false }
|
|
40
|
+
: null,
|
|
41
|
+
},
|
|
42
|
+
duplicable: {
|
|
43
|
+
allow: ({ node }) => !(node as { metadata?: { locked?: boolean } }).metadata?.locked,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
floorplanMoveTarget: () => ({
|
|
47
|
+
affectedIds: [],
|
|
48
|
+
apply() {},
|
|
49
|
+
canCommit: () => false,
|
|
50
|
+
commit() {},
|
|
51
|
+
}),
|
|
52
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
53
|
+
} as AnyNodeDefinition)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
function node(locked: boolean) {
|
|
57
|
+
return {
|
|
58
|
+
id: locked ? 'move_lock_locked' : 'move_lock_free',
|
|
59
|
+
type: KIND,
|
|
60
|
+
parentId: 'level_move_lock',
|
|
61
|
+
position: [1, 0, 2],
|
|
62
|
+
rotation: 0,
|
|
63
|
+
metadata: locked ? { locked: true } : {},
|
|
64
|
+
} as unknown as AnyNode
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test('a per-node override lock outranks the kind-level 2D mover', () => {
|
|
68
|
+
expect(isMoveLocked(node(true))).toBe(true)
|
|
69
|
+
expect(isMoveLocked(node(false))).toBe(false)
|
|
70
|
+
expect(registryMoveDisabled(node(true))).toBe(true)
|
|
71
|
+
expect(registryMoveDisabled(node(false))).toBe(false)
|
|
72
|
+
expect(canDirectMoveNode(node(true))).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('group pick-up leaves a locked node behind', () => {
|
|
76
|
+
const locked = node(true)
|
|
77
|
+
const free = node(false)
|
|
78
|
+
const nodes = { [locked.id]: locked, [free.id]: free }
|
|
79
|
+
expect(classifyParticipant(locked, 'level_move_lock', nodes)).toBeNull()
|
|
80
|
+
expect(classifyParticipant(free, 'level_move_lock', nodes)).toBe('scalar')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('every moving-node entry refuses a locked node', () => {
|
|
84
|
+
const api = createEditorApi()
|
|
85
|
+
useEditor.getState().setMovingNode(node(true))
|
|
86
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
87
|
+
api.engageMoveDrag(node(true))
|
|
88
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
89
|
+
expect(useEditor.getState().placementDragMode).toBe(false)
|
|
90
|
+
api.engageMove(node(false))
|
|
91
|
+
expect(useInteractionScope.getState().scope.kind).toBe('moving')
|
|
92
|
+
useEditor.getState().setMovingNode(null)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('a locked node cannot be duplicated or copied', () => {
|
|
96
|
+
const level = {
|
|
97
|
+
id: 'level_move_lock',
|
|
98
|
+
type: 'level',
|
|
99
|
+
parentId: null,
|
|
100
|
+
children: ['move_lock_locked', 'move_lock_free'],
|
|
101
|
+
} as unknown as AnyNode
|
|
102
|
+
const locked = node(true)
|
|
103
|
+
const free = node(false)
|
|
104
|
+
useScene.setState({
|
|
105
|
+
nodes: { [level.id]: level, [locked.id]: locked, [free.id]: free } as never,
|
|
106
|
+
rootNodeIds: [level.id as AnyNodeId],
|
|
107
|
+
})
|
|
108
|
+
expect(isNodeDuplicable(locked)).toBe(false)
|
|
109
|
+
expect(isNodeDuplicable(free)).toBe(true)
|
|
110
|
+
duplicateNodeAndPickUp(locked)
|
|
111
|
+
expect(Object.keys(useScene.getState().nodes)).toHaveLength(3)
|
|
112
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
113
|
+
expect(copySelectedNodesToEditorClipboard([locked.id as AnyNodeId])).toBe(false)
|
|
114
|
+
expect(copySelectedNodesToEditorClipboard([free.id as AnyNodeId])).toBe(true)
|
|
115
|
+
})
|
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
cloneNodesInto,
|
|
5
5
|
collectSubtree,
|
|
6
6
|
isMovable,
|
|
7
|
+
isMoveLocked,
|
|
7
8
|
nodeRegistry,
|
|
8
9
|
useScene,
|
|
9
10
|
} from '@pascal-app/core'
|
|
10
11
|
import { copyCreateOps } from './fresh-planar-placement'
|
|
11
12
|
|
|
12
13
|
export function registryMoveDisabled(node: AnyNode): boolean {
|
|
14
|
+
if (isMoveLocked(node)) return true
|
|
13
15
|
const def = nodeRegistry.get(node.type)
|
|
14
16
|
return Boolean(
|
|
15
17
|
def?.capabilities.movable &&
|