@pascal-app/editor 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +110 -42
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +77 -68
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +31 -11
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +458 -140
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +23 -1
- package/src/components/editor/index.tsx +150 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +2 -3
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +4 -3
- package/src/components/editor/room-handles-2d.tsx +158 -7
- package/src/components/editor/room-handles.tsx +244 -54
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +33 -17
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +15 -7
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +47 -19
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
- package/src/components/ui/helpers/helper-manager.tsx +136 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
- package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +11 -10
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +9 -5
- package/src/components/ui/panels/panel-wrapper.tsx +63 -127
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +465 -66
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +22 -13
- package/src/components/ui/panels/room-panel.tsx +207 -100
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +62 -5
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15849 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +39331 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +39331 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +39331 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +39331 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +224 -45
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +101 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -2
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +15 -6
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +24 -6
- package/src/lib/measurements.ts +1 -203
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +28 -17
- package/src/lib/room-built-on.ts +21 -17
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +141 -1
- package/src/lib/room-handle-drag.ts +554 -58
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +38 -1
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +11 -5
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +21 -6
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +17 -6
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +44 -0
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +53 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-snap-indicator.ts +15 -2
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -100
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getLevelDisplayName,
|
|
19
19
|
getLevelElevations,
|
|
20
20
|
getResolvedElevatorDoorStyle,
|
|
21
|
+
type InteractionSound,
|
|
21
22
|
itemPrompt,
|
|
22
23
|
nodeMechanism,
|
|
23
24
|
openElevatorDoor,
|
|
@@ -45,12 +46,20 @@ import {
|
|
|
45
46
|
CROUCH_FLOAT_HEIGHT,
|
|
46
47
|
CROUCH_RUN_SPEED,
|
|
47
48
|
CROUCH_WALK_SPEED,
|
|
49
|
+
createDroneInput,
|
|
50
|
+
DRONE_SLOW_MULTIPLIER,
|
|
51
|
+
type DroneLook,
|
|
48
52
|
EYE_LERP_SPEED,
|
|
49
53
|
type MovementInput,
|
|
54
|
+
openingSettled,
|
|
55
|
+
proceduralSettled,
|
|
50
56
|
STAND_CAPSULE,
|
|
51
57
|
STAND_CLEARANCE,
|
|
52
58
|
STAND_FLOAT_HEIGHT,
|
|
59
|
+
seedDroneLook,
|
|
53
60
|
setSurfaceRaycastLayers,
|
|
61
|
+
stepDroneFlight,
|
|
62
|
+
turnFirstPersonLook,
|
|
54
63
|
useViewer,
|
|
55
64
|
WALKTHROUGH_FOV,
|
|
56
65
|
} from '@pascal-app/viewer'
|
|
@@ -76,21 +85,19 @@ import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-
|
|
|
76
85
|
import '../../three-types'
|
|
77
86
|
import {
|
|
78
87
|
closeDoorOpenState,
|
|
88
|
+
closeWindowOpenState,
|
|
79
89
|
DOOR_SWING_OPEN_ANGLE,
|
|
80
90
|
getDisplayedDoorValue,
|
|
81
|
-
isOperationDoorType,
|
|
82
|
-
toggleDoorOpenState,
|
|
83
|
-
} from '../../lib/door-interaction'
|
|
84
|
-
import { isTypingTarget } from '../../lib/typing-target'
|
|
85
|
-
import {
|
|
86
|
-
closeWindowOpenState,
|
|
87
91
|
getDisplayedWindowValue,
|
|
88
92
|
isOperableWindowType,
|
|
93
|
+
isOperationDoorType,
|
|
94
|
+
toggleDoorOpenState,
|
|
89
95
|
toggleWindowOpenState,
|
|
90
|
-
} from '
|
|
96
|
+
} from '@pascal-app/core'
|
|
97
|
+
import { isTypingTarget } from '../../lib/typing-target'
|
|
91
98
|
import useEditor from '../../store/use-editor'
|
|
92
99
|
import { useFirstPersonHud, type WalkthroughInteract } from '../../store/use-first-person-hud'
|
|
93
|
-
import { WalkthroughHud } from '../walkthrough-hud'
|
|
100
|
+
import { DroneHud, WalkthroughHud } from '../walkthrough-hud'
|
|
94
101
|
import {
|
|
95
102
|
buildFirstPersonColliderWorldFromRegistry,
|
|
96
103
|
deriveFirstPersonSpawn,
|
|
@@ -107,13 +114,6 @@ import {
|
|
|
107
114
|
} from './first-person/mechanism-targets'
|
|
108
115
|
|
|
109
116
|
const CAMERA_EYE_OFFSET = 0.45
|
|
110
|
-
const LOOK_SENSITIVITY = 0.002
|
|
111
|
-
// Drone mode: metres per second, and how hard Shift boosts it. The smoothing
|
|
112
|
-
// constant is an exponential approach rate, not a linear acceleration.
|
|
113
|
-
const DRONE_SPEED = 7
|
|
114
|
-
const DRONE_RUN_MULTIPLIER = 3
|
|
115
|
-
const DRONE_SLOW_MULTIPLIER = 0.2
|
|
116
|
-
const DRONE_SMOOTHING = 12
|
|
117
117
|
const CONTROLLER_CENTER_FROM_EYE = 0.85
|
|
118
118
|
const DOOR_INTERACTION_DISTANCE = 2.5
|
|
119
119
|
const DOOR_LEAF_INTERACTION_DEPTH = 0.08
|
|
@@ -168,10 +168,7 @@ function focusFirstPersonCanvas(canvas: HTMLCanvasElement) {
|
|
|
168
168
|
|
|
169
169
|
const cameraOffset = new Vector3()
|
|
170
170
|
const cameraEuler = new Euler(0, 0, 0, 'YXZ')
|
|
171
|
-
const
|
|
172
|
-
const droneForward = new Vector3()
|
|
173
|
-
const droneRight = new Vector3()
|
|
174
|
-
const droneDesiredVelocity = new Vector3()
|
|
171
|
+
const droneInput = createDroneInput()
|
|
175
172
|
const standClearanceRaycaster = new Raycaster()
|
|
176
173
|
const standClearanceUp = new Vector3(0, 1, 0)
|
|
177
174
|
const centerScreenPoint = new Vector2(0, 0)
|
|
@@ -425,6 +422,40 @@ function resolveHudInteract(target: FirstPersonInteractableTarget | null): Walkt
|
|
|
425
422
|
return { label: node.name || 'door', verb: isOpen ? 'close' : 'open' }
|
|
426
423
|
}
|
|
427
424
|
|
|
425
|
+
/** E plays what the viewer's callout plays for the same door, window or switch. */
|
|
426
|
+
function playTargetSound(target: FirstPersonInteractableTarget) {
|
|
427
|
+
const verb = resolveHudInteract(target)?.verb
|
|
428
|
+
if (!verb || verb === 'press') return
|
|
429
|
+
const node = useScene.getState().nodes[target.id]
|
|
430
|
+
// A recipe's door or lid swings and claps shut, its drawer slides; both land on the motion's end.
|
|
431
|
+
if (target.type === 'procedural' && (verb === 'open' || verb === 'close')) {
|
|
432
|
+
if (verb === 'open') {
|
|
433
|
+
emitter.emit('interaction:sound', 'panel-open')
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
if (target.kind !== 'slide') emitter.emit('interaction:sound', 'panel-swing')
|
|
437
|
+
void proceduralSettled(target.id).then(() => emitter.emit('interaction:sound', 'panel-close'))
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
let sound: InteractionSound
|
|
441
|
+
if (verb !== 'open' && verb !== 'close')
|
|
442
|
+
sound = verb === 'turn on' || verb === 'play' ? 'switch-on' : 'switch-off'
|
|
443
|
+
else if (node?.type === 'door' && node.doorType?.startsWith('garage-')) sound = 'garage'
|
|
444
|
+
else if (node?.type === 'window') sound = verb === 'open' ? 'window-open' : 'window-close'
|
|
445
|
+
else sound = verb === 'open' ? 'door-open' : 'door-close'
|
|
446
|
+
// A built-in leaf or sash swings shut, then latches on the frame it lands.
|
|
447
|
+
if (
|
|
448
|
+
(sound === 'door-close' || sound === 'window-close') &&
|
|
449
|
+
(target.type === 'door' || target.type === 'window') &&
|
|
450
|
+
node
|
|
451
|
+
) {
|
|
452
|
+
emitter.emit('interaction:sound', sound === 'door-close' ? 'door-swing' : 'window-swing')
|
|
453
|
+
void openingSettled(node).then(() => emitter.emit('interaction:sound', sound))
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
emitter.emit('interaction:sound', sound)
|
|
457
|
+
}
|
|
458
|
+
|
|
428
459
|
function resolveElevatorButtonTarget(object: Object3D): ElevatorButtonTarget | null {
|
|
429
460
|
let current: Object3D | null = object
|
|
430
461
|
|
|
@@ -734,8 +765,7 @@ export const FirstPersonControls = () => {
|
|
|
734
765
|
const controllerRef = useRef<BVHEcctrlApi | null>(null)
|
|
735
766
|
const movementInputRef = useRef<MovementInput>({ ...inactiveMovementInput })
|
|
736
767
|
const hadPointerLockRef = useRef(false)
|
|
737
|
-
const
|
|
738
|
-
const pitchRef = useRef(0)
|
|
768
|
+
const lookRef = useRef<DroneLook>({ yaw: 0, pitch: 0 })
|
|
739
769
|
const interactableTargetRef = useRef<FirstPersonInteractableTarget | null>(null)
|
|
740
770
|
const hudLabelFrameRef = useRef(HUD_LABEL_SAMPLE_FRAMES - 1)
|
|
741
771
|
const crouchKeyRef = useRef(false)
|
|
@@ -1131,6 +1161,7 @@ export const FirstPersonControls = () => {
|
|
|
1131
1161
|
|
|
1132
1162
|
const target = interactableTargetRef.current ?? resolveInteractableTarget()
|
|
1133
1163
|
if (!target) return
|
|
1164
|
+
playTargetSound(target)
|
|
1134
1165
|
if (target.type === 'mechanism') {
|
|
1135
1166
|
const node = useScene.getState().nodes[target.id]
|
|
1136
1167
|
if (node) toggleNodeMechanism(node)
|
|
@@ -1325,9 +1356,7 @@ export const FirstPersonControls = () => {
|
|
|
1325
1356
|
// Drone picks up wherever the camera currently is — orbit pose or walk eye.
|
|
1326
1357
|
useEffect(() => {
|
|
1327
1358
|
if (!isDroneMode) return
|
|
1328
|
-
|
|
1329
|
-
yawRef.current = droneEuler.y
|
|
1330
|
-
pitchRef.current = droneEuler.x
|
|
1359
|
+
seedDroneLook(lookRef.current, camera.quaternion)
|
|
1331
1360
|
droneVelocityRef.current.set(0, 0, 0)
|
|
1332
1361
|
|
|
1333
1362
|
// Interaction prompts belong to walk; drop whatever its frame left behind.
|
|
@@ -1344,8 +1373,8 @@ export const FirstPersonControls = () => {
|
|
|
1344
1373
|
|
|
1345
1374
|
const spawn = placedSpawn ?? deriveFirstPersonSpawn(camera, world)
|
|
1346
1375
|
const [x, y, z] = spawn.position
|
|
1347
|
-
|
|
1348
|
-
|
|
1376
|
+
lookRef.current.yaw = spawn.yaw
|
|
1377
|
+
lookRef.current.pitch = 0
|
|
1349
1378
|
setControllerStart({
|
|
1350
1379
|
position: [x, y - CONTROLLER_CENTER_FROM_EYE, z],
|
|
1351
1380
|
yaw: spawn.yaw,
|
|
@@ -1381,15 +1410,13 @@ export const FirstPersonControls = () => {
|
|
|
1381
1410
|
// Shutter hold: the shot is rendering — a mouse twitch must not pan it.
|
|
1382
1411
|
if (useEditor.getState().captureShutterHold) return
|
|
1383
1412
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1413
|
+
turnFirstPersonLook(
|
|
1414
|
+
lookRef.current,
|
|
1415
|
+
e.movementX,
|
|
1416
|
+
e.movementY,
|
|
1417
|
+
useEditor.getState().firstPersonMovementMode === 'drone' && droneSlowKeyRef.current
|
|
1387
1418
|
? DRONE_SLOW_MULTIPLIER
|
|
1388
|
-
: 1
|
|
1389
|
-
yawRef.current -= e.movementX * lookSensitivity
|
|
1390
|
-
pitchRef.current = Math.max(
|
|
1391
|
-
-(Math.PI / 2 - 0.05),
|
|
1392
|
-
Math.min(Math.PI / 2 - 0.05, pitchRef.current - e.movementY * lookSensitivity),
|
|
1419
|
+
: 1,
|
|
1393
1420
|
)
|
|
1394
1421
|
}
|
|
1395
1422
|
|
|
@@ -1817,45 +1844,24 @@ export const FirstPersonControls = () => {
|
|
|
1817
1844
|
return standClearanceRaycaster.intersectObjects(meshes, false).length === 0
|
|
1818
1845
|
}, [])
|
|
1819
1846
|
|
|
1820
|
-
// Drone:
|
|
1821
|
-
//
|
|
1822
|
-
// rises, Q (or Ctrl) sinks,
|
|
1847
|
+
// Drone: the viewer's free flight (`stepDroneFlight`, also behind the viewer's
|
|
1848
|
+
// `DroneController`) fed from this controller's keys — WASD along the view
|
|
1849
|
+
// axes, Space or E rises, Q (or Ctrl) sinks, Shift boosts, Alt slows.
|
|
1823
1850
|
useFrame((_, delta) => {
|
|
1824
1851
|
if (!isDroneMode) return
|
|
1825
1852
|
// Shutter hold: freeze the drone mid-air while the shot renders.
|
|
1826
1853
|
if (useEditor.getState().captureShutterHold) return
|
|
1827
1854
|
|
|
1828
|
-
const step = Math.min(delta, 0.1)
|
|
1829
1855
|
const movement = movementInputRef.current
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
if (movement.rightward) droneDesiredVelocity.add(droneRight)
|
|
1840
|
-
if (movement.leftward) droneDesiredVelocity.sub(droneRight)
|
|
1841
|
-
if (movement.jump || droneAscendKeyRef.current) droneDesiredVelocity.y += 1
|
|
1842
|
-
if (droneDescendKeyRef.current || crouchKeyRef.current) droneDesiredVelocity.y -= 1
|
|
1843
|
-
if (droneDesiredVelocity.lengthSq() > 0) {
|
|
1844
|
-
droneDesiredVelocity
|
|
1845
|
-
.normalize()
|
|
1846
|
-
.multiplyScalar(
|
|
1847
|
-
DRONE_SPEED *
|
|
1848
|
-
(droneSlowKeyRef.current
|
|
1849
|
-
? DRONE_SLOW_MULTIPLIER
|
|
1850
|
-
: movement.run
|
|
1851
|
-
? DRONE_RUN_MULTIPLIER
|
|
1852
|
-
: 1),
|
|
1853
|
-
)
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
droneVelocityRef.current.lerp(droneDesiredVelocity, 1 - Math.exp(-step * DRONE_SMOOTHING))
|
|
1857
|
-
camera.position.addScaledVector(droneVelocityRef.current, step)
|
|
1858
|
-
camera.updateMatrixWorld(true)
|
|
1856
|
+
droneInput.forward = movement.forward === true
|
|
1857
|
+
droneInput.backward = movement.backward === true
|
|
1858
|
+
droneInput.leftward = movement.leftward === true
|
|
1859
|
+
droneInput.rightward = movement.rightward === true
|
|
1860
|
+
droneInput.up = movement.jump === true || droneAscendKeyRef.current
|
|
1861
|
+
droneInput.down = droneDescendKeyRef.current || crouchKeyRef.current
|
|
1862
|
+
droneInput.boost = movement.run === true
|
|
1863
|
+
droneInput.slow = droneSlowKeyRef.current
|
|
1864
|
+
stepDroneFlight(camera, lookRef.current, droneInput, droneVelocityRef.current, delta)
|
|
1859
1865
|
}, 2.5)
|
|
1860
1866
|
|
|
1861
1867
|
useFrame((_, delta) => {
|
|
@@ -1899,7 +1905,7 @@ export const FirstPersonControls = () => {
|
|
|
1899
1905
|
|
|
1900
1906
|
group.rotation.y = 0
|
|
1901
1907
|
camera.position.copy(group.position).add(cameraOffset)
|
|
1902
|
-
cameraEuler.set(
|
|
1908
|
+
cameraEuler.set(lookRef.current.pitch, lookRef.current.yaw, 0, 'YXZ')
|
|
1903
1909
|
camera.quaternion.setFromEuler(cameraEuler)
|
|
1904
1910
|
camera.updateMatrixWorld(true)
|
|
1905
1911
|
syncElevatorRide(group)
|
|
@@ -1991,6 +1997,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
1991
1997
|
const zoneLabel = useFirstPersonHud((state) => state.zoneLabel)
|
|
1992
1998
|
const interact = useFirstPersonHud((state) => state.interact)
|
|
1993
1999
|
const suspended = useViewer((state) => state.walkthroughSuspended)
|
|
2000
|
+
const isDroneMode = useEditor((state) => state.firstPersonMovementMode === 'drone')
|
|
1994
2001
|
|
|
1995
2002
|
const handleExit = useCallback(() => {
|
|
1996
2003
|
if (document.pointerLockElement) {
|
|
@@ -1999,6 +2006,8 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
1999
2006
|
onExit()
|
|
2000
2007
|
}, [onExit])
|
|
2001
2008
|
|
|
2009
|
+
if (isDroneMode) return <DroneHud onExit={handleExit} suspended={suspended} />
|
|
2010
|
+
|
|
2002
2011
|
return (
|
|
2003
2012
|
<WalkthroughHud
|
|
2004
2013
|
floorLabel={floorLabel}
|
|
@@ -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
|
+
})
|