@pascal-app/editor 1.0.7 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +101 -45
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1983
- 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 +5 -3
- 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 +8 -4
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +199 -30
- 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/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +43 -13
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +2 -2
- 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/room-controls.tsx +10 -6
- package/src/components/editor/room-handles-2d.tsx +3 -3
- package/src/components/editor/room-handles.tsx +4 -5
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +41 -1670
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +11 -7
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
- 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 +30 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +33 -21
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
- 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/use-placement-coordinator.tsx +3 -4
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- 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-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
- package/src/components/ui/helpers/helper-manager.tsx +55 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
- package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +42 -19
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +7 -7
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +7 -9
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +22 -11
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +1036 -306
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2267 -439
- package/src/i18n/locales/fr/messages.po +2267 -439
- package/src/i18n/locales/ja/messages.po +2267 -439
- package/src/i18n/locales/zh-CN/messages.po +2267 -439
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +183 -178
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -3
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +2 -2
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- 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/material-paint.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +7 -5
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/paint-region-mode.ts +1 -1
- 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/point-ask/session.ts +1 -2
- 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/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +8 -37
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +22 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- 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/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/measurements.ts +0 -225
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -183
- package/src/lib/scene-bounds.ts +0 -169
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/window-interaction.ts +0 -142
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,68 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
3
|
-
import type { FloorplanLineSegment, FloorplanSelectionBounds } from './types'
|
|
4
|
-
|
|
5
|
-
// Baseline rotation (deg) that orients the plan-local scene "north up" on
|
|
6
|
-
// screen. The on-screen floor-plan scene `<g>` is rotated by
|
|
7
|
-
// `FLOORPLAN_VIEW_ROTATION_DEG + userRotation - buildingRotation`; PDF export
|
|
8
|
-
// uses the same user rotation so its orientation matches the live plan.
|
|
9
|
-
//
|
|
10
|
-
// North is world −Z: with a 0 baseline a rotation-0 reference image (top =
|
|
11
|
-
// −Z) reads upright in the north-aligned view, and "align north" maps to a
|
|
12
|
-
// 3D camera azimuth of 0. The old 90° baseline made north world −X, so
|
|
13
|
-
// north-up floorplan scans displayed sideways whenever the compass claimed
|
|
14
|
-
// the view was aligned.
|
|
15
|
-
export const FLOORPLAN_VIEW_ROTATION_DEG = 0
|
|
16
|
-
|
|
17
|
-
export function clampPlanValue(value: number, min: number, max: number) {
|
|
18
|
-
return Math.min(Math.max(value, min), max)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function rotatePlanVector(x: number, y: number, rotation: number): [number, number] {
|
|
22
|
-
const cos = Math.cos(rotation)
|
|
23
|
-
const sin = Math.sin(rotation)
|
|
24
|
-
return [x * cos + y * sin, -x * sin + y * cos]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Converts a world X/Z point into the floor-plan-local (building-local)
|
|
28
|
-
// frame used by the SVG scene `<g>` and every stored node position. The
|
|
29
|
-
// inverse of `floorplanLocalToWorldPoint`. Shared so the floor-plan panel
|
|
30
|
-
// and the 2D move overlay resolve the same frame — feeding a world-space
|
|
31
|
-
// `original` into a local-space cursor solver lands the drop off by the
|
|
32
|
-
// building's world X/Z (worse for an off-origin building).
|
|
33
|
-
export function worldToFloorplanLocalPoint(
|
|
34
|
-
worldX: number,
|
|
35
|
-
worldZ: number,
|
|
36
|
-
buildingPosition: readonly [number, number, number],
|
|
37
|
-
buildingRotationY: number,
|
|
38
|
-
): Point2D {
|
|
39
|
-
const dx = worldX - buildingPosition[0]
|
|
40
|
-
const dz = worldZ - buildingPosition[2]
|
|
41
|
-
const cos = Math.cos(buildingRotationY)
|
|
42
|
-
const sin = Math.sin(buildingRotationY)
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
x: dx * cos - dz * sin,
|
|
46
|
-
y: dx * sin + dz * cos,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Inverse of `worldToFloorplanLocalPoint`: floor-plan-local X/Y → world X/Z.
|
|
51
|
-
export function floorplanLocalToWorldPoint(
|
|
52
|
-
point: Point2D | [number, number],
|
|
53
|
-
buildingPosition: readonly [number, number, number],
|
|
54
|
-
buildingRotationY: number,
|
|
55
|
-
): { x: number; z: number } {
|
|
56
|
-
const localX = Array.isArray(point) ? point[0] : point.x
|
|
57
|
-
const localY = Array.isArray(point) ? point[1] : point.y
|
|
58
|
-
const cos = Math.cos(buildingRotationY)
|
|
59
|
-
const sin = Math.sin(buildingRotationY)
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
x: buildingPosition[0] + localX * cos + localY * sin,
|
|
63
|
-
z: buildingPosition[2] - localX * sin + localY * cos,
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
import { clampPlanValue, type Point2D, rotatePlanVector } from '@pascal-app/core'
|
|
2
|
+
import type { FloorplanSelectionBounds } from './types'
|
|
66
3
|
|
|
67
4
|
export function getRotatedRectanglePolygon(
|
|
68
5
|
center: Point2D,
|
|
@@ -88,53 +25,6 @@ export function getRotatedRectanglePolygon(
|
|
|
88
25
|
})
|
|
89
26
|
}
|
|
90
27
|
|
|
91
|
-
export function interpolatePlanPoint(start: Point2D, end: Point2D, t: number): Point2D {
|
|
92
|
-
return {
|
|
93
|
-
x: start.x + (end.x - start.x) * t,
|
|
94
|
-
y: start.y + (end.y - start.y) * t,
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function getPlanPointDistance(start: Point2D, end: Point2D): number {
|
|
99
|
-
return Math.hypot(end.x - start.x, end.y - start.y)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function movePlanPointTowards(start: Point2D, end: Point2D, distance: number): Point2D {
|
|
103
|
-
const totalDistance = getPlanPointDistance(start, end)
|
|
104
|
-
if (totalDistance <= Number.EPSILON || distance <= 0) {
|
|
105
|
-
return start
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return interpolatePlanPoint(start, end, Math.min(1, distance / totalDistance))
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function getThickPlanLinePolygon(
|
|
112
|
-
line: FloorplanLineSegment,
|
|
113
|
-
thickness: number,
|
|
114
|
-
justification?: WallJustification,
|
|
115
|
-
): Point2D[] {
|
|
116
|
-
const dx = line.end.x - line.start.x
|
|
117
|
-
const dy = line.end.y - line.start.y
|
|
118
|
-
const length = Math.hypot(dx, dy)
|
|
119
|
-
|
|
120
|
-
if (length <= Number.EPSILON || thickness <= 0) {
|
|
121
|
-
return [line.start, line.end, line.end, line.start]
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const { a, b } = getWallFaceOffsets({ thickness, justification })
|
|
125
|
-
const normalX = (-dy / length) * a
|
|
126
|
-
const normalY = (dx / length) * a
|
|
127
|
-
|
|
128
|
-
const backX = (-dy / length) * -b
|
|
129
|
-
const backY = (dx / length) * -b
|
|
130
|
-
return [
|
|
131
|
-
{ x: line.start.x + normalX, y: line.start.y + normalY },
|
|
132
|
-
{ x: line.end.x + normalX, y: line.end.y + normalY },
|
|
133
|
-
{ x: line.end.x - backX, y: line.end.y - backY },
|
|
134
|
-
{ x: line.start.x - backX, y: line.start.y - backY },
|
|
135
|
-
]
|
|
136
|
-
}
|
|
137
|
-
|
|
138
28
|
export function getFloorplanSelectionBounds(
|
|
139
29
|
start: [number, number],
|
|
140
30
|
end: [number, number],
|
|
@@ -6,23 +6,14 @@ export {
|
|
|
6
6
|
type FloorplanAlignmentResult,
|
|
7
7
|
} from './apply-alignment'
|
|
8
8
|
export {
|
|
9
|
-
clampPlanValue,
|
|
10
9
|
doesPolygonIntersectSelectionBounds,
|
|
11
|
-
FLOORPLAN_VIEW_ROTATION_DEG,
|
|
12
|
-
floorplanLocalToWorldPoint,
|
|
13
10
|
getDistanceToWallSegment,
|
|
14
11
|
getFloorplanSelectionBounds,
|
|
15
|
-
getPlanPointDistance,
|
|
16
12
|
getRotatedRectanglePolygon,
|
|
17
|
-
getThickPlanLinePolygon,
|
|
18
|
-
interpolatePlanPoint,
|
|
19
13
|
isPointInsidePolygon,
|
|
20
14
|
isPointInsidePolygonWithHoles,
|
|
21
15
|
isPointInsideSelectionBounds,
|
|
22
|
-
movePlanPointTowards,
|
|
23
16
|
pointMatchesWallPlanPoint,
|
|
24
|
-
rotatePlanVector,
|
|
25
|
-
worldToFloorplanLocalPoint,
|
|
26
17
|
} from './geometry'
|
|
27
18
|
export {
|
|
28
19
|
buildFloorplanItemEntry,
|
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
getWallLocalFaceZ,
|
|
7
7
|
type ItemNode,
|
|
8
8
|
type LevelNode,
|
|
9
|
+
rotatePlanVector,
|
|
9
10
|
useLiveTransforms,
|
|
10
11
|
} from '@pascal-app/core'
|
|
11
|
-
import { getRotatedRectanglePolygon
|
|
12
|
+
import { getRotatedRectanglePolygon } from './geometry'
|
|
12
13
|
import type { FloorplanItemEntry, FloorplanNodeTransform, LevelDescendantMap } from './types'
|
|
13
14
|
|
|
14
15
|
export function collectLevelDescendants(
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
formatLinearMeasurement,
|
|
3
|
+
type LinearUnit,
|
|
2
4
|
type OpenWallEnd,
|
|
3
5
|
planJoinOpenWallEnd,
|
|
4
6
|
runAsSingleSceneHistoryStep,
|
|
5
7
|
useScene,
|
|
6
8
|
type WallJoinResult,
|
|
7
9
|
} from '@pascal-app/core'
|
|
8
|
-
import { formatLinearMeasurement, type LinearUnit } from '../measurements'
|
|
9
10
|
|
|
10
11
|
const JOIN_REFUSALS: Record<Extract<WallJoinResult, { ok: false }>['reason'], string> = {
|
|
11
12
|
'covered-existing-wall': 'An existing wall already covers this span',
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { AnyNode, AnyNodeId, DoorNode, WallNode, WindowNode, ZoneNode } from '@pascal-app/core'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
deriveZoneQuantityReport,
|
|
4
|
+
type FloorplanSchedule,
|
|
5
|
+
resolveHostedOpeningFrameDepth,
|
|
6
|
+
resolveMarkDetail,
|
|
7
|
+
} from '@pascal-app/core'
|
|
3
8
|
import { collectFloorplanSchedules } from './floorplan-export'
|
|
4
|
-
import type { FloorplanSchedule } from './floorplan-extension'
|
|
5
|
-
import { resolveMarkDetail } from './marks'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Construction-document schedule DATA — WS3.
|
|
@@ -30,7 +30,11 @@ import type {
|
|
|
30
30
|
FloorplanGeometry,
|
|
31
31
|
LiveNodeOverrides,
|
|
32
32
|
} from '@pascal-app/core'
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
type FloorplanMetricNotation,
|
|
35
|
+
nodeRegistry,
|
|
36
|
+
resolveBuildingForLevel,
|
|
37
|
+
} from '@pascal-app/core'
|
|
34
38
|
import {
|
|
35
39
|
buildContext,
|
|
36
40
|
collectFloorplanLinkedLevelNodes,
|
|
@@ -52,14 +56,12 @@ import {
|
|
|
52
56
|
resolveFloorplanExportRotationDeg,
|
|
53
57
|
resolveFloorplanExportViewState,
|
|
54
58
|
} from './floorplan-export'
|
|
55
|
-
import type { FloorplanMetricNotation } from './floorplan-extension'
|
|
56
59
|
import { createFloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
57
60
|
import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
|
|
58
61
|
|
|
59
62
|
/** PDF user-space units per inch. */
|
|
60
63
|
export const POINTS_PER_INCH = 72
|
|
61
64
|
|
|
62
|
-
export type { FloorplanSchedule } from './floorplan-extension'
|
|
63
65
|
/**
|
|
64
66
|
* The schedules the kinds on a level contribute
|
|
65
67
|
* (`def.extensions['pascal:editor/floorplan'].schedule`). Re-exported here so
|
|
@@ -316,3 +318,5 @@ async function drawPlate(
|
|
|
316
318
|
},
|
|
317
319
|
)
|
|
318
320
|
}
|
|
321
|
+
|
|
322
|
+
export type { FloorplanSchedule } from '@pascal-app/core'
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
isOutdoorRoomType,
|
|
16
16
|
type LevelNode,
|
|
17
17
|
migrateSiteMetadata,
|
|
18
|
+
type RoofNode,
|
|
18
19
|
type RoofSegmentNode,
|
|
20
|
+
roofFaceShapes,
|
|
19
21
|
type SceneSnapshot,
|
|
20
22
|
type SiteNode,
|
|
21
23
|
siteFrame,
|
|
@@ -75,7 +77,8 @@ export function findLowestLevel(
|
|
|
75
77
|
/**
|
|
76
78
|
* The roof's outline on the lot — every roof segment's plan rectangle out to
|
|
77
79
|
* its deck's reach at its eaves and rakes, through the segment's, the roof's
|
|
78
|
-
* and the building's turns,
|
|
80
|
+
* and the building's turns, and every face of another kind (`capabilities.roofs`)
|
|
81
|
+
* by its own outline, unioned into the outer ring(s). Dashed on the site plan: what the eye sees
|
|
79
82
|
* from above is the roof, and the setback is measured to the wall under it.
|
|
80
83
|
*/
|
|
81
84
|
export function roofOutlineRings(
|
|
@@ -93,6 +96,9 @@ export function roofOutlineRings(
|
|
|
93
96
|
}
|
|
94
97
|
if (roofs.size === 0) return []
|
|
95
98
|
const rects: Pt[][] = []
|
|
99
|
+
for (const roof of roofs.values())
|
|
100
|
+
for (const face of roofFaceShapes(roof as RoofNode, scene.nodes))
|
|
101
|
+
rects.push(face.outline.map(([x, z]) => toSite(x, z)))
|
|
96
102
|
for (const node of Object.values(scene.nodes)) {
|
|
97
103
|
if (node.type !== 'roof-segment') continue
|
|
98
104
|
const roof = roofs.get(String((node as { parentId?: unknown }).parentId))
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
resumeSceneHistory,
|
|
8
8
|
useScene,
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
|
+
import { FloorplanGeometryRenderer } from '@pascal-app/viewer/ui'
|
|
10
11
|
import { type PointerEvent as ReactPointerEvent, useCallback, useMemo, useRef } from 'react'
|
|
11
|
-
import { FloorplanGeometryRenderer } from '../../../components/editor-2d/renderers/floorplan-geometry-renderer'
|
|
12
12
|
import { clientToPlan } from '../plan-coords'
|
|
13
13
|
import { buildSitePlanDrawing } from './build-site-plan-drawing'
|
|
14
14
|
|
|
@@ -33,7 +33,9 @@ export type GestureSpec = {
|
|
|
33
33
|
kind: string
|
|
34
34
|
/**
|
|
35
35
|
* Drops the gesture's own in-flight state (draft, preview, listeners,
|
|
36
|
-
* pending frames).
|
|
36
|
+
* pending frames). Runs once, on cancel only. Undo and redo cancel before
|
|
37
|
+
* they jump, so this must record no history: a gesture that previews
|
|
38
|
+
* through scene writes under paused history reverts them here, then resumes.
|
|
37
39
|
*/
|
|
38
40
|
onCancel: (reason: GestureCancelReason) => void
|
|
39
41
|
/** A scope the owner begins for the gesture and releases only while it is still this one. */
|
package/src/lib/glb-export.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type ViewerPresentationContribution,
|
|
33
33
|
viewerPresentationRegistry,
|
|
34
34
|
} from '@pascal-app/viewer'
|
|
35
|
+
import { nodeDisplayLabel } from '@pascal-app/viewer/ui'
|
|
35
36
|
import type { Object3D } from 'three'
|
|
36
37
|
import * as THREE from 'three'
|
|
37
38
|
import {
|
|
@@ -42,7 +43,6 @@ import {
|
|
|
42
43
|
import { createExportTextureUtils, type ExportTextureUtils } from './export-texture-utils'
|
|
43
44
|
import { cloneExportUserData } from './export-user-data'
|
|
44
45
|
import { isIfcRolePart } from './ifc-parts'
|
|
45
|
-
import { nodeDisplayLabel } from './node-display-label'
|
|
46
46
|
import {
|
|
47
47
|
type CompressedTextureDecompressor,
|
|
48
48
|
decompressCanonicalNormalMaps,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type AnyNode,
|
|
4
4
|
type AnyNodeId,
|
|
5
5
|
BuildingNode,
|
|
6
|
+
ElevatorNode,
|
|
6
7
|
LevelNode,
|
|
7
8
|
RoofNode,
|
|
8
9
|
useScene,
|
|
@@ -27,10 +28,14 @@ beforeEach(() => {
|
|
|
27
28
|
metadata: { role: 'roof' },
|
|
28
29
|
})
|
|
29
30
|
const roof = RoofNode.parse({ id: 'roof_main', parentId: roofLevel.id })
|
|
30
|
-
const
|
|
31
|
+
const elevator = ElevatorNode.parse({ id: 'elevator_lift', parentId: BUILDING })
|
|
32
|
+
const building = BuildingNode.parse({
|
|
33
|
+
id: BUILDING,
|
|
34
|
+
children: [GROUND, roofLevel.id, elevator.id],
|
|
35
|
+
})
|
|
31
36
|
useScene.setState({
|
|
32
37
|
nodes: Object.fromEntries(
|
|
33
|
-
[building, ground, wall, roofLevel, roof].map((node) => [node.id, node as AnyNode]),
|
|
38
|
+
[building, ground, wall, roofLevel, roof, elevator].map((node) => [node.id, node as AnyNode]),
|
|
34
39
|
),
|
|
35
40
|
rootNodeIds: [BUILDING],
|
|
36
41
|
} as never)
|
|
@@ -54,4 +59,15 @@ describe('selectPlacedNode', () => {
|
|
|
54
59
|
selectedIds: ['wall_a'],
|
|
55
60
|
})
|
|
56
61
|
})
|
|
62
|
+
|
|
63
|
+
test('a building-parented node keeps the floor in view and adopts its building', () => {
|
|
64
|
+
useViewer.getState().resetSelection()
|
|
65
|
+
useViewer.getState().setSelection({ levelId: GROUND })
|
|
66
|
+
selectPlacedNode('elevator_lift' as AnyNodeId)
|
|
67
|
+
expect(useViewer.getState().selection).toMatchObject({
|
|
68
|
+
buildingId: BUILDING,
|
|
69
|
+
levelId: GROUND,
|
|
70
|
+
selectedIds: ['elevator_lift'],
|
|
71
|
+
})
|
|
72
|
+
})
|
|
57
73
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
|
|
1
|
+
import type { AnyNode, AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
|
|
2
2
|
import { findLevelAncestorId, useScene } from '@pascal-app/core'
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
4
|
|
|
@@ -30,17 +30,40 @@ export function deleteLevelWithFallbackSelection(levelId: AnyNodeId) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function findBuildingAncestorId(
|
|
34
|
+
nodeId: AnyNodeId,
|
|
35
|
+
nodes: Record<string, AnyNode>,
|
|
36
|
+
): BuildingNode['id'] | null {
|
|
37
|
+
let current: AnyNode | undefined = nodes[nodeId]
|
|
38
|
+
for (let guard = 0; current && guard < 16; guard++) {
|
|
39
|
+
if (current.type === 'building') return current.id
|
|
40
|
+
current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* Selects a node a tool just made, on its own level when that is not the one in view: a roof drawn
|
|
35
47
|
* over a storey's walls lands on the level above, which the editor hides while the storey is
|
|
36
|
-
* selected, so selecting it there alone would leave it out of sight.
|
|
48
|
+
* selected, so selecting it there alone would leave it out of sight. A node parented to the
|
|
49
|
+
* building itself (an elevator) has no level, so the floor in view stays active when it belongs
|
|
50
|
+
* to that building.
|
|
37
51
|
*/
|
|
38
52
|
export function selectPlacedNode(nodeId: AnyNodeId) {
|
|
39
|
-
const
|
|
53
|
+
const { nodes } = useScene.getState()
|
|
54
|
+
const levelId = findLevelAncestorId(nodeId, nodes) as LevelNode['id'] | null
|
|
55
|
+
const buildingId = findBuildingAncestorId(nodeId, nodes)
|
|
40
56
|
const { selection, setSelection } = useViewer.getState()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
const updates: Partial<typeof selection> = { selectedIds: [nodeId] }
|
|
58
|
+
if (buildingId && buildingId !== selection.buildingId) {
|
|
59
|
+
updates.buildingId = buildingId
|
|
60
|
+
updates.levelId =
|
|
61
|
+
levelId ??
|
|
62
|
+
(selection.levelId && findBuildingAncestorId(selection.levelId, nodes) === buildingId
|
|
63
|
+
? selection.levelId
|
|
64
|
+
: null)
|
|
65
|
+
} else if (levelId && levelId !== selection.levelId) {
|
|
66
|
+
updates.levelId = levelId
|
|
67
|
+
}
|
|
68
|
+
setSelection(updates)
|
|
46
69
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createZoneDivisionContext,
|
|
7
7
|
generateId,
|
|
8
8
|
nodeRegistry,
|
|
9
|
+
roomFloorElevation,
|
|
9
10
|
type SlabNode,
|
|
10
11
|
useLiveNodeOverrides,
|
|
11
12
|
useScene,
|
|
@@ -29,7 +30,6 @@ import { roomRelativeFloorHeight, stepRoomFloorElevation } from './room-construc
|
|
|
29
30
|
import {
|
|
30
31
|
commitRoomElevation,
|
|
31
32
|
mezzanineEdgeHandles,
|
|
32
|
-
roomFloorElevation,
|
|
33
33
|
roomPushHandles,
|
|
34
34
|
runFloorplanMezzanineEdge,
|
|
35
35
|
runFloorplanRoomPush,
|
|
@@ -309,7 +309,7 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|
|
309
309
|
) {
|
|
310
310
|
const roofNode = selectedNode as RoofNode
|
|
311
311
|
const fallbackSegment = (roofNode.children ?? [])
|
|
312
|
-
.map((id
|
|
312
|
+
.map((id) => nodes[id as AnyNodeId] as RoofSegmentNode | undefined)
|
|
313
313
|
.find(
|
|
314
314
|
(segment: RoofSegmentNode | undefined) =>
|
|
315
315
|
segment?.type === 'roof-segment' &&
|
|
@@ -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,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,16 +10,19 @@ 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
20
|
MEASUREMENT_FLOORPLAN_COLOR,
|
|
17
21
|
MEASUREMENT_PERSISTENT_COLOR,
|
|
18
22
|
measurementFloorplanPresentationColor,
|
|
19
23
|
measurementPresentationColor,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
squareMetersToAreaUnit,
|
|
23
|
-
} from './measurements'
|
|
24
|
+
} from '@pascal-app/viewer'
|
|
25
|
+
import { measurementHint, parseMeasurement } from '../index'
|
|
24
26
|
|
|
25
27
|
describe('measurement presentation', () => {
|
|
26
28
|
test('uses black at rest, indigo while active, and red for dangling references', () => {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type ZoneNode,
|
|
16
16
|
} from '@pascal-app/core'
|
|
17
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
18
|
+
import { RoomSelectionIndex, resolveRoomHit } from '@pascal-app/viewer/ui'
|
|
18
19
|
import { deleteConfirmationContent } from '../components/editor/delete-confirmation-dialog'
|
|
19
20
|
import { resolveFloorRegionRoom } from '../components/editor/floor-region-controls'
|
|
20
21
|
import { MEZZANINE_GESTURE_HINTS, mezzanineGesture } from '../components/ui/helpers/helper-manager'
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
mezzanineElevationBounds,
|
|
24
25
|
ROOM_ELEVATION_DRAG_LABEL,
|
|
25
26
|
} from './room-handle-drag'
|
|
26
|
-
import { RoomSelectionIndex, resolveRoomHit } from './room-selection'
|
|
27
27
|
import { applyRoomPlan } from './room-structure-commands'
|
|
28
28
|
import { ROOM_MOVE_DRAG_LABEL } from './room-transform-session'
|
|
29
29
|
import { zoneAtLevelPoint } from './units'
|
|
@@ -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 &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { isTypingTarget } from '@pascal-app/viewer'
|
|
1
2
|
import { create } from 'zustand'
|
|
2
3
|
import type { ActivePaintMaterial } from './material-paint'
|
|
3
|
-
import { isTypingTarget } from './typing-target'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* What a paint click does: paint the whole surface under it, draw part of a
|
|
@@ -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
|