@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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { CLOSE_UP_NEAR, ORBIT_NEAR, orbitNearPlane } from './orbit-near-plane'
|
|
3
|
+
|
|
4
|
+
describe('the orbit near plane follows the eye in', () => {
|
|
5
|
+
test("standing back it is the viewer's own; close in, a tenth of the distance; never under an inch-off look", () => {
|
|
6
|
+
expect(orbitNearPlane(40)).toBe(ORBIT_NEAR)
|
|
7
|
+
expect(orbitNearPlane(3)).toBe(ORBIT_NEAR)
|
|
8
|
+
expect(orbitNearPlane(1)).toBeCloseTo(0.1, 9)
|
|
9
|
+
expect(orbitNearPlane(0.5)).toBeCloseTo(0.05, 9)
|
|
10
|
+
expect(orbitNearPlane(0.01)).toBe(CLOSE_UP_NEAR)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The viewer's near plane (`viewerCameraClipping`), kept while the eye stands back. */
|
|
2
|
+
export const ORBIT_NEAR = 0.3
|
|
3
|
+
/** How close the near plane comes in when the eye does: a casing seen from an inch off. */
|
|
4
|
+
export const CLOSE_UP_NEAR = 0.01
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The near plane for an orbit `distance` from its target: a tenth of it, between `CLOSE_UP_NEAR`
|
|
8
|
+
* and the viewer's own, so zooming in on a window's trim never clips it while the eye standing
|
|
9
|
+
* back keeps the depth precision it had.
|
|
10
|
+
*/
|
|
11
|
+
export function orbitNearPlane(distance: number): number {
|
|
12
|
+
return Math.min(ORBIT_NEAR, Math.max(CLOSE_UP_NEAR, distance * 0.1))
|
|
13
|
+
}
|
|
@@ -136,10 +136,11 @@ describe('paint region sub-mode', () => {
|
|
|
136
136
|
})
|
|
137
137
|
|
|
138
138
|
test('off anything it draws on, the HUD says where to go: a floor for Polygon', () => {
|
|
139
|
-
expect(
|
|
140
|
-
'
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
expect(
|
|
140
|
+
paintRegionHints('polygon', null, false).map((hint) =>
|
|
141
|
+
typeof hint.label === 'string' ? hint.label : hint.label.message,
|
|
142
|
+
),
|
|
143
|
+
).toEqual(['Hover a floor or ceiling', 'Cancel'])
|
|
143
144
|
expect(paintRegionHints('rectangle', null, false)[0]).toMatchObject({
|
|
144
145
|
label: 'Hover a wall, floor or ceiling',
|
|
145
146
|
active: true,
|
|
@@ -158,7 +159,9 @@ describe('paint region sub-mode', () => {
|
|
|
158
159
|
|
|
159
160
|
test('the whole-surface HUD says what a click does; only painting teaches the held eyedropper', () => {
|
|
160
161
|
const labels = (mode: Parameters<typeof paintHints>[0]) =>
|
|
161
|
-
paintHints(mode).map((hint) =>
|
|
162
|
+
paintHints(mode).map((hint) =>
|
|
163
|
+
typeof hint.label === 'string' ? hint.label : (hint.label.message ?? ''),
|
|
164
|
+
)
|
|
162
165
|
expect(labels('surface')).toEqual(['Hold to pick a material'])
|
|
163
166
|
expect(labels('erase')).toEqual(['Remove a painted part, or reset a surface'])
|
|
164
167
|
expect(labels('erase').some((label) => /pick|colour|color|paint with/i.test(label))).toBe(false)
|
|
@@ -790,3 +790,56 @@ describe('generated floor plates: scopes per surface', () => {
|
|
|
790
790
|
expect(paintScopeRole(manual, 'surface', 'room')).toBe('surface')
|
|
791
791
|
})
|
|
792
792
|
})
|
|
793
|
+
|
|
794
|
+
describe('a surface that declares its own scopes', () => {
|
|
795
|
+
it('offers its scopes and labels and paints the role it names for the scope', () => {
|
|
796
|
+
const kind = 'paint-surface-scope-test'
|
|
797
|
+
if (!nodeRegistry.has(kind))
|
|
798
|
+
registerNode({
|
|
799
|
+
kind,
|
|
800
|
+
schemaVersion: 1,
|
|
801
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
802
|
+
category: 'structure',
|
|
803
|
+
defaults: () => ({}),
|
|
804
|
+
capabilities: {
|
|
805
|
+
slots: () => [{ slotId: 'end:1', label: 'End' }],
|
|
806
|
+
paint: {
|
|
807
|
+
resolveRole: () => null,
|
|
808
|
+
buildPatch: () => ({}),
|
|
809
|
+
applyPreview: () => null,
|
|
810
|
+
roleLabel: (_node: AnyNode, role: string) => (role === 'end:1' ? 'This end' : null),
|
|
811
|
+
scopes: (_node: AnyNode, role: string) =>
|
|
812
|
+
role === 'end:1'
|
|
813
|
+
? {
|
|
814
|
+
scopes: ['single', 'matching'],
|
|
815
|
+
labels: { matching: 'Every end' },
|
|
816
|
+
roleFor: (scope: PaintScope) => (scope === 'matching' ? 'ends:all' : role),
|
|
817
|
+
}
|
|
818
|
+
: null,
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
floorplanScope: 'level',
|
|
822
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
823
|
+
} as unknown as AnyNodeDefinition)
|
|
824
|
+
const node = { id: 'scoped_1', type: kind } as unknown as AnyNode
|
|
825
|
+
const hover = paintHoverInfo(node, 'end:1', [])
|
|
826
|
+
expect(hover.scopes).toEqual(['single', 'matching'])
|
|
827
|
+
expect(paintScopeLabel('single', hover)).toBe('This end')
|
|
828
|
+
expect(paintScopeLabel('matching', hover)).toBe('Every end')
|
|
829
|
+
const targets = (scope: PaintScope) =>
|
|
830
|
+
resolvePaintScopeTargets({
|
|
831
|
+
node,
|
|
832
|
+
role: 'end:1',
|
|
833
|
+
scope,
|
|
834
|
+
nodes: { scoped_1: node },
|
|
835
|
+
spaces: {},
|
|
836
|
+
slotRolesOf: () => [],
|
|
837
|
+
}).map((target) => `${target.nodeId}:${target.role}`)
|
|
838
|
+
expect(targets('single')).toEqual(['scoped_1:end:1'])
|
|
839
|
+
expect(targets('matching')).toEqual(['scoped_1:ends:all'])
|
|
840
|
+
// a scope the surface does not offer paints the surface alone
|
|
841
|
+
expect(targets('room')).toEqual(['scoped_1:end:1'])
|
|
842
|
+
// other roles of the kind keep the derived set
|
|
843
|
+
expect(availablePaintScopes({ node, slotRoles: [], role: 'side' })).toEqual(['single'])
|
|
844
|
+
})
|
|
845
|
+
})
|
package/src/lib/paint-scope.ts
CHANGED
|
@@ -55,6 +55,11 @@ function nodeOffersRoomScope(node: AnyNode): boolean {
|
|
|
55
55
|
return nodeRegistry.get(node.type)?.capabilities?.paint?.roomScope === true
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** The scopes the hovered surface declares for itself (`capabilities.paint.scopes`), if any. */
|
|
59
|
+
function surfaceScopes(node: AnyNode, role: string) {
|
|
60
|
+
return nodeRegistry.get(node.type)?.capabilities?.paint?.scopes?.(node, role) ?? null
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
const isFloorPlate = (node: AnyNode): boolean => node.type === 'slab' && node.boundary === 'auto'
|
|
59
64
|
|
|
60
65
|
const FOOTPRINT_FACE_LABELS: Record<string, string> = {
|
|
@@ -104,6 +109,8 @@ export function availablePaintScopes(args: {
|
|
|
104
109
|
if (args.node.type === 'wall') return ['single', 'room']
|
|
105
110
|
const plate = args.role === undefined ? null : platePaintScopes(args.node, args.role)
|
|
106
111
|
if (plate) return plate.scopes
|
|
112
|
+
const own = args.role === undefined ? null : surfaceScopes(args.node, args.role)
|
|
113
|
+
if (own) return own.scopes
|
|
107
114
|
const scopes: PaintScope[] = ['single']
|
|
108
115
|
if (args.slotRoles.length > 1) scopes.push('object')
|
|
109
116
|
if (nodeHasAsset(args.node)) scopes.push('matching')
|
|
@@ -123,7 +130,7 @@ export function paintHoverInfo(node: AnyNode, role: string, slotRoles: string[])
|
|
|
123
130
|
scopes: availablePaintScopes({ node, slotRoles, role }),
|
|
124
131
|
slotLabel: paintSurfaceLabel(node, role),
|
|
125
132
|
nodeNoun: node.type,
|
|
126
|
-
labels: platePaintScopes(node, role)?.labels,
|
|
133
|
+
labels: platePaintScopes(node, role)?.labels ?? surfaceScopes(node, role)?.labels,
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
|
|
@@ -168,12 +175,13 @@ export function paintSurfaceLabel(node: AnyNode, role: string): string {
|
|
|
168
175
|
return slotDisplayLabel(node, role)
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
/** Display label for the hovered slot
|
|
178
|
+
/** Display label for the hovered slot, with a capability override for contextual copy. */
|
|
172
179
|
export function slotDisplayLabel(node: AnyNode, role: string): string {
|
|
173
180
|
const capabilities = nodeRegistry.get(node.type)?.capabilities
|
|
181
|
+
const roleLabel = capabilities?.paint?.roleLabel?.(node, role)
|
|
182
|
+
if (roleLabel) return roleLabel
|
|
174
183
|
const declared = capabilities?.slots?.(node)?.find((slot) => slot.slotId === role)
|
|
175
|
-
|
|
176
|
-
return capabilities?.paint?.roleLabel?.(node, role) ?? slotLabelFromId(role)
|
|
184
|
+
return declared?.label ?? slotLabelFromId(role)
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
// ── Fan-out resolution ──────────────────────────────────────────────────────
|
|
@@ -506,6 +514,12 @@ export function resolvePaintScopeTargets(args: {
|
|
|
506
514
|
.map((other) => ({ nodeId: other.id, role: stepRole }))
|
|
507
515
|
return targets.length ? targets : [{ nodeId: node.id as AnyNodeId, role: stepRole }]
|
|
508
516
|
}
|
|
517
|
+
// A surface with its own scope set paints the role it names for the scope.
|
|
518
|
+
const own = surfaceScopes(node, role)
|
|
519
|
+
if (own) {
|
|
520
|
+
const roleFor = own.roleFor?.(effectivePaintScope(scope, own.scopes)) ?? role
|
|
521
|
+
return [{ nodeId: node.id as AnyNodeId, role: roleFor }]
|
|
522
|
+
}
|
|
509
523
|
if (scope === 'single') return single
|
|
510
524
|
|
|
511
525
|
// Whole object: paint every slot of the clicked node. Generic across any
|
package/src/lib/panel-rows.ts
CHANGED
|
@@ -65,19 +65,28 @@ export const PANEL_MODEL_EXTENSION = 'pascal:editor/panel-model'
|
|
|
65
65
|
/**
|
|
66
66
|
* A control the multi-selection inspector renders for every selected node of
|
|
67
67
|
* a kind at once (derived settings the raw parametrics can't express). `value`
|
|
68
|
-
* null means the selection is mixed: no option shows as active.
|
|
68
|
+
* null means the selection is mixed: no option shows as active. `segmented`
|
|
69
|
+
* suits a few short options, `select` a longer list; `readout` only shows a
|
|
70
|
+
* value the selection cannot edit here.
|
|
69
71
|
*/
|
|
70
72
|
export type MultiPanelControl = {
|
|
71
73
|
id: string
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The section the control sits in: a parametrics group title, or a section
|
|
76
|
+
* of its own, shown after the groups in the order controls name them.
|
|
77
|
+
*/
|
|
74
78
|
section: string
|
|
75
79
|
label: string
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
} & (
|
|
81
|
+
| {
|
|
82
|
+
kind: 'segmented' | 'select'
|
|
83
|
+
options: ReadonlyArray<{ value: string; label: string }>
|
|
84
|
+
value: string | null
|
|
85
|
+
/** Applies to every selected node in one undo step. */
|
|
86
|
+
onChange: (value: string) => void
|
|
87
|
+
}
|
|
88
|
+
| { kind: 'readout'; value: string; detail?: string }
|
|
89
|
+
)
|
|
81
90
|
|
|
82
91
|
export type NodePanelModel<N extends AnyNode = AnyNode> = {
|
|
83
92
|
rows: (context: {
|
|
@@ -17,6 +17,10 @@ export function isFreshPlacementMetadata(metadata: unknown): boolean {
|
|
|
17
17
|
return getPlacementMetadataRecord(metadata).isNew === true
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function stripPlacementMetadataFlags(
|
|
21
|
+
metadata: Record<string, unknown>,
|
|
22
|
+
): Record<string, unknown>
|
|
23
|
+
export function stripPlacementMetadataFlags(metadata: unknown): unknown
|
|
20
24
|
export function stripPlacementMetadataFlags(metadata: unknown): unknown {
|
|
21
25
|
if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
|
|
22
26
|
return metadata
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
area,
|
|
4
|
+
formatLinearMeasurement,
|
|
5
|
+
getAreaUnitLabel,
|
|
4
6
|
getScaledDimensions,
|
|
5
7
|
getWallCurveLength,
|
|
6
8
|
getWallEffectiveHeightForNodes,
|
|
7
9
|
getWallThickness,
|
|
8
10
|
type ItemNode,
|
|
11
|
+
type LinearUnit,
|
|
12
|
+
type MetricNotation,
|
|
9
13
|
resolveLevelId,
|
|
10
14
|
type SlabNode,
|
|
11
15
|
type StairNode,
|
|
16
|
+
squareMetersToAreaUnit,
|
|
12
17
|
type WallNode,
|
|
13
18
|
type WindowNode,
|
|
14
19
|
type ZoneNode,
|
|
15
20
|
} from '@pascal-app/core'
|
|
16
|
-
import {
|
|
17
|
-
formatLinearMeasurement,
|
|
18
|
-
getAreaUnitLabel,
|
|
19
|
-
type LinearUnit,
|
|
20
|
-
type MetricNotation,
|
|
21
|
-
squareMetersToAreaUnit,
|
|
22
|
-
} from '../measurements'
|
|
23
21
|
import type { PointKind, PointTarget } from './types'
|
|
24
22
|
|
|
25
23
|
// Point and ask names what the finger is on (the owner, 8 October): the label chip and the
|
|
@@ -115,7 +113,7 @@ const roomOfWall = (nodes: Nodes, levelId: string, wallId: string) =>
|
|
|
115
113
|
|
|
116
114
|
function wallHeight(wall: WallNode, nodes: Nodes): number {
|
|
117
115
|
try {
|
|
118
|
-
const height = getWallEffectiveHeightForNodes(wall, nodes as Record<string, AnyNode
|
|
116
|
+
const height = getWallEffectiveHeightForNodes(wall, nodes as Record<string, AnyNode>, 0)
|
|
119
117
|
if (Number.isFinite(height) && height > 0) return height
|
|
120
118
|
} catch {
|
|
121
119
|
// No slab election to consult in a bare scene: the wall's own height stands.
|
|
@@ -148,8 +148,19 @@ function preparedWallHeight(
|
|
|
148
148
|
},
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
if (Math.abs(node.endHeightOffset ?? 0) > 1e-5) {
|
|
152
|
+
return {
|
|
153
|
+
height: null,
|
|
154
|
+
diagnostic: {
|
|
155
|
+
severity: 'error',
|
|
156
|
+
code: 'unsupported_wall_print_slope',
|
|
157
|
+
message: `Wall ${node.id} has a sloped top that does not yet have a canonical printable solid.`,
|
|
158
|
+
nodeIds: [node.id],
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
151
162
|
|
|
152
|
-
const height = getWallEffectiveHeightForNodes(node, nodes)
|
|
163
|
+
const height = getWallEffectiveHeightForNodes(node, nodes, 0)
|
|
153
164
|
if (!Number.isFinite(height) || height <= 1e-7) {
|
|
154
165
|
return {
|
|
155
166
|
height: null,
|
|
@@ -155,6 +155,11 @@ describe('buildPrintableWallSolids', () => {
|
|
|
155
155
|
{ effectiveHeight: 2.5 },
|
|
156
156
|
{ [shaped.id]: shaped },
|
|
157
157
|
)
|
|
158
|
+
const sloped = buildPrintableWallSolids(
|
|
159
|
+
{ ...wall, endHeightOffset: 1.0 },
|
|
160
|
+
{ effectiveHeight: 2.5 },
|
|
161
|
+
{ [shaped.id]: shaped },
|
|
162
|
+
)
|
|
158
163
|
const terrain = buildPrintableWallSolids(
|
|
159
164
|
{ ...wall, children: [], fillToTerrain: true },
|
|
160
165
|
{ effectiveHeight: 2.5 },
|
|
@@ -172,6 +177,12 @@ describe('buildPrintableWallSolids', () => {
|
|
|
172
177
|
diagnostics: [expect.objectContaining({ code: 'unsupported_wall_print_curve' })],
|
|
173
178
|
}),
|
|
174
179
|
)
|
|
180
|
+
expect(sloped).toEqual(
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
status: 'blocked',
|
|
183
|
+
diagnostics: [expect.objectContaining({ code: 'unsupported_wall_print_slope' })],
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
175
186
|
expect(terrain).toEqual(
|
|
176
187
|
expect.objectContaining({
|
|
177
188
|
status: 'blocked',
|
|
@@ -27,6 +27,7 @@ export type PrintWallSolidDiagnostic = {
|
|
|
27
27
|
code:
|
|
28
28
|
| 'invalid_wall_print_dimensions'
|
|
29
29
|
| 'unsupported_wall_print_curve'
|
|
30
|
+
| 'unsupported_wall_print_slope'
|
|
30
31
|
| 'unsupported_wall_print_terrain'
|
|
31
32
|
| 'unsupported_wall_print_opening_shape'
|
|
32
33
|
| 'invalid_wall_print_opening'
|
|
@@ -258,6 +259,14 @@ export function buildPrintableWallSolids(
|
|
|
258
259
|
nodeIds: [node.id],
|
|
259
260
|
})
|
|
260
261
|
}
|
|
262
|
+
if (Math.abs(node.endHeightOffset ?? 0) > DIMENSION_EPSILON) {
|
|
263
|
+
diagnostics.push({
|
|
264
|
+
severity: 'error',
|
|
265
|
+
code: 'unsupported_wall_print_slope',
|
|
266
|
+
message: `Sloped wall ${node.id} does not yet have a canonical printable solid.`,
|
|
267
|
+
nodeIds: [node.id],
|
|
268
|
+
})
|
|
269
|
+
}
|
|
261
270
|
if (node.fillToTerrain) {
|
|
262
271
|
diagnostics.push({
|
|
263
272
|
severity: 'error',
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { formatNumber, type GuideScaleReference } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export type ReferenceScaleUnit = 'meters' | 'centimeters' | 'feet' | 'inches'
|
|
4
|
+
|
|
5
|
+
const MAX_REFERENCE_SCALE_LABEL_LENGTH = 64
|
|
6
|
+
|
|
7
|
+
/** Mirrors `GuideScaleReference.displayLength`'s `.max()` in core. */
|
|
8
|
+
export const MAX_REFERENCE_SCALE_DISPLAY_LENGTH = 1_000_000
|
|
9
|
+
|
|
10
|
+
export function isValidReferenceScaleDisplayLength(value: number): boolean {
|
|
11
|
+
return Number.isFinite(value) && value > 0 && value <= MAX_REFERENCE_SCALE_DISPLAY_LENGTH
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const REFERENCE_SCALE_UNIT_LABELS: Record<ReferenceScaleUnit, string> = {
|
|
15
|
+
meters: 'm',
|
|
16
|
+
centimeters: 'cm',
|
|
17
|
+
feet: 'ft',
|
|
18
|
+
inches: 'in',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatScaleValue(value: number, unit: string, locale: string): string {
|
|
22
|
+
return `${formatNumber(value, { locale, max: 2 })} ${unit}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getReferenceScaleUnitLabel(unit: unknown): string | null {
|
|
26
|
+
if (typeof unit !== 'string' || !Object.hasOwn(REFERENCE_SCALE_UNIT_LABELS, unit)) return null
|
|
27
|
+
return REFERENCE_SCALE_UNIT_LABELS[unit as ReferenceScaleUnit]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseLegacyScaleLabel(label: string): { unit: string; value: number } | null {
|
|
31
|
+
// `\s` covers NBSP and narrow NBSP; the whitespace run and digit group are
|
|
32
|
+
// disjoint, so each repetition has exactly one way to match.
|
|
33
|
+
const match = label.trim().match(/^([+−-]?[\d.,]+(?:\s+[\d.,]+)*)\s*(m|cm|ft|in)$/i)
|
|
34
|
+
if (!match) return null
|
|
35
|
+
|
|
36
|
+
const compact = match[1]!.replace(/\s/g, '').replace('−', '-')
|
|
37
|
+
const lastDot = compact.lastIndexOf('.')
|
|
38
|
+
const lastComma = compact.lastIndexOf(',')
|
|
39
|
+
const separatorIndex = Math.max(lastDot, lastComma)
|
|
40
|
+
const trailingDigits = separatorIndex === -1 ? 0 : compact.length - separatorIndex - 1
|
|
41
|
+
const decimalSeparator =
|
|
42
|
+
separatorIndex !== -1 && trailingDigits > 0 && trailingDigits <= 2
|
|
43
|
+
? compact[separatorIndex]
|
|
44
|
+
: null
|
|
45
|
+
const normalized = decimalSeparator
|
|
46
|
+
? `${compact.slice(0, separatorIndex).replace(/[.,]/g, '')}.${compact
|
|
47
|
+
.slice(separatorIndex + 1)
|
|
48
|
+
.replace(/[.,]/g, '')}`
|
|
49
|
+
: compact.replace(/[.,]/g, '')
|
|
50
|
+
|
|
51
|
+
if (!/^[+-]?\d+(?:\.\d+)?$/.test(normalized)) return null
|
|
52
|
+
const value = Number(normalized)
|
|
53
|
+
return Number.isFinite(value) ? { unit: match[2]!.toLowerCase(), value } : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createGuideScaleReference(
|
|
57
|
+
reference: Omit<GuideScaleReference, 'displayLength' | 'displayUnit' | 'label'>,
|
|
58
|
+
displayLength: number,
|
|
59
|
+
displayUnit: ReferenceScaleUnit,
|
|
60
|
+
): GuideScaleReference {
|
|
61
|
+
const unitLabel = REFERENCE_SCALE_UNIT_LABELS[displayUnit]
|
|
62
|
+
const label = formatScaleValue(displayLength, unitLabel, 'en')
|
|
63
|
+
if (!isValidReferenceScaleDisplayLength(displayLength)) return { ...reference, label }
|
|
64
|
+
return { ...reference, displayLength, displayUnit, label }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatReferenceScaleLabel(reference: GuideScaleReference, locale: string): string {
|
|
68
|
+
const unitLabel = getReferenceScaleUnitLabel(reference.displayUnit)
|
|
69
|
+
if (
|
|
70
|
+
typeof reference.displayLength === 'number' &&
|
|
71
|
+
Number.isFinite(reference.displayLength) &&
|
|
72
|
+
unitLabel
|
|
73
|
+
) {
|
|
74
|
+
return formatScaleValue(reference.displayLength, unitLabel, locale)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const label: unknown = reference.label
|
|
78
|
+
if (typeof label !== 'string') return typeof label === 'number' ? String(label) : ''
|
|
79
|
+
if (label.length > MAX_REFERENCE_SCALE_LABEL_LENGTH) return label
|
|
80
|
+
|
|
81
|
+
const parsed = parseLegacyScaleLabel(label)
|
|
82
|
+
return parsed ? formatScaleValue(parsed.value, parsed.unit, locale) : label
|
|
83
|
+
}
|
|
@@ -77,7 +77,7 @@ export function resolveRoomAssemblyHeights(
|
|
|
77
77
|
const wall = nodes[id]
|
|
78
78
|
if (wall?.type !== 'wall') continue
|
|
79
79
|
const base = getWallBaseElevationForNodes(wall, sceneNodes)
|
|
80
|
-
wallTops.set(id, getWallTop(wall, levelId, sceneNodes, base))
|
|
80
|
+
wallTops.set(id, getWallTop(wall, levelId, sceneNodes, base, 0.5))
|
|
81
81
|
}
|
|
82
82
|
const ceilingNode = room.ceilingId ? nodes[room.ceilingId] : undefined
|
|
83
83
|
const ceiling =
|
|
@@ -3,10 +3,12 @@ import {
|
|
|
3
3
|
type AnyNode,
|
|
4
4
|
BuildingNode,
|
|
5
5
|
clearSceneHistory,
|
|
6
|
+
createI18n,
|
|
6
7
|
createZone,
|
|
7
8
|
generateId,
|
|
8
9
|
initSpaceDetectionSync,
|
|
9
10
|
LevelNode,
|
|
11
|
+
roomFloorElevation,
|
|
10
12
|
SlabNode,
|
|
11
13
|
type SlabNode as SlabNodeType,
|
|
12
14
|
useScene,
|
|
@@ -23,11 +25,7 @@ import {
|
|
|
23
25
|
setRoomRelativeFloorHeight,
|
|
24
26
|
stepRoomFloorElevation,
|
|
25
27
|
} from './room-construction-commands'
|
|
26
|
-
import {
|
|
27
|
-
commitRoomElevation,
|
|
28
|
-
ownFloorElevationBounds,
|
|
29
|
-
roomFloorElevation,
|
|
30
|
-
} from './room-handle-drag'
|
|
28
|
+
import { commitRoomElevation, ownFloorElevationBounds } from './room-handle-drag'
|
|
31
29
|
import { applyRoomPlan } from './room-structure-commands'
|
|
32
30
|
import { showRoomNotice, useRoomTransform } from './room-transform-session'
|
|
33
31
|
|
|
@@ -67,6 +65,7 @@ const scene = (...nodes: AnyNode[]): Record<string, AnyNode> =>
|
|
|
67
65
|
const living = zone('zone_living', 'Living', rect(0, 0, 4, 4))
|
|
68
66
|
const lanai = zone('zone_lanai', 'Lanai', rect(4, 0, 6, 4))
|
|
69
67
|
const keyedLanai = zone('zone_lanai', 'Lanai', rect(4, 0, 6, 4), 'floor_lanai')
|
|
68
|
+
const english = createI18n()
|
|
70
69
|
const length = (meters: number) => `${meters.toFixed(2)} m`
|
|
71
70
|
|
|
72
71
|
describe('Built on: when the picker shows and what it offers', () => {
|
|
@@ -116,7 +115,11 @@ describe('Built on: when the picker shows and what it offers', () => {
|
|
|
116
115
|
expect(model.shared).toBeNull()
|
|
117
116
|
expect(separateFloorOffset(nodes, keyedLanai.id)).toBeNull()
|
|
118
117
|
expect(
|
|
119
|
-
separateFloorSummary(
|
|
118
|
+
separateFloorSummary(
|
|
119
|
+
{ ...model, offset: separateFloorOffset(nodes, keyedLanai.id) },
|
|
120
|
+
length,
|
|
121
|
+
english,
|
|
122
|
+
),
|
|
120
123
|
).toBe('Lanai floor')
|
|
121
124
|
})
|
|
122
125
|
|
|
@@ -159,18 +162,22 @@ describe('Built on: the Floor row summary', () => {
|
|
|
159
162
|
const lanaiFloor = { name: 'Lanai floor' }
|
|
160
163
|
test('says how a separate floor sits against the shared floor', () => {
|
|
161
164
|
const shared = { name: 'Shared floor' }
|
|
162
|
-
expect(separateFloorSummary({ current: lanaiFloor, shared, offset: -0.15 }, length)).toBe(
|
|
163
|
-
'Lanai floor · 0.15 m below Shared floor',
|
|
164
|
-
)
|
|
165
165
|
expect(
|
|
166
|
-
separateFloorSummary({ current: lanaiFloor, shared
|
|
166
|
+
separateFloorSummary({ current: lanaiFloor, shared, offset: -0.15 }, length, english),
|
|
167
|
+
).toBe('Lanai floor · 0.15 m below Shared floor')
|
|
168
|
+
expect(
|
|
169
|
+
separateFloorSummary(
|
|
170
|
+
{ current: lanaiFloor, shared: { name: 'Cabin' }, offset: 0.3 },
|
|
171
|
+
length,
|
|
172
|
+
english,
|
|
173
|
+
),
|
|
167
174
|
).toBe('Lanai floor · 0.30 m above Cabin')
|
|
168
|
-
expect(
|
|
169
|
-
|
|
170
|
-
)
|
|
171
|
-
expect(
|
|
172
|
-
|
|
173
|
-
)
|
|
175
|
+
expect(
|
|
176
|
+
separateFloorSummary({ current: lanaiFloor, shared, offset: 0.002 }, length, english),
|
|
177
|
+
).toBe('Lanai floor · level with Shared floor')
|
|
178
|
+
expect(
|
|
179
|
+
separateFloorSummary({ current: lanaiFloor, shared: null, offset: null }, length, english),
|
|
180
|
+
).toBe('Lanai floor')
|
|
174
181
|
})
|
|
175
182
|
|
|
176
183
|
test("compares the room's walking surface with the shared-floor room beside it, not plate with plate", () => {
|
|
@@ -188,7 +195,11 @@ describe('Built on: the Floor row summary', () => {
|
|
|
188
195
|
expect(separateFloorOffset(nodes, keyedLanai.id)).toBeCloseTo(-0.3)
|
|
189
196
|
const model = roomBuiltOn(nodes, keyedLanai.id)!
|
|
190
197
|
expect(
|
|
191
|
-
separateFloorSummary(
|
|
198
|
+
separateFloorSummary(
|
|
199
|
+
{ ...model, offset: separateFloorOffset(nodes, keyedLanai.id) },
|
|
200
|
+
length,
|
|
201
|
+
english,
|
|
202
|
+
),
|
|
192
203
|
).toBe('Lanai floor · 0.30 m below Living floor')
|
|
193
204
|
// Nothing shared across a wall or open side: the shared plate's top.
|
|
194
205
|
expect(separateFloorOffset(scene(raised, keyedLanai, house, own), keyedLanai.id)).toBeCloseTo(
|
package/src/lib/room-built-on.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
roomFloorChoices as coreRoomFloorChoices,
|
|
4
|
+
levelFootprints,
|
|
5
|
+
type PascalI18n,
|
|
4
6
|
roomDrawnFloor,
|
|
5
7
|
type SlabNode,
|
|
6
8
|
type ZoneNode,
|
|
7
9
|
} from '@pascal-app/core'
|
|
8
|
-
import { levelFootprints } from './floor-footprints'
|
|
9
10
|
|
|
10
11
|
// "Built on": which floor a room stands on. By default a room is part of its
|
|
11
12
|
// connected footprint's shared floor (`floor.footprint` absent); a floor key
|
|
@@ -60,20 +61,6 @@ export function roomFloorChoices(nodes: Nodes, zoneId: string): PlateChoice[] {
|
|
|
60
61
|
.map(({ key, plateId, name }) => ({ key, plateId, name }))
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
/**
|
|
64
|
-
* The plate a room on a separate floor stands on alone: its height is that
|
|
65
|
-
* plate's floor height (nothing else stands on it). Null for a room on the
|
|
66
|
-
* shared floor or sharing its separate floor with other rooms.
|
|
67
|
-
*/
|
|
68
|
-
export function roomOwnPlate(nodes: Nodes, zoneId: string): SlabNode | null {
|
|
69
|
-
const zone = nodes[zoneId]
|
|
70
|
-
if (!isRoom(zone) || !zone.floor?.footprint || zone.floor.support === 'open') return null
|
|
71
|
-
const plate = listingPlate(nodes, zone)
|
|
72
|
-
if (!plate) return null
|
|
73
|
-
const alone = !(plate.zoneIds ?? []).some((id) => id !== zoneId && isGroundRoom(nodes[id]))
|
|
74
|
-
return alone ? plate : null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
64
|
/**
|
|
78
65
|
* The shared floor beside a room on a separate floor, where it goes back to.
|
|
79
66
|
* For a room on the shared floor, its own plate.
|
|
@@ -130,9 +117,26 @@ export function roomBuiltOn(nodes: Nodes, zoneId: string): RoomBuiltOnModel | nu
|
|
|
130
117
|
export function separateFloorSummary(
|
|
131
118
|
model: { current: { name: string }; shared: { name: string } | null; offset: number | null },
|
|
132
119
|
length: (meters: number) => string,
|
|
120
|
+
i18n: PascalI18n,
|
|
133
121
|
): string {
|
|
134
122
|
const { current, shared, offset } = model
|
|
135
123
|
if (!shared || offset === null) return current.name
|
|
136
|
-
|
|
137
|
-
|
|
124
|
+
const values = { current: current.name, shared: shared.name, distance: length(Math.abs(offset)) }
|
|
125
|
+
if (Math.abs(offset) < 0.005)
|
|
126
|
+
return i18n._({
|
|
127
|
+
id: 'editor.room.builtOn.levelWith',
|
|
128
|
+
message: '{current} · level with {shared}',
|
|
129
|
+
values,
|
|
130
|
+
})
|
|
131
|
+
return offset > 0
|
|
132
|
+
? i18n._({
|
|
133
|
+
id: 'editor.room.builtOn.above',
|
|
134
|
+
message: '{current} · {distance} above {shared}',
|
|
135
|
+
values,
|
|
136
|
+
})
|
|
137
|
+
: i18n._({
|
|
138
|
+
id: 'editor.room.builtOn.below',
|
|
139
|
+
message: '{current} · {distance} below {shared}',
|
|
140
|
+
values,
|
|
141
|
+
})
|
|
138
142
|
}
|
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
type NodeChange,
|
|
18
18
|
planWallJustification,
|
|
19
19
|
resolveCeilingHeight,
|
|
20
|
+
roomFloorElevation,
|
|
20
21
|
roomFloorElevationFromRelative,
|
|
22
|
+
roomOwnPlate,
|
|
21
23
|
roomSideFaces,
|
|
22
24
|
runAsSingleSceneHistoryStep,
|
|
23
25
|
type StructureConflict,
|
|
@@ -32,13 +34,12 @@ import useDeleteConfirmation, {
|
|
|
32
34
|
} from '../store/use-delete-confirmation'
|
|
33
35
|
import useEditor from '../store/use-editor'
|
|
34
36
|
import { footprintHeightValue } from './floor-footprints'
|
|
35
|
-
import {
|
|
37
|
+
import { roomSharedFootprint } from './room-built-on'
|
|
36
38
|
import { roomClearPolygon, roomConstructionState, roomFloorContents } from './room-construction'
|
|
37
39
|
import {
|
|
38
40
|
mezzanineElevationBounds,
|
|
39
41
|
ownFloorElevationBounds,
|
|
40
42
|
planRoomElevation,
|
|
41
|
-
roomFloorElevation,
|
|
42
43
|
} from './room-handle-drag'
|
|
43
44
|
import { applyRoomPlan } from './room-structure-commands'
|
|
44
45
|
import { showRoomNotice } from './room-transform-session'
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
area,
|
|
5
5
|
BuildingNode,
|
|
6
6
|
CeilingNode,
|
|
7
|
+
ColumnNode,
|
|
7
8
|
clearSceneHistory,
|
|
8
9
|
createZone,
|
|
9
10
|
DoorNode,
|
|
@@ -25,7 +26,12 @@ import { deleteConfirmationContent } from '../components/editor/delete-confirmat
|
|
|
25
26
|
import { constructionActions, wallsSwitch } from '../components/ui/panels/room-construction-rows'
|
|
26
27
|
import useDeleteConfirmation from '../store/use-delete-confirmation'
|
|
27
28
|
import useEditor from '../store/use-editor'
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
roomClearPolygon,
|
|
31
|
+
roomConstructionState,
|
|
32
|
+
roomFloorContents,
|
|
33
|
+
roomPorchFramingCounts,
|
|
34
|
+
} from './room-construction'
|
|
29
35
|
import {
|
|
30
36
|
addRoomConstruction,
|
|
31
37
|
adoptExistingCeiling,
|
|
@@ -177,6 +183,27 @@ describe('room construction state', () => {
|
|
|
177
183
|
expect(constructionActions(room, 'floor')).toEqual({ add: false, remove: true })
|
|
178
184
|
})
|
|
179
185
|
|
|
186
|
+
test('porch framing counts the room’s generated parts by marker, across node kinds', () => {
|
|
187
|
+
const zone = nodes()[zoneId as AnyNodeId] as ZoneNode
|
|
188
|
+
const parts = [
|
|
189
|
+
ColumnNode.parse({ metadata: { porchOf: zoneId, porchPart: 'corner:0' } }),
|
|
190
|
+
ItemNode.parse({
|
|
191
|
+
...item('item_side_post', 'Post', LEVEL, [0, 0, 0]),
|
|
192
|
+
metadata: { porchOf: zoneId, porchPart: 'side:0:1' },
|
|
193
|
+
}),
|
|
194
|
+
ColumnNode.parse({ metadata: { porchOf: zoneId, porchPart: 'beam:0' } }),
|
|
195
|
+
ColumnNode.parse({ metadata: { porchOf: 'zone_other', porchPart: 'corner:0' } }),
|
|
196
|
+
ColumnNode.parse({}),
|
|
197
|
+
]
|
|
198
|
+
const level = nodes()[LEVEL as AnyNodeId] as LevelNode
|
|
199
|
+
const scene = {
|
|
200
|
+
...nodes(),
|
|
201
|
+
...Object.fromEntries(parts.map((part) => [part.id, part])),
|
|
202
|
+
[level.id]: { ...level, children: [...level.children, ...parts.map((part) => part.id)] },
|
|
203
|
+
}
|
|
204
|
+
expect(roomPorchFramingCounts(scene, zone)).toEqual({ posts: 2, beams: 1 })
|
|
205
|
+
})
|
|
206
|
+
|
|
180
207
|
test('memoized per nodes identity', () => {
|
|
181
208
|
expect(roomConstructionState(nodes(), zoneId)).toBe(roomConstructionState(nodes(), zoneId))
|
|
182
209
|
expect(roomConstructionState(nodes(), 'zone_missing')).toBeNull()
|