@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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { CameraControlsImpl } from '@react-three/drei'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import {
|
|
5
|
+
computeFitSceneLookAt,
|
|
6
|
+
type PendingFitScene,
|
|
7
|
+
planFitSceneOnEvent,
|
|
8
|
+
planFitSceneOnOrbitResume,
|
|
9
|
+
planLevelFollow,
|
|
10
|
+
} from './fit-scene-framing'
|
|
11
|
+
|
|
12
|
+
const sampleBounds = {
|
|
13
|
+
center: [10, 20] as [number, number],
|
|
14
|
+
size: [30, 40] as [number, number],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('fit-scene framing', () => {
|
|
18
|
+
test('computes default look-at when bounds are null', () => {
|
|
19
|
+
expect(computeFitSceneLookAt(null)).toEqual({
|
|
20
|
+
eyeX: 20,
|
|
21
|
+
eyeY: 20,
|
|
22
|
+
eyeZ: 20,
|
|
23
|
+
targetX: 0,
|
|
24
|
+
targetY: 0,
|
|
25
|
+
targetZ: 0,
|
|
26
|
+
eyeHeight: 20,
|
|
27
|
+
fitSphere: { center: [0, 0, 0], radius: 15 },
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('computes orbit look-at from scene bounds', () => {
|
|
32
|
+
const lookAt = computeFitSceneLookAt(sampleBounds)
|
|
33
|
+
// Longer extent is depth=40 → distance=56, height=32
|
|
34
|
+
expect(lookAt).toEqual({
|
|
35
|
+
eyeX: 10 + 56 * 0.7,
|
|
36
|
+
eyeY: 32,
|
|
37
|
+
eyeZ: 20 + 56 * 0.7,
|
|
38
|
+
targetX: 10,
|
|
39
|
+
targetY: 0,
|
|
40
|
+
targetZ: 20,
|
|
41
|
+
eyeHeight: 32,
|
|
42
|
+
fitSphere: { center: [10, 0, 20], radius: 25 * 1.3 },
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('rests the pivot and lifts the eye by the level ground height', () => {
|
|
47
|
+
const lookAt = computeFitSceneLookAt(sampleBounds, 3)
|
|
48
|
+
expect(lookAt.targetY).toBe(3)
|
|
49
|
+
expect(lookAt.eyeY).toBe(35)
|
|
50
|
+
expect(lookAt.eyeHeight).toBe(32)
|
|
51
|
+
expect(computeFitSceneLookAt(null, 3)).toMatchObject({ eyeY: 23, targetY: 3 })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('fits the orthographic sphere around the footprint at the pivot', () => {
|
|
55
|
+
const lookAt = computeFitSceneLookAt(sampleBounds, 3)
|
|
56
|
+
expect(lookAt.fitSphere.center).toEqual([lookAt.targetX, lookAt.targetY, lookAt.targetZ])
|
|
57
|
+
const [cx, cz] = sampleBounds.center
|
|
58
|
+
const [w, d] = sampleBounds.size
|
|
59
|
+
for (const [x, z] of [
|
|
60
|
+
[cx - w / 2, cz - d / 2],
|
|
61
|
+
[cx + w / 2, cz + d / 2],
|
|
62
|
+
[cx - w / 2, cz + d / 2],
|
|
63
|
+
] as const) {
|
|
64
|
+
expect(Math.hypot(x - cx, z - cz)).toBeLessThan(lookAt.fitSphere.radius)
|
|
65
|
+
}
|
|
66
|
+
const lone = computeFitSceneLookAt({ center: [0, 0], size: [1, 0.2] })
|
|
67
|
+
expect(lone.fitSphere.radius).toBe(5)
|
|
68
|
+
expect(computeFitSceneLookAt(null, 3).fitSphere).toEqual({ center: [0, 3, 0], radius: 15 })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('an orthographic camera fitted to the sphere shows the whole footprint whatever its zoom was', () => {
|
|
72
|
+
const previous = globalThis.DOMRect
|
|
73
|
+
globalThis.DOMRect ??= class {} as typeof DOMRect
|
|
74
|
+
CameraControlsImpl.install({ THREE })
|
|
75
|
+
const camera = new THREE.OrthographicCamera(-8, 8, 5, -5, 0.1, 1000)
|
|
76
|
+
const control = new CameraControlsImpl(camera)
|
|
77
|
+
const bounds = { center: [60, -20] as [number, number], size: [80, 30] as [number, number] }
|
|
78
|
+
try {
|
|
79
|
+
// The previous project left the camera zoomed in on a small room.
|
|
80
|
+
control.zoomTo(12, false)
|
|
81
|
+
const lookAt = computeFitSceneLookAt(bounds)
|
|
82
|
+
control.setLookAt(
|
|
83
|
+
lookAt.eyeX,
|
|
84
|
+
lookAt.eyeY,
|
|
85
|
+
lookAt.eyeZ,
|
|
86
|
+
lookAt.targetX,
|
|
87
|
+
lookAt.targetY,
|
|
88
|
+
lookAt.targetZ,
|
|
89
|
+
false,
|
|
90
|
+
)
|
|
91
|
+
const sphere = new THREE.Sphere(
|
|
92
|
+
new THREE.Vector3(...lookAt.fitSphere.center),
|
|
93
|
+
lookAt.fitSphere.radius,
|
|
94
|
+
)
|
|
95
|
+
control.fitToSphere(sphere, false)
|
|
96
|
+
control.update(0)
|
|
97
|
+
camera.updateMatrixWorld(true)
|
|
98
|
+
|
|
99
|
+
const target = control.getTarget(new THREE.Vector3())
|
|
100
|
+
expect(target.toArray()).toEqual([lookAt.targetX, lookAt.targetY, lookAt.targetZ])
|
|
101
|
+
const [cx, cz] = bounds.center
|
|
102
|
+
const [w, d] = bounds.size
|
|
103
|
+
for (const x of [cx - w / 2, cx + w / 2])
|
|
104
|
+
for (const z of [cz - d / 2, cz + d / 2]) {
|
|
105
|
+
const projected = new THREE.Vector3(x, 0, z).project(camera)
|
|
106
|
+
expect(Math.abs(projected.x)).toBeLessThanOrEqual(1)
|
|
107
|
+
expect(Math.abs(projected.y)).toBeLessThanOrEqual(1)
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
control.dispose()
|
|
111
|
+
globalThis.DOMRect = previous
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('applies immediately when orbit controls are live', () => {
|
|
116
|
+
const plan = planFitSceneOnEvent({
|
|
117
|
+
isPreviewMode: false,
|
|
118
|
+
isFirstPersonMode: false,
|
|
119
|
+
hasControls: true,
|
|
120
|
+
bounds: sampleBounds,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(plan).toEqual({
|
|
124
|
+
action: 'apply',
|
|
125
|
+
lookAt: computeFitSceneLookAt(sampleBounds),
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('ignores fit-scene while preview mode is active', () => {
|
|
130
|
+
const plan = planFitSceneOnEvent({
|
|
131
|
+
isPreviewMode: true,
|
|
132
|
+
isFirstPersonMode: false,
|
|
133
|
+
hasControls: true,
|
|
134
|
+
bounds: sampleBounds,
|
|
135
|
+
})
|
|
136
|
+
expect(plan).toEqual({ action: 'ignore' })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('queues fit-scene while first-person is active (scene-ready during FP)', () => {
|
|
140
|
+
const plan = planFitSceneOnEvent({
|
|
141
|
+
isPreviewMode: false,
|
|
142
|
+
isFirstPersonMode: true,
|
|
143
|
+
hasControls: false,
|
|
144
|
+
bounds: sampleBounds,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
expect(plan).toEqual({
|
|
148
|
+
action: 'queue',
|
|
149
|
+
pending: { bounds: sampleBounds },
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('queues fit-scene when orbit controls are not mounted yet', () => {
|
|
154
|
+
const plan = planFitSceneOnEvent({
|
|
155
|
+
isPreviewMode: false,
|
|
156
|
+
isFirstPersonMode: false,
|
|
157
|
+
hasControls: false,
|
|
158
|
+
bounds: sampleBounds,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
expect(plan).toEqual({
|
|
162
|
+
action: 'queue',
|
|
163
|
+
pending: { bounds: sampleBounds },
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('scene-ready during first-person then return to orbit applies the pending frame', () => {
|
|
168
|
+
// Mirrors the blocking lifecycle: fit arrives while FP is active, then
|
|
169
|
+
// orbit remounts and the queued frame must still apply.
|
|
170
|
+
let pending: PendingFitScene | null = null
|
|
171
|
+
|
|
172
|
+
const duringFirstPerson = planFitSceneOnEvent({
|
|
173
|
+
isPreviewMode: false,
|
|
174
|
+
isFirstPersonMode: true,
|
|
175
|
+
hasControls: false,
|
|
176
|
+
bounds: sampleBounds,
|
|
177
|
+
})
|
|
178
|
+
expect(duringFirstPerson.action).toBe('queue')
|
|
179
|
+
if (duringFirstPerson.action === 'queue') {
|
|
180
|
+
pending = duringFirstPerson.pending
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Still in first-person: controls are not ready, keep the pending frame.
|
|
184
|
+
expect(
|
|
185
|
+
planFitSceneOnOrbitResume({
|
|
186
|
+
isPreviewMode: false,
|
|
187
|
+
isFirstPersonMode: true,
|
|
188
|
+
hasControls: false,
|
|
189
|
+
pending,
|
|
190
|
+
}),
|
|
191
|
+
).toEqual({ action: 'noop' })
|
|
192
|
+
|
|
193
|
+
// Leave first-person; orbit controls remount and can apply the frame.
|
|
194
|
+
const afterOrbitResume = planFitSceneOnOrbitResume({
|
|
195
|
+
isPreviewMode: false,
|
|
196
|
+
isFirstPersonMode: false,
|
|
197
|
+
hasControls: true,
|
|
198
|
+
pending,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
expect(afterOrbitResume).toEqual({
|
|
202
|
+
action: 'apply',
|
|
203
|
+
lookAt: computeFitSceneLookAt(sampleBounds),
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('latest fit-scene while first-person wins when orbit resumes', () => {
|
|
208
|
+
let pending: PendingFitScene | null = null
|
|
209
|
+
const first = planFitSceneOnEvent({
|
|
210
|
+
isPreviewMode: false,
|
|
211
|
+
isFirstPersonMode: true,
|
|
212
|
+
hasControls: false,
|
|
213
|
+
bounds: null,
|
|
214
|
+
})
|
|
215
|
+
if (first.action === 'queue') pending = first.pending
|
|
216
|
+
|
|
217
|
+
const secondBounds = {
|
|
218
|
+
center: [0, 0] as [number, number],
|
|
219
|
+
size: [10, 10] as [number, number],
|
|
220
|
+
}
|
|
221
|
+
const second = planFitSceneOnEvent({
|
|
222
|
+
isPreviewMode: false,
|
|
223
|
+
isFirstPersonMode: true,
|
|
224
|
+
hasControls: false,
|
|
225
|
+
bounds: secondBounds,
|
|
226
|
+
})
|
|
227
|
+
if (second.action === 'queue') pending = second.pending
|
|
228
|
+
|
|
229
|
+
expect(
|
|
230
|
+
planFitSceneOnOrbitResume({
|
|
231
|
+
isPreviewMode: false,
|
|
232
|
+
isFirstPersonMode: false,
|
|
233
|
+
hasControls: true,
|
|
234
|
+
pending,
|
|
235
|
+
}),
|
|
236
|
+
).toEqual({
|
|
237
|
+
action: 'apply',
|
|
238
|
+
lookAt: computeFitSceneLookAt(secondBounds),
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('does not flush a pending frame into preview mode', () => {
|
|
243
|
+
expect(
|
|
244
|
+
planFitSceneOnOrbitResume({
|
|
245
|
+
isPreviewMode: true,
|
|
246
|
+
isFirstPersonMode: false,
|
|
247
|
+
hasControls: true,
|
|
248
|
+
pending: { bounds: sampleBounds },
|
|
249
|
+
}),
|
|
250
|
+
).toEqual({ action: 'noop' })
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('level follow', () => {
|
|
255
|
+
type Inputs = { levelId: string | null; levelMode: string; camera: object; sceneEmpty: boolean }
|
|
256
|
+
|
|
257
|
+
// The effect's bookkeeping: one instance per mounted CustomCameraControls.
|
|
258
|
+
function mountLevelFollow(initial: Inputs) {
|
|
259
|
+
let firstLoad = true
|
|
260
|
+
let previous = { ...initial }
|
|
261
|
+
return (next: Inputs) => {
|
|
262
|
+
const plan = planLevelFollow({
|
|
263
|
+
firstLoad,
|
|
264
|
+
sceneEmpty: next.sceneEmpty,
|
|
265
|
+
levelChanged: previous.levelId !== next.levelId,
|
|
266
|
+
modeChanged: previous.levelMode !== next.levelMode,
|
|
267
|
+
cameraChanged: previous.camera !== next.camera,
|
|
268
|
+
})
|
|
269
|
+
firstLoad = false
|
|
270
|
+
previous = { ...next }
|
|
271
|
+
return plan
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const perspective = {}
|
|
276
|
+
const loaded: Inputs = {
|
|
277
|
+
levelId: 'level_1',
|
|
278
|
+
levelMode: 'stacked',
|
|
279
|
+
camera: perspective,
|
|
280
|
+
sceneEmpty: false,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
test('an empty first load takes the default pose', () => {
|
|
284
|
+
const run = mountLevelFollow({ ...loaded, levelId: null, sceneEmpty: true })
|
|
285
|
+
expect(run({ ...loaded, levelId: null, sceneEmpty: true })).toBe('default-pose')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('a remount over a loaded scene rests the fresh pivot on the level', () => {
|
|
289
|
+
const run = mountLevelFollow(loaded)
|
|
290
|
+
expect(run(loaded)).toBe('rest-pivot')
|
|
291
|
+
expect(run(loaded)).toBe('none')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('a level-mode change pans the pivot', () => {
|
|
295
|
+
const run = mountLevelFollow(loaded)
|
|
296
|
+
run(loaded)
|
|
297
|
+
expect(run({ ...loaded, levelMode: 'exploded' })).toBe('rest-pivot')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('the first pick after a site-phase load pans to the level', () => {
|
|
301
|
+
const site = { ...loaded, levelId: null }
|
|
302
|
+
const run = mountLevelFollow(site)
|
|
303
|
+
run(site)
|
|
304
|
+
expect(run(loaded)).toBe('rest-pivot')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('re-picking the same level does not move the pivot', () => {
|
|
308
|
+
const run = mountLevelFollow(loaded)
|
|
309
|
+
run(loaded)
|
|
310
|
+
expect(run({ ...loaded })).toBe('none')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('a camera swap re-rests the new controls pivot', () => {
|
|
314
|
+
const run = mountLevelFollow(loaded)
|
|
315
|
+
run(loaded)
|
|
316
|
+
expect(run({ ...loaded, camera: {} })).toBe('rest-pivot')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export type FitSceneBounds = {
|
|
2
|
+
center: [number, number]
|
|
3
|
+
size: [number, number]
|
|
4
|
+
} | null
|
|
5
|
+
|
|
6
|
+
export type FitSceneLookAt = {
|
|
7
|
+
eyeX: number
|
|
8
|
+
eyeY: number
|
|
9
|
+
eyeZ: number
|
|
10
|
+
targetX: number
|
|
11
|
+
targetY: number
|
|
12
|
+
targetZ: number
|
|
13
|
+
/** The eye's height above `targetY`, before any keep-over-ground lift. */
|
|
14
|
+
eyeHeight: number
|
|
15
|
+
/**
|
|
16
|
+
* What an orthographic camera fits its zoom to: `setLookAt` leaves the
|
|
17
|
+
* previous project's zoom in place. Centred on the target, so fitting it
|
|
18
|
+
* does not move the pivot.
|
|
19
|
+
*/
|
|
20
|
+
fitSphere: { center: [number, number, number]; radius: number }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The empty-editor pose shows the default ±15 m site.
|
|
24
|
+
const DEFAULT_FIT_RADIUS = 15
|
|
25
|
+
// Margin around the footprint, as the frame-node path uses for orthographic fits.
|
|
26
|
+
const FIT_SPHERE_MARGIN = 1.3
|
|
27
|
+
// Keeps a lone wall from filling the viewport, like the perspective 15 m floor.
|
|
28
|
+
const MIN_FIT_RADIUS = 5
|
|
29
|
+
|
|
30
|
+
export type PendingFitScene = {
|
|
31
|
+
bounds: FitSceneBounds
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type FitSceneEventPlan =
|
|
35
|
+
| { action: 'ignore' }
|
|
36
|
+
| { action: 'apply'; lookAt: FitSceneLookAt }
|
|
37
|
+
| { action: 'queue'; pending: PendingFitScene }
|
|
38
|
+
|
|
39
|
+
export type FitSceneResumePlan = { action: 'noop' } | { action: 'apply'; lookAt: FitSceneLookAt }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compute the orbit look-at used by `camera-controls:fit-scene`, resting the
|
|
43
|
+
* pivot at `groundY` (the selected level's orbit height).
|
|
44
|
+
* Null bounds restore the empty-editor default pose.
|
|
45
|
+
*/
|
|
46
|
+
export function computeFitSceneLookAt(bounds: FitSceneBounds, groundY = 0): FitSceneLookAt {
|
|
47
|
+
if (!bounds) {
|
|
48
|
+
return {
|
|
49
|
+
eyeX: 20,
|
|
50
|
+
eyeY: 20 + groundY,
|
|
51
|
+
eyeZ: 20,
|
|
52
|
+
targetX: 0,
|
|
53
|
+
targetY: groundY,
|
|
54
|
+
targetZ: 0,
|
|
55
|
+
eyeHeight: 20,
|
|
56
|
+
fitSphere: { center: [0, groundY, 0], radius: DEFAULT_FIT_RADIUS },
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const [cx, cz] = bounds.center
|
|
61
|
+
const [w, d] = bounds.size
|
|
62
|
+
// Use the longer horizontal extent to size the orbit radius so the whole
|
|
63
|
+
// footprint sits in view regardless of aspect ratio.
|
|
64
|
+
const maxExtent = Math.max(w, d)
|
|
65
|
+
const distance = Math.max(maxExtent * 1.4, 15)
|
|
66
|
+
const height = Math.max(maxExtent * 0.8, 10)
|
|
67
|
+
return {
|
|
68
|
+
eyeX: cx + distance * 0.7,
|
|
69
|
+
eyeY: height + groundY,
|
|
70
|
+
eyeZ: cz + distance * 0.7,
|
|
71
|
+
targetX: cx,
|
|
72
|
+
targetY: groundY,
|
|
73
|
+
targetZ: cz,
|
|
74
|
+
eyeHeight: height,
|
|
75
|
+
fitSphere: {
|
|
76
|
+
center: [cx, groundY, cz],
|
|
77
|
+
radius: Math.max((Math.hypot(w, d) / 2) * FIT_SPHERE_MARGIN, MIN_FIT_RADIUS),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Decide what to do when a fit-scene event arrives.
|
|
84
|
+
* First-person (and briefly-unmounted orbit controls) must queue the frame
|
|
85
|
+
* so it can apply once orbit is back.
|
|
86
|
+
*/
|
|
87
|
+
export function planFitSceneOnEvent(input: {
|
|
88
|
+
isPreviewMode: boolean
|
|
89
|
+
isFirstPersonMode: boolean
|
|
90
|
+
hasControls: boolean
|
|
91
|
+
bounds: FitSceneBounds
|
|
92
|
+
groundY?: number
|
|
93
|
+
}): FitSceneEventPlan {
|
|
94
|
+
if (input.isPreviewMode) return { action: 'ignore' }
|
|
95
|
+
|
|
96
|
+
const pending: PendingFitScene = { bounds: input.bounds }
|
|
97
|
+
if (input.isFirstPersonMode || !input.hasControls) {
|
|
98
|
+
return { action: 'queue', pending }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { action: 'apply', lookAt: computeFitSceneLookAt(input.bounds, input.groundY) }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Flush a queued load frame once orbit controls can apply it. The ground is
|
|
106
|
+
* read at flush time: the selected level may have changed while it waited.
|
|
107
|
+
*/
|
|
108
|
+
export function planFitSceneOnOrbitResume(input: {
|
|
109
|
+
isPreviewMode: boolean
|
|
110
|
+
isFirstPersonMode: boolean
|
|
111
|
+
hasControls: boolean
|
|
112
|
+
pending: PendingFitScene | null
|
|
113
|
+
groundY?: number
|
|
114
|
+
}): FitSceneResumePlan {
|
|
115
|
+
if (!input.pending || input.isPreviewMode || input.isFirstPersonMode || !input.hasControls) {
|
|
116
|
+
return { action: 'noop' }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { action: 'apply', lookAt: computeFitSceneLookAt(input.pending.bounds, input.groundY) }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type LevelFollowPlan = 'none' | 'default-pose' | 'rest-pivot'
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What the level-follow effect does with the orbit pivot.
|
|
126
|
+
*
|
|
127
|
+
* Only a scene-less first run takes the default pose. A first run over a
|
|
128
|
+
* loaded scene is a fresh controls instance (mount, leaving XR, error-boundary
|
|
129
|
+
* recovery) whose pivot sits at the origin, so it is rested on the level like
|
|
130
|
+
* a camera change; on a real load the later scene-ready fit still wins.
|
|
131
|
+
* Afterwards the pivot follows level, level-mode and camera changes only.
|
|
132
|
+
*/
|
|
133
|
+
export function planLevelFollow(input: {
|
|
134
|
+
firstLoad: boolean
|
|
135
|
+
sceneEmpty: boolean
|
|
136
|
+
levelChanged: boolean
|
|
137
|
+
modeChanged: boolean
|
|
138
|
+
cameraChanged: boolean
|
|
139
|
+
}): LevelFollowPlan {
|
|
140
|
+
if (input.firstLoad) return input.sceneEmpty ? 'default-pose' : 'rest-pivot'
|
|
141
|
+
return input.levelChanged || input.modeChanged || input.cameraChanged ? 'rest-pivot' : 'none'
|
|
142
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getWallThickness,
|
|
15
15
|
isCurvedWall,
|
|
16
16
|
isDerivedNode,
|
|
17
|
+
isNodeDuplicable,
|
|
17
18
|
isRegistryMovable,
|
|
18
19
|
isRegistrySelectable,
|
|
19
20
|
isSplineFence,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
type WallNode,
|
|
30
31
|
} from '@pascal-app/core'
|
|
31
32
|
import { useViewer } from '@pascal-app/viewer'
|
|
33
|
+
import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
|
|
32
34
|
import { useFrame } from '@react-three/fiber'
|
|
33
35
|
import { useCallback, useMemo, useRef } from 'react'
|
|
34
36
|
import * as THREE from 'three'
|
|
@@ -53,7 +55,6 @@ import useInteractionScope, {
|
|
|
53
55
|
} from '../../store/use-interaction-scope'
|
|
54
56
|
import { IconRefGlyph } from '../ui/icon-ref'
|
|
55
57
|
import { duplicateNodeAndPickUp } from './duplicate-node'
|
|
56
|
-
import { Html } from './localized-html'
|
|
57
58
|
import { formatMeasurement, MeasurementPill } from './measurement-pill'
|
|
58
59
|
import { NodeActionMenu } from './node-action-menu'
|
|
59
60
|
import { startZoneRoomTransform } from './room-controls'
|
|
@@ -284,7 +285,7 @@ function getHeightPillDimensions(node: WallNode | FenceNode): {
|
|
|
284
285
|
} {
|
|
285
286
|
if (node.type === 'wall') {
|
|
286
287
|
return {
|
|
287
|
-
height: getWallEffectiveHeightForNodes(node, useScene.getState().nodes),
|
|
288
|
+
height: getWallEffectiveHeightForNodes(node, useScene.getState().nodes, 0),
|
|
288
289
|
length: getWallCurveLength(node),
|
|
289
290
|
thickness: getWallThickness(node),
|
|
290
291
|
}
|
|
@@ -423,7 +424,7 @@ export function FloatingActionMenu() {
|
|
|
423
424
|
| undefined
|
|
424
425
|
const fallbackHeight =
|
|
425
426
|
node.type === 'wall'
|
|
426
|
-
? getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
|
|
427
|
+
? getWallEffectiveHeightForNodes(node, useScene.getState().nodes, 0)
|
|
427
428
|
: FENCE_DEFAULT_HEIGHT
|
|
428
429
|
const liveHeight = override?.height ?? node.height ?? fallbackHeight
|
|
429
430
|
pillHeightRef.current.textContent = `H ${formatMeasurement(liveHeight, unit, metricNotation)}`
|
|
@@ -681,6 +682,7 @@ export function FloatingActionMenu() {
|
|
|
681
682
|
onDelete={handleDelete}
|
|
682
683
|
onDuplicate={
|
|
683
684
|
node &&
|
|
685
|
+
isNodeDuplicable(node as AnyNode) &&
|
|
684
686
|
node.type !== 'spawn' &&
|
|
685
687
|
!DELETE_ONLY_TYPES.includes(node.type) &&
|
|
686
688
|
!HOLE_TYPES.includes(node.type)
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { type BuildingNode, sceneRegistry, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
|
|
5
6
|
import { useFrame } from '@react-three/fiber'
|
|
6
7
|
import { useCallback, useRef } from 'react'
|
|
7
8
|
import * as THREE from 'three'
|
|
8
9
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
9
10
|
import useEditor from '../../store/use-editor'
|
|
10
|
-
import { Html } from './localized-html'
|
|
11
11
|
import { NodeActionMenu } from './node-action-menu'
|
|
12
12
|
|
|
13
13
|
export function FloatingBuildingActionMenu() {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type FloorOpeningNode,
|
|
7
7
|
resolveCeilingHeight,
|
|
8
|
+
roomFloorElevation,
|
|
8
9
|
sceneRegistry,
|
|
9
10
|
useScene,
|
|
10
11
|
} from '@pascal-app/core'
|
|
@@ -24,7 +25,6 @@ import { useCeilingEditSession } from '../../lib/ceiling-edit-session'
|
|
|
24
25
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
25
26
|
import { openingRoom, roomSurfaceOpenings, useFloorEditSession } from '../../lib/floor-edit-session'
|
|
26
27
|
import { useOpeningDraft } from '../../lib/floor-opening-draft'
|
|
27
|
-
import { roomFloorElevation } from '../../lib/room-handle-drag'
|
|
28
28
|
import { PolygonEditor } from '../tools/shared/polygon-editor'
|
|
29
29
|
|
|
30
30
|
// The openings of the room being edited ("Edit floor", "Edit ceiling", or a
|
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
import {
|
|
4
4
|
containsPoint,
|
|
5
5
|
emitter,
|
|
6
|
+
formatAreaLabel,
|
|
6
7
|
type MultiPolygon,
|
|
7
8
|
resolveCeilingHeight,
|
|
9
|
+
roomFloorElevation,
|
|
8
10
|
sceneRegistry,
|
|
9
11
|
useScene,
|
|
10
12
|
} from '@pascal-app/core'
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getSceneTheme,
|
|
15
|
+
isTypingTarget,
|
|
16
|
+
setSurfaceRaycastLayers,
|
|
17
|
+
useViewer,
|
|
18
|
+
} from '@pascal-app/viewer'
|
|
12
19
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
13
20
|
import { Pentagon, Square } from 'lucide-react'
|
|
14
21
|
import { useEffect, useMemo, useRef } from 'react'
|
|
@@ -60,21 +67,18 @@ import {
|
|
|
60
67
|
} from '../../lib/floor-region-session'
|
|
61
68
|
import { type FloorRegionPoint, snapFloorRegionPoint } from '../../lib/floor-region-snap'
|
|
62
69
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
63
|
-
import { formatAreaLabel } from '../../lib/measurements'
|
|
64
70
|
import {
|
|
65
71
|
cancelMezzanineDraft,
|
|
66
72
|
mezzanineDraftActive,
|
|
67
73
|
useMezzanineDraft,
|
|
68
74
|
} from '../../lib/mezzanine-draft'
|
|
69
75
|
import { usePaintRegionMode } from '../../lib/paint-region-mode'
|
|
70
|
-
import { roomFloorElevation } from '../../lib/room-handle-drag'
|
|
71
76
|
import {
|
|
72
77
|
cancelTerraceDraft,
|
|
73
78
|
terraceDraftActive,
|
|
74
79
|
toggleTerraceShape,
|
|
75
80
|
useTerraceDraft,
|
|
76
81
|
} from '../../lib/terrace-draft'
|
|
77
|
-
import { isTypingTarget } from '../../lib/typing-target'
|
|
78
82
|
import useEditor from '../../store/use-editor'
|
|
79
83
|
import { useFloorplanRender } from '../editor-2d/floorplan-render-context'
|
|
80
84
|
import { usePaintTint } from '../tools/paint-region/use-paint-tint'
|
|
@@ -2,13 +2,11 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import {
|
|
3
3
|
canApplyFloorplanNavigationSync,
|
|
4
4
|
canZoomFloorplanDuringNavigation,
|
|
5
|
-
createFloorplanNavigationSyncScheduler,
|
|
6
5
|
finalizeFloorplanNavigation,
|
|
7
6
|
flushFloorplanRotationPresentationRestore,
|
|
8
7
|
getFloorplanRotationOverscanViewBox,
|
|
9
8
|
queueFloorplanRotationPresentationRestore,
|
|
10
9
|
resolveFloorplanPresentationViewBox,
|
|
11
|
-
setFloorplanCompassRotation,
|
|
12
10
|
} from './floorplan-navigation-presentation'
|
|
13
11
|
|
|
14
12
|
describe('floorplan navigation presentation', () => {
|
|
@@ -60,16 +58,6 @@ describe('floorplan navigation presentation', () => {
|
|
|
60
58
|
expect(canApplyFloorplanNavigationSync(false)).toBe(true)
|
|
61
59
|
})
|
|
62
60
|
|
|
63
|
-
test('updates the compass presentation on every live rotation frame', () => {
|
|
64
|
-
const compass = { style: { transform: '' } }
|
|
65
|
-
|
|
66
|
-
setFloorplanCompassRotation(compass, 0.25)
|
|
67
|
-
expect(compass.style.transform).toBe('rotate(0.25deg)')
|
|
68
|
-
|
|
69
|
-
setFloorplanCompassRotation(compass, 0.5)
|
|
70
|
-
expect(compass.style.transform).toBe('rotate(0.5deg)')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
61
|
test('keeps the rotation preview in place until the committed state is ready to paint', () => {
|
|
74
62
|
const pending = { current: null }
|
|
75
63
|
const svg = {
|
|
@@ -111,32 +99,4 @@ describe('floorplan navigation presentation', () => {
|
|
|
111
99
|
|
|
112
100
|
expect(calls).toEqual(['zoom', 'pan', 'rotation:42'])
|
|
113
101
|
})
|
|
114
|
-
|
|
115
|
-
test('coalesces a camera pose stream into one settled React commit', () => {
|
|
116
|
-
const presented: number[] = []
|
|
117
|
-
const committed: number[] = []
|
|
118
|
-
let scheduled: (() => void) | null = null
|
|
119
|
-
const scheduler = createFloorplanNavigationSyncScheduler<number>({
|
|
120
|
-
applyPresentation: (pose) => presented.push(pose),
|
|
121
|
-
commit: (pose) => committed.push(pose),
|
|
122
|
-
schedule: (callback) => {
|
|
123
|
-
scheduled = callback
|
|
124
|
-
return 1 as unknown as ReturnType<typeof globalThis.setTimeout>
|
|
125
|
-
},
|
|
126
|
-
cancel: () => {
|
|
127
|
-
scheduled = null
|
|
128
|
-
},
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
for (let pose = 1; pose <= 30; pose += 1) {
|
|
132
|
-
scheduler.update(pose)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
expect(presented).toHaveLength(30)
|
|
136
|
-
expect(committed).toEqual([])
|
|
137
|
-
|
|
138
|
-
const settle = scheduled as (() => void) | null
|
|
139
|
-
settle?.()
|
|
140
|
-
expect(committed).toEqual([30])
|
|
141
|
-
})
|
|
142
102
|
})
|