@pascal-app/editor 0.9.1 → 1.0.0-beta.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 +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +188 -0
- package/src/lib/measurements.ts +210 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNodeId,
|
|
5
5
|
type CameraControlEvent,
|
|
6
6
|
type CameraControlFitSceneEvent,
|
|
7
|
+
type CameraPose,
|
|
7
8
|
emitter,
|
|
8
9
|
sceneRegistry,
|
|
9
10
|
useScene,
|
|
@@ -20,8 +21,25 @@ import {
|
|
|
20
21
|
Spherical,
|
|
21
22
|
Vector3,
|
|
22
23
|
} from 'three'
|
|
24
|
+
import {
|
|
25
|
+
type CameraPoseApplicationPlan,
|
|
26
|
+
normalizeCameraPose,
|
|
27
|
+
planCameraPoseApplication,
|
|
28
|
+
publishInitialCameraPose,
|
|
29
|
+
releaseCameraPoseEventSuppression,
|
|
30
|
+
stepCameraPoseInterpolation,
|
|
31
|
+
withCameraPoseDistance,
|
|
32
|
+
} from '../../lib/camera-pose'
|
|
23
33
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
34
|
+
import { editorOwnsOneFingerDrag } from '../../lib/touch-gesture-priority'
|
|
35
|
+
import { publishCameraPose } from '../../store/camera-pose-store'
|
|
24
36
|
import useEditor from '../../store/use-editor'
|
|
37
|
+
import {
|
|
38
|
+
useActiveHandleDrag,
|
|
39
|
+
useEndpointReshape,
|
|
40
|
+
useMovingNode,
|
|
41
|
+
} from '../../store/use-interaction-scope'
|
|
42
|
+
import { createCameraDraggingLifecycle } from './camera-dragging-lifecycle'
|
|
25
43
|
|
|
26
44
|
const currentTarget = new Vector3()
|
|
27
45
|
const tempBox = new Box3()
|
|
@@ -30,14 +48,11 @@ const tempDelta = new Vector3()
|
|
|
30
48
|
const tempPosition = new Vector3()
|
|
31
49
|
const tempSize = new Vector3()
|
|
32
50
|
const tempTarget = new Vector3()
|
|
33
|
-
const
|
|
34
|
-
const
|
|
51
|
+
const transitionFreezePosition = new Vector3()
|
|
52
|
+
const transitionFreezeTarget = new Vector3()
|
|
35
53
|
const keyboardPanSpherical = new Spherical()
|
|
36
54
|
const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
|
|
37
55
|
const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05
|
|
38
|
-
const NAVIGATION_SYNC_POSITION_EPSILON = 0.001
|
|
39
|
-
const NAVIGATION_SYNC_AZIMUTH_EPSILON = 0.0005
|
|
40
|
-
const NAVIGATION_SYNC_VIEW_WIDTH_EPSILON = 0.001
|
|
41
56
|
const KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND = 0.65
|
|
42
57
|
const KEYBOARD_PAN_MIN_SPEED = 2
|
|
43
58
|
const KEYBOARD_PAN_MAX_SPEED = 55
|
|
@@ -47,14 +62,6 @@ type CameraPoseSnapshot = {
|
|
|
47
62
|
position: [number, number, number]
|
|
48
63
|
target: [number, number, number]
|
|
49
64
|
}
|
|
50
|
-
type NavigationCameraPoseSnapshot = {
|
|
51
|
-
target: [number, number, number]
|
|
52
|
-
azimuth: number
|
|
53
|
-
viewWidth: number
|
|
54
|
-
}
|
|
55
|
-
type PendingNavigationCameraPoseSnapshot = NavigationCameraPoseSnapshot & {
|
|
56
|
-
publishOnComplete: boolean
|
|
57
|
-
}
|
|
58
65
|
type CameraViewWidthUpdate =
|
|
59
66
|
| { type: 'distance'; distance: number; viewWidth: number }
|
|
60
67
|
| { type: 'zoom'; viewWidth: number; zoom: number }
|
|
@@ -92,6 +99,21 @@ function restoreCameraPose(control: CameraControlsImpl, pose: CameraPoseSnapshot
|
|
|
92
99
|
)
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
function freezeCameraControlTransition(control: CameraControlsImpl) {
|
|
103
|
+
// `stop()` snaps to the transition endpoint, so replace it with the current pose instead.
|
|
104
|
+
control.getPosition(transitionFreezePosition, false)
|
|
105
|
+
control.getTarget(transitionFreezeTarget, false)
|
|
106
|
+
void control.setLookAt(
|
|
107
|
+
transitionFreezePosition.x,
|
|
108
|
+
transitionFreezePosition.y,
|
|
109
|
+
transitionFreezePosition.z,
|
|
110
|
+
transitionFreezeTarget.x,
|
|
111
|
+
transitionFreezeTarget.y,
|
|
112
|
+
transitionFreezeTarget.z,
|
|
113
|
+
false,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
95
117
|
function isEditableKeyboardTarget(target: EventTarget | null) {
|
|
96
118
|
return (
|
|
97
119
|
target instanceof HTMLInputElement ||
|
|
@@ -136,6 +158,10 @@ function isKeyboardPanKey(code: string): boolean {
|
|
|
136
158
|
return code === 'KeyW' || code === 'KeyA' || code === 'KeyS' || code === 'KeyD'
|
|
137
159
|
}
|
|
138
160
|
|
|
161
|
+
function hasKeyboardPanInput(state: KeyboardPanState): boolean {
|
|
162
|
+
return state.forward || state.backward || state.left || state.right
|
|
163
|
+
}
|
|
164
|
+
|
|
139
165
|
type CameraViewportSize = {
|
|
140
166
|
width: number
|
|
141
167
|
height: number
|
|
@@ -166,14 +192,6 @@ function getCameraViewWidth(camera: Camera, distance: number, size: CameraViewpo
|
|
|
166
192
|
return Math.max(0.001, distance)
|
|
167
193
|
}
|
|
168
194
|
|
|
169
|
-
function getAngleDeltaRadians(a: number, b: number) {
|
|
170
|
-
return Math.atan2(Math.sin(a - b), Math.cos(a - b))
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function nearestEquivalentRadians(angle: number, reference: number) {
|
|
174
|
-
return reference + getAngleDeltaRadians(angle, reference)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
195
|
function clampFinite(value: number, min: number, max: number) {
|
|
178
196
|
const resolvedMin = Number.isFinite(min) ? min : Number.NEGATIVE_INFINITY
|
|
179
197
|
const resolvedMax = Number.isFinite(max) ? max : Number.POSITIVE_INFINITY
|
|
@@ -198,21 +216,6 @@ function clampCameraControlZoom(control: CameraControlsImpl, zoom: number) {
|
|
|
198
216
|
)
|
|
199
217
|
}
|
|
200
218
|
|
|
201
|
-
function isCameraAtNavigationPose(
|
|
202
|
-
pose: NavigationCameraPoseSnapshot,
|
|
203
|
-
target: Vector3,
|
|
204
|
-
azimuth: number,
|
|
205
|
-
viewWidth: number,
|
|
206
|
-
) {
|
|
207
|
-
return (
|
|
208
|
-
Math.abs(pose.target[0] - target.x) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
209
|
-
Math.abs(pose.target[1] - target.y) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
210
|
-
Math.abs(pose.target[2] - target.z) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
211
|
-
Math.abs(getAngleDeltaRadians(pose.azimuth, azimuth)) < NAVIGATION_SYNC_AZIMUTH_EPSILON &&
|
|
212
|
-
Math.abs(pose.viewWidth - viewWidth) < NAVIGATION_SYNC_VIEW_WIDTH_EPSILON
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
219
|
function getCameraDistanceForViewWidth(
|
|
217
220
|
camera: Camera,
|
|
218
221
|
viewWidth: number,
|
|
@@ -267,14 +270,18 @@ function resolveCameraViewWidthUpdate(
|
|
|
267
270
|
return { type: 'none', viewWidth }
|
|
268
271
|
}
|
|
269
272
|
|
|
270
|
-
function applyCameraViewWidth(
|
|
273
|
+
function applyCameraViewWidth(
|
|
274
|
+
control: CameraControlsImpl,
|
|
275
|
+
update: CameraViewWidthUpdate,
|
|
276
|
+
enableTransition = true,
|
|
277
|
+
) {
|
|
271
278
|
if (update.type === 'distance') {
|
|
272
|
-
control.dollyTo(update.distance,
|
|
279
|
+
control.dollyTo(update.distance, enableTransition)
|
|
273
280
|
return
|
|
274
281
|
}
|
|
275
282
|
|
|
276
283
|
if (update.type === 'zoom') {
|
|
277
|
-
control.zoomTo(update.zoom,
|
|
284
|
+
control.zoomTo(update.zoom, enableTransition)
|
|
278
285
|
}
|
|
279
286
|
}
|
|
280
287
|
|
|
@@ -342,6 +349,13 @@ function useFirstPersonCameraPoseRestore(
|
|
|
342
349
|
|
|
343
350
|
export const CustomCameraControls = () => {
|
|
344
351
|
const controls = useRef<CameraControlsImpl | null>(null)
|
|
352
|
+
const pendingAppliedPose = useRef<CameraPoseApplicationPlan | null>(null)
|
|
353
|
+
const activePoseInterpolation = useRef<{
|
|
354
|
+
camera: Camera
|
|
355
|
+
control: CameraControlsImpl
|
|
356
|
+
plan: CameraPoseApplicationPlan
|
|
357
|
+
} | null>(null)
|
|
358
|
+
const suppressPoseEvents = useRef(false)
|
|
345
359
|
const keyboardPanKeys = useRef<KeyboardPanState>({
|
|
346
360
|
forward: false,
|
|
347
361
|
backward: false,
|
|
@@ -351,7 +365,6 @@ export const CustomCameraControls = () => {
|
|
|
351
365
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
352
366
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
353
367
|
const allowUndergroundCamera = useEditor((s) => s.allowUndergroundCamera)
|
|
354
|
-
const isFloorplanOpen = useEditor((s) => s.isFloorplanOpen)
|
|
355
368
|
const selection = useViewer((s) => s.selection)
|
|
356
369
|
const cameraMode = useViewer((state) => state.cameraMode)
|
|
357
370
|
const isRestoringFirstPersonPose = useFirstPersonCameraPoseRestore(
|
|
@@ -361,19 +374,21 @@ export const CustomCameraControls = () => {
|
|
|
361
374
|
)
|
|
362
375
|
const currentLevelId = selection.levelId
|
|
363
376
|
const firstLoad = useRef(true)
|
|
364
|
-
const lastPublishedNavigationSync = useRef<NavigationCameraPoseSnapshot | null>(null)
|
|
365
|
-
const pendingFloorplanNavigationPose = useRef<PendingNavigationCameraPoseSnapshot | null>(null)
|
|
366
|
-
const lastApplied2dNavigationRevision = useRef(0)
|
|
367
377
|
const maxPolarAngle =
|
|
368
378
|
!isPreviewMode && allowUndergroundCamera ? DEBUG_MAX_POLAR_ANGLE : DEFAULT_MAX_POLAR_ANGLE
|
|
369
|
-
const clearPendingFloorplanNavigationPose = useCallback(() => {
|
|
370
|
-
pendingFloorplanNavigationPose.current = null
|
|
371
|
-
}, [])
|
|
372
379
|
|
|
373
380
|
const camera = useThree((state) => state.camera)
|
|
374
381
|
const gl = useThree((state) => state.gl)
|
|
375
382
|
const raycaster = useThree((state) => state.raycaster)
|
|
376
383
|
const viewportSize = useThree((state) => state.size)
|
|
384
|
+
const cameraDraggingLifecycle = useMemo(
|
|
385
|
+
() =>
|
|
386
|
+
createCameraDraggingLifecycle({
|
|
387
|
+
setDragging: (dragging) => useViewer.getState().setCameraDragging(dragging),
|
|
388
|
+
}),
|
|
389
|
+
[],
|
|
390
|
+
)
|
|
391
|
+
useEffect(() => () => cameraDraggingLifecycle.end(), [cameraDraggingLifecycle])
|
|
377
392
|
useEffect(() => {
|
|
378
393
|
camera.layers.enable(EDITOR_LAYER)
|
|
379
394
|
camera.layers.enable(GRID_LAYER)
|
|
@@ -381,6 +396,136 @@ export const CustomCameraControls = () => {
|
|
|
381
396
|
raycaster.layers.enable(ZONE_LAYER)
|
|
382
397
|
}, [camera, raycaster])
|
|
383
398
|
|
|
399
|
+
const freezeActivePoseInterpolation = useCallback(() => {
|
|
400
|
+
const active = activePoseInterpolation.current
|
|
401
|
+
if (!active) return
|
|
402
|
+
|
|
403
|
+
activePoseInterpolation.current = null
|
|
404
|
+
freezeCameraControlTransition(active.control)
|
|
405
|
+
}, [])
|
|
406
|
+
|
|
407
|
+
const cancelPoseApplication = useCallback(() => {
|
|
408
|
+
pendingAppliedPose.current = null
|
|
409
|
+
try {
|
|
410
|
+
freezeActivePoseInterpolation()
|
|
411
|
+
} finally {
|
|
412
|
+
suppressPoseEvents.current = false
|
|
413
|
+
}
|
|
414
|
+
}, [freezeActivePoseInterpolation])
|
|
415
|
+
|
|
416
|
+
const beginLocalCameraInteraction = useCallback(
|
|
417
|
+
({ dragging = true }: { dragging?: boolean } = {}) => {
|
|
418
|
+
cancelPoseApplication()
|
|
419
|
+
if (dragging) cameraDraggingLifecycle.begin()
|
|
420
|
+
emitter.emit('camera-controls:interaction-start', undefined)
|
|
421
|
+
},
|
|
422
|
+
[cameraDraggingLifecycle, cancelPoseApplication],
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
const applyPendingPose = useCallback(() => {
|
|
426
|
+
if (isFirstPersonMode) {
|
|
427
|
+
cancelPoseApplication()
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const plan = pendingAppliedPose.current
|
|
432
|
+
const control = controls.current
|
|
433
|
+
|
|
434
|
+
const active = activePoseInterpolation.current
|
|
435
|
+
if (active && (active.camera !== camera || active.control !== control)) {
|
|
436
|
+
try {
|
|
437
|
+
freezeActivePoseInterpolation()
|
|
438
|
+
} finally {
|
|
439
|
+
if (!plan) suppressPoseEvents.current = false
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!(plan && control)) return
|
|
444
|
+
|
|
445
|
+
const cameraMatchesProjection =
|
|
446
|
+
(plan.pose.projection === 'perspective' && isPerspectiveCamera(camera)) ||
|
|
447
|
+
(plan.pose.projection === 'orthographic' && isOrthographicCamera(camera))
|
|
448
|
+
if (!cameraMatchesProjection) return
|
|
449
|
+
|
|
450
|
+
pendingAppliedPose.current = null
|
|
451
|
+
if (plan.perspectiveFov !== null && isPerspectiveCamera(camera)) {
|
|
452
|
+
camera.fov = plan.perspectiveFov
|
|
453
|
+
camera.updateProjectionMatrix()
|
|
454
|
+
}
|
|
455
|
+
let appliedPlan = plan
|
|
456
|
+
if (plan.pose.viewWidth !== undefined) {
|
|
457
|
+
const viewWidthUpdate = resolveCameraViewWidthUpdate(
|
|
458
|
+
control,
|
|
459
|
+
camera,
|
|
460
|
+
plan.pose.viewWidth,
|
|
461
|
+
viewportSize,
|
|
462
|
+
)
|
|
463
|
+
if (viewWidthUpdate.type === 'distance') {
|
|
464
|
+
appliedPlan = {
|
|
465
|
+
...plan,
|
|
466
|
+
pose: withCameraPoseDistance(plan.pose, viewWidthUpdate.distance),
|
|
467
|
+
}
|
|
468
|
+
} else if (viewWidthUpdate.type === 'zoom') {
|
|
469
|
+
applyCameraViewWidth(control, viewWidthUpdate, false)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
activePoseInterpolation.current = { camera, control, plan: appliedPlan }
|
|
474
|
+
}, [
|
|
475
|
+
camera,
|
|
476
|
+
cancelPoseApplication,
|
|
477
|
+
freezeActivePoseInterpolation,
|
|
478
|
+
isFirstPersonMode,
|
|
479
|
+
viewportSize,
|
|
480
|
+
])
|
|
481
|
+
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
applyPendingPose()
|
|
484
|
+
}, [applyPendingPose])
|
|
485
|
+
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
const handleAppliedPose = (pose: CameraPose) => {
|
|
488
|
+
if (isFirstPersonMode) return
|
|
489
|
+
|
|
490
|
+
const plan = planCameraPoseApplication(pose)
|
|
491
|
+
if (!plan) return
|
|
492
|
+
|
|
493
|
+
pendingAppliedPose.current = plan
|
|
494
|
+
suppressPoseEvents.current = true
|
|
495
|
+
|
|
496
|
+
if (useViewer.getState().cameraMode !== plan.pose.projection) {
|
|
497
|
+
freezeActivePoseInterpolation()
|
|
498
|
+
useViewer.getState().setCameraMode(plan.pose.projection)
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
applyPendingPose()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
emitter.on('camera-controls:apply-pose', handleAppliedPose)
|
|
506
|
+
emitter.on('camera-controls:cancel-pose', cancelPoseApplication)
|
|
507
|
+
return () => {
|
|
508
|
+
emitter.off('camera-controls:apply-pose', handleAppliedPose)
|
|
509
|
+
emitter.off('camera-controls:cancel-pose', cancelPoseApplication)
|
|
510
|
+
}
|
|
511
|
+
}, [applyPendingPose, cancelPoseApplication, freezeActivePoseInterpolation, isFirstPersonMode])
|
|
512
|
+
|
|
513
|
+
useEffect(() => cancelPoseApplication, [cancelPoseApplication])
|
|
514
|
+
|
|
515
|
+
useEffect(() => {
|
|
516
|
+
// Dev-only: deterministic camera poses for screenshot/automation tooling.
|
|
517
|
+
// A getter, not a snapshot — drei recreates the impl when the default
|
|
518
|
+
// camera changes, so a captured instance goes stale.
|
|
519
|
+
if (process.env.NODE_ENV !== 'development') return
|
|
520
|
+
const w = window as typeof window & {
|
|
521
|
+
__pascalCameraControls?: (() => CameraControlsImpl | null) | null
|
|
522
|
+
}
|
|
523
|
+
w.__pascalCameraControls = () => controls.current
|
|
524
|
+
return () => {
|
|
525
|
+
w.__pascalCameraControls = null
|
|
526
|
+
}
|
|
527
|
+
}, [])
|
|
528
|
+
|
|
384
529
|
useEffect(() => {
|
|
385
530
|
if (isPreviewMode || isFirstPersonMode || isRestoringFirstPersonPose()) return
|
|
386
531
|
let targetY = 0
|
|
@@ -393,19 +538,11 @@ export const CustomCameraControls = () => {
|
|
|
393
538
|
if (!controls.current) return
|
|
394
539
|
if (firstLoad.current) {
|
|
395
540
|
firstLoad.current = false
|
|
396
|
-
clearPendingFloorplanNavigationPose()
|
|
397
541
|
controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
|
|
398
542
|
}
|
|
399
543
|
controls.current.getTarget(currentTarget)
|
|
400
|
-
clearPendingFloorplanNavigationPose()
|
|
401
544
|
controls.current.moveTo(currentTarget.x, targetY, currentTarget.z, true)
|
|
402
|
-
}, [
|
|
403
|
-
clearPendingFloorplanNavigationPose,
|
|
404
|
-
currentLevelId,
|
|
405
|
-
isPreviewMode,
|
|
406
|
-
isFirstPersonMode,
|
|
407
|
-
isRestoringFirstPersonPose,
|
|
408
|
-
])
|
|
545
|
+
}, [currentLevelId, isPreviewMode, isFirstPersonMode, isRestoringFirstPersonPose])
|
|
409
546
|
|
|
410
547
|
useEffect(() => {
|
|
411
548
|
if (isFirstPersonMode || !controls.current) return
|
|
@@ -433,7 +570,6 @@ export const CustomCameraControls = () => {
|
|
|
433
570
|
controls.current.getTarget(tempTarget)
|
|
434
571
|
tempDelta.copy(tempCenter).sub(tempTarget)
|
|
435
572
|
|
|
436
|
-
clearPendingFloorplanNavigationPose()
|
|
437
573
|
controls.current.setLookAt(
|
|
438
574
|
tempPosition.x + tempDelta.x,
|
|
439
575
|
tempPosition.y + tempDelta.y,
|
|
@@ -444,120 +580,88 @@ export const CustomCameraControls = () => {
|
|
|
444
580
|
true,
|
|
445
581
|
)
|
|
446
582
|
},
|
|
447
|
-
[
|
|
583
|
+
[isPreviewMode, isFirstPersonMode],
|
|
448
584
|
)
|
|
449
585
|
|
|
450
|
-
|
|
451
|
-
if (isFirstPersonMode) return
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
camera,
|
|
470
|
-
pose.viewWidth,
|
|
471
|
-
viewportSize,
|
|
472
|
-
)
|
|
473
|
-
pendingFloorplanNavigationPose.current = {
|
|
474
|
-
target: [...pose.target],
|
|
475
|
-
azimuth: targetAzimuth,
|
|
476
|
-
viewWidth: viewWidthUpdate.viewWidth,
|
|
477
|
-
publishOnComplete:
|
|
478
|
-
Math.abs(viewWidthUpdate.viewWidth - pose.viewWidth) >=
|
|
479
|
-
NAVIGATION_SYNC_VIEW_WIDTH_EPSILON,
|
|
480
|
-
}
|
|
481
|
-
control.moveTo(pose.target[0], pose.target[1], pose.target[2], true)
|
|
482
|
-
control.rotateTo(targetAzimuth, control.polarAngle, true)
|
|
483
|
-
applyCameraViewWidth(control, viewWidthUpdate)
|
|
586
|
+
const publishCurrentPose = useCallback(() => {
|
|
587
|
+
if (isFirstPersonMode || suppressPoseEvents.current || !controls.current) return
|
|
588
|
+
|
|
589
|
+
controls.current.getPosition(tempPosition, false)
|
|
590
|
+
controls.current.getTarget(tempTarget, false)
|
|
591
|
+
const targetDistance = tempPosition.distanceTo(tempTarget)
|
|
592
|
+
const projection = isPerspectiveCamera(camera)
|
|
593
|
+
? 'perspective'
|
|
594
|
+
: isOrthographicCamera(camera)
|
|
595
|
+
? 'orthographic'
|
|
596
|
+
: null
|
|
597
|
+
if (!projection) return
|
|
598
|
+
|
|
599
|
+
const pose = normalizeCameraPose({
|
|
600
|
+
position: [tempPosition.x, tempPosition.y, tempPosition.z],
|
|
601
|
+
target: [tempTarget.x, tempTarget.y, tempTarget.z],
|
|
602
|
+
projection,
|
|
603
|
+
viewWidth: getCameraViewWidth(camera, targetDistance, viewportSize),
|
|
604
|
+
...(isPerspectiveCamera(camera) ? { fov: camera.fov } : {}),
|
|
484
605
|
})
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const publishCurrentNavigationPose = useCallback(() => {
|
|
488
|
-
if (isFirstPersonMode || !controls.current) return
|
|
489
|
-
|
|
490
|
-
controls.current.getTarget(syncTarget, false)
|
|
491
|
-
controls.current.getSpherical(syncSpherical, false)
|
|
492
|
-
const viewWidth = getCameraViewWidth(camera, syncSpherical.radius, viewportSize)
|
|
493
|
-
|
|
494
|
-
const pendingFloorplanPose = pendingFloorplanNavigationPose.current
|
|
495
|
-
if (pendingFloorplanPose) {
|
|
496
|
-
// The camera is still damping toward a 2D-originated pose; do not echo
|
|
497
|
-
// intermediate 3D poses back into the floorplan.
|
|
498
|
-
if (
|
|
499
|
-
isCameraAtNavigationPose(pendingFloorplanPose, syncTarget, syncSpherical.theta, viewWidth)
|
|
500
|
-
) {
|
|
501
|
-
lastPublishedNavigationSync.current = pendingFloorplanPose
|
|
502
|
-
pendingFloorplanNavigationPose.current = null
|
|
503
|
-
if (pendingFloorplanPose.publishOnComplete) {
|
|
504
|
-
useEditor.getState().publishNavigationSyncPose({
|
|
505
|
-
source: '3d',
|
|
506
|
-
target: [
|
|
507
|
-
pendingFloorplanPose.target[0],
|
|
508
|
-
pendingFloorplanPose.target[1],
|
|
509
|
-
pendingFloorplanPose.target[2],
|
|
510
|
-
],
|
|
511
|
-
azimuth: pendingFloorplanPose.azimuth,
|
|
512
|
-
viewWidth: pendingFloorplanPose.viewWidth,
|
|
513
|
-
})
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return
|
|
606
|
+
if (pose) {
|
|
607
|
+
publishCameraPose(pose)
|
|
517
608
|
}
|
|
518
|
-
|
|
519
|
-
const previous = lastPublishedNavigationSync.current
|
|
520
|
-
if (
|
|
521
|
-
previous &&
|
|
522
|
-
Math.abs(previous.target[0] - syncTarget.x) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
523
|
-
Math.abs(previous.target[1] - syncTarget.y) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
524
|
-
Math.abs(previous.target[2] - syncTarget.z) < NAVIGATION_SYNC_POSITION_EPSILON &&
|
|
525
|
-
Math.abs(getAngleDeltaRadians(previous.azimuth, syncSpherical.theta)) <
|
|
526
|
-
NAVIGATION_SYNC_AZIMUTH_EPSILON &&
|
|
527
|
-
Math.abs(previous.viewWidth - viewWidth) < NAVIGATION_SYNC_VIEW_WIDTH_EPSILON
|
|
528
|
-
) {
|
|
529
|
-
return
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
lastPublishedNavigationSync.current = {
|
|
533
|
-
target: [syncTarget.x, syncTarget.y, syncTarget.z],
|
|
534
|
-
azimuth: syncSpherical.theta,
|
|
535
|
-
viewWidth,
|
|
536
|
-
}
|
|
537
|
-
useEditor.getState().publishNavigationSyncPose({
|
|
538
|
-
source: '3d',
|
|
539
|
-
target: [syncTarget.x, syncTarget.y, syncTarget.z],
|
|
540
|
-
azimuth: syncSpherical.theta,
|
|
541
|
-
viewWidth,
|
|
542
|
-
})
|
|
543
609
|
}, [camera, isFirstPersonMode, viewportSize])
|
|
544
610
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const frame = requestAnimationFrame(() => {
|
|
549
|
-
lastPublishedNavigationSync.current = null
|
|
550
|
-
publishCurrentNavigationPose()
|
|
551
|
-
})
|
|
611
|
+
const handleCameraUpdate = useCallback(() => {
|
|
612
|
+
publishCurrentPose()
|
|
613
|
+
}, [publishCurrentPose])
|
|
552
614
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}, [currentLevelId, isFirstPersonMode, isFloorplanOpen, publishCurrentNavigationPose])
|
|
615
|
+
useEffect(() => {
|
|
616
|
+
publishInitialCameraPose(publishCurrentPose)
|
|
617
|
+
}, [publishCurrentPose])
|
|
557
618
|
|
|
558
619
|
useFrame((_, delta) => {
|
|
559
620
|
if (isFirstPersonMode || !controls.current) return
|
|
560
621
|
|
|
622
|
+
const activePose = activePoseInterpolation.current
|
|
623
|
+
if (activePose) {
|
|
624
|
+
const control = controls.current
|
|
625
|
+
if (activePose.camera !== camera || activePose.control !== control) {
|
|
626
|
+
try {
|
|
627
|
+
freezeActivePoseInterpolation()
|
|
628
|
+
} finally {
|
|
629
|
+
if (!pendingAppliedPose.current) suppressPoseEvents.current = false
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
control.getPosition(tempPosition, false)
|
|
633
|
+
control.getTarget(tempTarget, false)
|
|
634
|
+
const step = stepCameraPoseInterpolation(
|
|
635
|
+
[tempPosition.x, tempPosition.y, tempPosition.z],
|
|
636
|
+
[tempTarget.x, tempTarget.y, tempTarget.z],
|
|
637
|
+
activePose.plan.pose,
|
|
638
|
+
delta,
|
|
639
|
+
)
|
|
640
|
+
try {
|
|
641
|
+
// Transition-enabled calls retain one rest listener each, so this frame loop owns smoothing.
|
|
642
|
+
void control.setLookAt(
|
|
643
|
+
step.position[0],
|
|
644
|
+
step.position[1],
|
|
645
|
+
step.position[2],
|
|
646
|
+
step.target[0],
|
|
647
|
+
step.target[1],
|
|
648
|
+
step.target[2],
|
|
649
|
+
false,
|
|
650
|
+
)
|
|
651
|
+
control.update(0)
|
|
652
|
+
} catch {
|
|
653
|
+
if (activePoseInterpolation.current === activePose) {
|
|
654
|
+
activePoseInterpolation.current = null
|
|
655
|
+
releaseCameraPoseEventSuppression(suppressPoseEvents, publishCurrentPose)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (step.settled && activePoseInterpolation.current === activePose) {
|
|
659
|
+
activePoseInterpolation.current = null
|
|
660
|
+
releaseCameraPoseEventSuppression(suppressPoseEvents, publishCurrentPose)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
561
665
|
const panKeys = keyboardPanKeys.current
|
|
562
666
|
const horizontal = (panKeys.right ? 1 : 0) - (panKeys.left ? 1 : 0)
|
|
563
667
|
const vertical = (panKeys.forward ? 1 : 0) - (panKeys.backward ? 1 : 0)
|
|
@@ -573,10 +677,9 @@ export const CustomCameraControls = () => {
|
|
|
573
677
|
)
|
|
574
678
|
const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
|
|
575
679
|
|
|
576
|
-
pendingFloorplanNavigationPose.current = null
|
|
577
680
|
if (horizontal !== 0) control.truck(horizontal * step, 0, true)
|
|
578
681
|
if (vertical !== 0) control.forward(vertical * step, true)
|
|
579
|
-
})
|
|
682
|
+
}, 0)
|
|
580
683
|
|
|
581
684
|
// Configure mouse buttons based on control mode and camera mode
|
|
582
685
|
const mouseButtons = useMemo(() => {
|
|
@@ -596,11 +699,11 @@ export const CustomCameraControls = () => {
|
|
|
596
699
|
|
|
597
700
|
// Touch gestures (mobile / trackpad).
|
|
598
701
|
// - One finger drag → rotate by default (much easier on a phone), but
|
|
599
|
-
// falls back to NONE while the
|
|
600
|
-
// placing/moving something
|
|
601
|
-
// so the editor's pointer handlers (place tool,
|
|
602
|
-
// drag-to-move endpoint, marquee selection drag
|
|
603
|
-
// keep priority over the camera.
|
|
702
|
+
// falls back to NONE while the editor owns the drag —
|
|
703
|
+
// placing/moving something, box-select, or a brush mode
|
|
704
|
+
// — so the editor's pointer handlers (place tool,
|
|
705
|
+
// drag-to-move endpoint, marquee selection drag, paint
|
|
706
|
+
// and sculpt strokes) keep priority over the camera.
|
|
604
707
|
// In preview mode it's TOUCH_TRUCK (pan), matching
|
|
605
708
|
// preview's left = SCREEN_PAN.
|
|
606
709
|
// - Two finger pinch → zoom + pan together (TOUCH_DOLLY_TRUCK for
|
|
@@ -611,19 +714,20 @@ export const CustomCameraControls = () => {
|
|
|
611
714
|
const tool = useEditor((s) => s.tool)
|
|
612
715
|
const mode = useEditor((s) => s.mode)
|
|
613
716
|
const selectionTool = useEditor((s) => s.floorplanSelectionTool)
|
|
614
|
-
const movingNode =
|
|
615
|
-
const
|
|
616
|
-
const
|
|
617
|
-
const activeHandleDrag = useEditor((s) => s.activeHandleDrag)
|
|
717
|
+
const movingNode = useMovingNode()
|
|
718
|
+
const endpointReshape = useEndpointReshape()
|
|
719
|
+
const activeHandleDrag = useActiveHandleDrag()
|
|
618
720
|
const isBoxSelectActive = mode === 'select' && selectionTool === 'marquee'
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
721
|
+
// The mode term lives in `editorOwnsOneFingerDrag` rather than in this OR: a
|
|
722
|
+
// brush mode owns the drag the way an armed tool does, but none of the
|
|
723
|
+
// transient terms notice it (entering paint or sculpt *clears* `tool`, and its
|
|
724
|
+
// scope is `painting`/`sculpting`, not `handle-drag`).
|
|
725
|
+
const isInteracting = editorOwnsOneFingerDrag({
|
|
726
|
+
mode,
|
|
727
|
+
activeGesture: Boolean(
|
|
728
|
+
tool || movingNode || endpointReshape || activeHandleDrag || isBoxSelectActive,
|
|
729
|
+
),
|
|
730
|
+
})
|
|
627
731
|
const touches = useMemo(() => {
|
|
628
732
|
const twoFingerAction =
|
|
629
733
|
cameraMode === 'orthographic'
|
|
@@ -730,8 +834,8 @@ export const CustomCameraControls = () => {
|
|
|
730
834
|
!(event.metaKey || event.ctrlKey || event.altKey) &&
|
|
731
835
|
!isEditableKeyboardTarget(event.target)
|
|
732
836
|
) {
|
|
733
|
-
setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
|
|
734
|
-
|
|
837
|
+
const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
|
|
838
|
+
if (changed) beginLocalCameraInteraction()
|
|
735
839
|
event.preventDefault()
|
|
736
840
|
event.stopPropagation()
|
|
737
841
|
}
|
|
@@ -763,6 +867,9 @@ export const CustomCameraControls = () => {
|
|
|
763
867
|
if (isKeyboardPanKey(event.code)) {
|
|
764
868
|
const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, false)
|
|
765
869
|
if (changed) {
|
|
870
|
+
if (!hasKeyboardPanInput(keyboardPanKeys.current)) {
|
|
871
|
+
cameraDraggingLifecycle.end()
|
|
872
|
+
}
|
|
766
873
|
event.preventDefault()
|
|
767
874
|
event.stopPropagation()
|
|
768
875
|
}
|
|
@@ -794,7 +901,6 @@ export const CustomCameraControls = () => {
|
|
|
794
901
|
|
|
795
902
|
const onPointerDown = (event: PointerEvent) => {
|
|
796
903
|
if (!(event.target instanceof Node) || !gl.domElement.contains(event.target)) return
|
|
797
|
-
pendingFloorplanNavigationPose.current = null
|
|
798
904
|
if (event.button !== 1 && !(event.button === 0 && keyState.space)) return
|
|
799
905
|
|
|
800
906
|
panPointerId = event.pointerId
|
|
@@ -803,7 +909,8 @@ export const CustomCameraControls = () => {
|
|
|
803
909
|
}
|
|
804
910
|
|
|
805
911
|
const onWheel = () => {
|
|
806
|
-
|
|
912
|
+
beginLocalCameraInteraction()
|
|
913
|
+
cameraDraggingLifecycle.scheduleEnd()
|
|
807
914
|
}
|
|
808
915
|
|
|
809
916
|
const onPointerUp = (event: PointerEvent) => {
|
|
@@ -822,6 +929,7 @@ export const CustomCameraControls = () => {
|
|
|
822
929
|
panPointerId = null
|
|
823
930
|
panPointerButton = null
|
|
824
931
|
clearNavigationCursor()
|
|
932
|
+
cameraDraggingLifecycle.end()
|
|
825
933
|
updateConfig()
|
|
826
934
|
}
|
|
827
935
|
|
|
@@ -831,7 +939,7 @@ export const CustomCameraControls = () => {
|
|
|
831
939
|
window.addEventListener('pointerup', onPointerUp, true)
|
|
832
940
|
window.addEventListener('pointercancel', onPointerUp, true)
|
|
833
941
|
window.addEventListener('blur', onBlur)
|
|
834
|
-
gl.domElement.addEventListener('wheel', onWheel, { passive: true })
|
|
942
|
+
gl.domElement.addEventListener('wheel', onWheel, { capture: true, passive: true })
|
|
835
943
|
updateConfig()
|
|
836
944
|
|
|
837
945
|
return () => {
|
|
@@ -841,11 +949,30 @@ export const CustomCameraControls = () => {
|
|
|
841
949
|
window.removeEventListener('pointerup', onPointerUp, true)
|
|
842
950
|
window.removeEventListener('pointercancel', onPointerUp, true)
|
|
843
951
|
window.removeEventListener('blur', onBlur)
|
|
844
|
-
gl.domElement.removeEventListener('wheel', onWheel)
|
|
952
|
+
gl.domElement.removeEventListener('wheel', onWheel, true)
|
|
845
953
|
clearKeyboardPanKeys()
|
|
846
954
|
clearNavigationCursor()
|
|
955
|
+
cameraDraggingLifecycle.end()
|
|
847
956
|
}
|
|
848
|
-
}, [
|
|
957
|
+
}, [
|
|
958
|
+
beginLocalCameraInteraction,
|
|
959
|
+
cameraDraggingLifecycle,
|
|
960
|
+
cameraMode,
|
|
961
|
+
gl,
|
|
962
|
+
isPreviewMode,
|
|
963
|
+
isFirstPersonMode,
|
|
964
|
+
])
|
|
965
|
+
|
|
966
|
+
// `controlstart` fires only for user pointer interactions. Pointerdowns
|
|
967
|
+
// mapped to ACTION.NONE must not flag the camera as dragging because no
|
|
968
|
+
// rest/sleep event follows to clear the flag.
|
|
969
|
+
const handleControlStart = useCallback(() => {
|
|
970
|
+
beginLocalCameraInteraction({
|
|
971
|
+
dragging: controls.current
|
|
972
|
+
? controls.current.currentAction !== CameraControlsImpl.ACTION.NONE
|
|
973
|
+
: false,
|
|
974
|
+
})
|
|
975
|
+
}, [beginLocalCameraInteraction])
|
|
849
976
|
|
|
850
977
|
// Preview mode: auto-navigate camera to selected node (viewer behavior)
|
|
851
978
|
const previewTargetNodeId = isPreviewMode
|
|
@@ -1052,7 +1179,6 @@ export const CustomCameraControls = () => {
|
|
|
1052
1179
|
if (!node?.camera) return
|
|
1053
1180
|
const { position, target } = node.camera
|
|
1054
1181
|
|
|
1055
|
-
clearPendingFloorplanNavigationPose()
|
|
1056
1182
|
controls.current.setLookAt(
|
|
1057
1183
|
position[0],
|
|
1058
1184
|
position[1],
|
|
@@ -1073,7 +1199,6 @@ export const CustomCameraControls = () => {
|
|
|
1073
1199
|
// Otherwise, go to top view (0°)
|
|
1074
1200
|
const targetAngle = currentPolarAngle < 0.1 ? Math.PI / 4 : 0
|
|
1075
1201
|
|
|
1076
|
-
clearPendingFloorplanNavigationPose()
|
|
1077
1202
|
controls.current.rotatePolarTo(targetAngle, true)
|
|
1078
1203
|
}
|
|
1079
1204
|
|
|
@@ -1086,7 +1211,6 @@ export const CustomCameraControls = () => {
|
|
|
1086
1211
|
const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
|
|
1087
1212
|
const target = rounded - Math.PI / 2
|
|
1088
1213
|
|
|
1089
|
-
clearPendingFloorplanNavigationPose()
|
|
1090
1214
|
controls.current.rotateTo(target, currentPolar, true)
|
|
1091
1215
|
}
|
|
1092
1216
|
|
|
@@ -1099,7 +1223,6 @@ export const CustomCameraControls = () => {
|
|
|
1099
1223
|
const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
|
|
1100
1224
|
const target = rounded + Math.PI / 2
|
|
1101
1225
|
|
|
1102
|
-
clearPendingFloorplanNavigationPose()
|
|
1103
1226
|
controls.current.rotateTo(target, currentPolar, true)
|
|
1104
1227
|
}
|
|
1105
1228
|
|
|
@@ -1111,7 +1234,6 @@ export const CustomCameraControls = () => {
|
|
|
1111
1234
|
if (isFirstPersonMode || !controls.current || isPreviewMode) return
|
|
1112
1235
|
if (!bounds) {
|
|
1113
1236
|
// Restore default framing pose when no bounds were computed.
|
|
1114
|
-
clearPendingFloorplanNavigationPose()
|
|
1115
1237
|
controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
|
|
1116
1238
|
return
|
|
1117
1239
|
}
|
|
@@ -1122,7 +1244,6 @@ export const CustomCameraControls = () => {
|
|
|
1122
1244
|
const maxExtent = Math.max(w, d)
|
|
1123
1245
|
const distance = Math.max(maxExtent * 1.4, 15)
|
|
1124
1246
|
const height = Math.max(maxExtent * 0.8, 10)
|
|
1125
|
-
clearPendingFloorplanNavigationPose()
|
|
1126
1247
|
controls.current.setLookAt(cx + distance * 0.7, height, cz + distance * 0.7, cx, 0, cz, true)
|
|
1127
1248
|
}
|
|
1128
1249
|
|
|
@@ -1143,23 +1264,33 @@ export const CustomCameraControls = () => {
|
|
|
1143
1264
|
emitter.off('camera-controls:orbit-ccw', handleOrbitCCW)
|
|
1144
1265
|
emitter.off('camera-controls:fit-scene', handleFitScene)
|
|
1145
1266
|
}
|
|
1146
|
-
}, [
|
|
1267
|
+
}, [focusNode, isPreviewMode, isFirstPersonMode])
|
|
1147
1268
|
|
|
1148
1269
|
const onTransitionStart = useCallback(() => {
|
|
1149
|
-
|
|
1150
|
-
}, [])
|
|
1270
|
+
cameraDraggingLifecycle.begin()
|
|
1271
|
+
}, [cameraDraggingLifecycle])
|
|
1151
1272
|
|
|
1152
1273
|
const onRest = useCallback(() => {
|
|
1153
|
-
|
|
1154
|
-
}, [])
|
|
1274
|
+
cameraDraggingLifecycle.end()
|
|
1275
|
+
}, [cameraDraggingLifecycle])
|
|
1276
|
+
|
|
1277
|
+
const onControlEnd = useCallback(() => {
|
|
1278
|
+
// A mapped-button tap with zero camera movement never wakes the
|
|
1279
|
+
// controls, so no rest/sleep follows — clear the dragging flag on
|
|
1280
|
+
// release. While damping is still settling (`active`), rest/sleep
|
|
1281
|
+
// clears it instead.
|
|
1282
|
+
if (!controls.current?.active) {
|
|
1283
|
+
cameraDraggingLifecycle.end()
|
|
1284
|
+
}
|
|
1285
|
+
}, [cameraDraggingLifecycle])
|
|
1155
1286
|
|
|
1156
1287
|
// Preset capture mode frames a single subtree (often a 0.3–2m preset),
|
|
1157
|
-
// so the default
|
|
1288
|
+
// so the default 2m minDistance prevents the user from getting close
|
|
1158
1289
|
// enough to compose a good thumbnail. Relax the clamp to 0.5m while
|
|
1159
1290
|
// capturing presets; reset on exit so general editing keeps the looser
|
|
1160
1291
|
// navigation guardrails.
|
|
1161
1292
|
const isPresetCapture = captureMode.mode === 'preset'
|
|
1162
|
-
const minDistance = isPresetCapture ? 0.5 :
|
|
1293
|
+
const minDistance = isPresetCapture ? 0.5 : 2
|
|
1163
1294
|
|
|
1164
1295
|
if (isFirstPersonMode) {
|
|
1165
1296
|
return null
|
|
@@ -1173,7 +1304,9 @@ export const CustomCameraControls = () => {
|
|
|
1173
1304
|
minDistance={minDistance}
|
|
1174
1305
|
minPolarAngle={0}
|
|
1175
1306
|
mouseButtons={mouseButtons}
|
|
1176
|
-
|
|
1307
|
+
onControlEnd={onControlEnd}
|
|
1308
|
+
onControlStart={handleControlStart}
|
|
1309
|
+
onUpdate={handleCameraUpdate}
|
|
1177
1310
|
onRest={onRest}
|
|
1178
1311
|
onSleep={onRest}
|
|
1179
1312
|
onTransitionStart={onTransitionStart}
|