@pascal-app/editor 1.0.3 → 1.0.4
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 +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type AnyNode,
|
|
4
5
|
type AnyNodeId,
|
|
5
6
|
type CameraControlEvent,
|
|
6
7
|
type CameraControlFitSceneEvent,
|
|
8
|
+
type CameraControlFrameEvent,
|
|
7
9
|
type CameraPose,
|
|
8
10
|
emitter,
|
|
9
11
|
sceneRegistry,
|
|
10
12
|
useScene,
|
|
11
13
|
} from '@pascal-app/core'
|
|
12
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
GRID_LAYER,
|
|
16
|
+
getLevelPresentationY,
|
|
17
|
+
useSceneGroundReplacement,
|
|
18
|
+
useViewer,
|
|
19
|
+
ZONE_LAYER,
|
|
20
|
+
} from '@pascal-app/viewer'
|
|
13
21
|
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
|
14
22
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
15
23
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
@@ -33,24 +41,35 @@ import {
|
|
|
33
41
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
34
42
|
import {
|
|
35
43
|
acceptsKeyboardPan,
|
|
44
|
+
advanceKeyboardPanMotion,
|
|
36
45
|
clearKeyboardPanKeys,
|
|
46
|
+
createKeyboardPanMotion,
|
|
37
47
|
hasKeyboardPanInput,
|
|
38
48
|
isEditableKeyboardTarget,
|
|
39
49
|
isKeyboardPanKey,
|
|
40
50
|
type KeyboardPanState,
|
|
41
|
-
keyboardPanDirection,
|
|
42
51
|
keyboardPanSpeed,
|
|
43
52
|
setKeyboardPanKey,
|
|
53
|
+
syncKeyboardPanMotion,
|
|
44
54
|
} from '../../lib/keyboard-pan'
|
|
55
|
+
import {
|
|
56
|
+
MAX_ORBIT_POLAR_ANGLE,
|
|
57
|
+
ORBIT_FLOOR_CLEARANCE,
|
|
58
|
+
ORBIT_TARGET_HEIGHT,
|
|
59
|
+
} from '../../lib/orbit-floor-clearance'
|
|
45
60
|
import { editorOwnsOneFingerDrag } from '../../lib/touch-gesture-priority'
|
|
61
|
+
import { frameViewerCamera } from '../../lib/viewer-selection'
|
|
46
62
|
import { publishCameraPose } from '../../store/camera-pose-store'
|
|
47
|
-
import useEditor from '../../store/use-editor'
|
|
63
|
+
import useEditor, { takePreviewCameraRestore } from '../../store/use-editor'
|
|
48
64
|
import {
|
|
49
65
|
useActiveHandleDrag,
|
|
50
66
|
useEndpointReshape,
|
|
51
67
|
useMovingNode,
|
|
52
68
|
} from '../../store/use-interaction-scope'
|
|
69
|
+
import { visibleScene } from '../../store/view-layout'
|
|
53
70
|
import { createCameraDraggingLifecycle } from './camera-dragging-lifecycle'
|
|
71
|
+
import { FloorAwareCameraControls } from './floor-aware-camera-controls'
|
|
72
|
+
import { FloorSurface } from './floor-surface'
|
|
54
73
|
|
|
55
74
|
const currentTarget = new Vector3()
|
|
56
75
|
const tempBox = new Box3()
|
|
@@ -62,11 +81,37 @@ const tempTarget = new Vector3()
|
|
|
62
81
|
const transitionFreezePosition = new Vector3()
|
|
63
82
|
const transitionFreezeTarget = new Vector3()
|
|
64
83
|
const keyboardPanSpherical = new Spherical()
|
|
84
|
+
/** Mouse/touch orbit speed (camera-controls default 1): slower for precise looks. */
|
|
85
|
+
const ORBIT_ROTATE_SPEED = 0.6
|
|
65
86
|
// In 2D-only view the canvas is paused, so the floor plan drives WASD, orbit and
|
|
66
87
|
// top view itself (`floorplan-panel.tsx`) and the camera stands down.
|
|
67
|
-
const planOwnsNavigation = () => useEditor.getState()
|
|
88
|
+
const planOwnsNavigation = () => visibleScene(useEditor.getState()) === '2d'
|
|
68
89
|
const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
|
|
69
|
-
|
|
90
|
+
// Focusing one of these aims at its floor (raised to the orbit's pivot
|
|
91
|
+
// height), not at the middle of its bounding box.
|
|
92
|
+
const GROUND_FOCUS_TYPES = new Set(['site', 'building', 'level', 'zone', 'slab'])
|
|
93
|
+
|
|
94
|
+
type LevelMode = ReturnType<typeof useViewer.getState>['levelMode']
|
|
95
|
+
|
|
96
|
+
/** Where the orbit pivot rests on a level: its floor plus the pivot height. */
|
|
97
|
+
function orbitGroundY(
|
|
98
|
+
levelId: string | null | undefined,
|
|
99
|
+
levelMode: LevelMode = useViewer.getState().levelMode,
|
|
100
|
+
) {
|
|
101
|
+
const floorY = levelId ? getLevelPresentationY(levelId, useScene.getState().nodes, levelMode) : 0
|
|
102
|
+
return floorY + ORBIT_TARGET_HEIGHT
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function levelOf(nodeId: string): string | null {
|
|
106
|
+
const nodes = useScene.getState().nodes
|
|
107
|
+
let node: AnyNode | undefined = nodes[nodeId as AnyNodeId]
|
|
108
|
+
while (node) {
|
|
109
|
+
if (node.type === 'level') return node.id
|
|
110
|
+
node = node.parentId ? nodes[node.parentId as AnyNodeId] : undefined
|
|
111
|
+
}
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
70
115
|
type CameraMode = ReturnType<typeof useViewer.getState>['cameraMode']
|
|
71
116
|
type CameraPoseSnapshot = {
|
|
72
117
|
mode: CameraMode
|
|
@@ -311,7 +356,7 @@ function useFirstPersonCameraPoseRestore(
|
|
|
311
356
|
}
|
|
312
357
|
|
|
313
358
|
export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) => {
|
|
314
|
-
const controls = useRef<
|
|
359
|
+
const controls = useRef<FloorAwareCameraControls | null>(null)
|
|
315
360
|
const pendingAppliedPose = useRef<CameraPoseApplicationPlan | null>(null)
|
|
316
361
|
const activePoseInterpolation = useRef<{
|
|
317
362
|
camera: Camera
|
|
@@ -325,6 +370,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
325
370
|
left: false,
|
|
326
371
|
right: false,
|
|
327
372
|
})
|
|
373
|
+
const keyboardPanMotion = useRef(createKeyboardPanMotion())
|
|
328
374
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
329
375
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
330
376
|
const allowUndergroundCamera = useEditor((s) => s.allowUndergroundCamera)
|
|
@@ -337,12 +383,24 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
337
383
|
isFirstPersonMode,
|
|
338
384
|
cameraMode,
|
|
339
385
|
)
|
|
340
|
-
|
|
386
|
+
// Selecting the building or site clears the level; the camera keeps
|
|
387
|
+
// standing on the last level rather than dropping to the ground floor.
|
|
388
|
+
const lastLevelId = useRef<string | null>(null)
|
|
389
|
+
if (selection.levelId) lastLevelId.current = selection.levelId
|
|
390
|
+
const currentLevelId = lastLevelId.current
|
|
341
391
|
const firstLoad = useRef(true)
|
|
392
|
+
const floorY = useScene((state) =>
|
|
393
|
+
currentLevelId ? getLevelPresentationY(currentLevelId, state.nodes, levelMode) : 0,
|
|
394
|
+
)
|
|
395
|
+
const allowUnderground = !isPreviewMode && allowUndergroundCamera
|
|
396
|
+
const floorMinY = allowUnderground ? null : floorY + ORBIT_FLOOR_CLEARANCE
|
|
342
397
|
const maxPolarAngle =
|
|
343
|
-
|
|
398
|
+
allowUnderground || cameraMode === 'perspective'
|
|
399
|
+
? MAX_ORBIT_POLAR_ANGLE
|
|
400
|
+
: DEFAULT_MAX_POLAR_ANGLE
|
|
344
401
|
|
|
345
402
|
const camera = useThree((state) => state.camera)
|
|
403
|
+
const scene = useThree((state) => state.scene)
|
|
346
404
|
const gl = useThree((state) => state.gl)
|
|
347
405
|
const raycaster = useThree((state) => state.raycaster)
|
|
348
406
|
const viewportSize = useThree((state) => state.size)
|
|
@@ -381,7 +439,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
381
439
|
const beginLocalCameraInteraction = useCallback(
|
|
382
440
|
({ dragging = true }: { dragging?: boolean } = {}) => {
|
|
383
441
|
cancelPoseApplication()
|
|
384
|
-
if (dragging)
|
|
442
|
+
if (dragging) {
|
|
443
|
+
controls.current?.releaseWrittenPose()
|
|
444
|
+
cameraDraggingLifecycle.begin()
|
|
445
|
+
}
|
|
385
446
|
emitter.emit('camera-controls:interaction-start', undefined)
|
|
386
447
|
},
|
|
387
448
|
[cameraDraggingLifecycle, cancelPoseApplication],
|
|
@@ -469,11 +530,21 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
469
530
|
|
|
470
531
|
emitter.on('camera-controls:apply-pose', handleAppliedPose)
|
|
471
532
|
emitter.on('camera-controls:cancel-pose', cancelPoseApplication)
|
|
533
|
+
if (!isPreviewMode) {
|
|
534
|
+
const restore = takePreviewCameraRestore()
|
|
535
|
+
if (restore) handleAppliedPose(restore)
|
|
536
|
+
}
|
|
472
537
|
return () => {
|
|
473
538
|
emitter.off('camera-controls:apply-pose', handleAppliedPose)
|
|
474
539
|
emitter.off('camera-controls:cancel-pose', cancelPoseApplication)
|
|
475
540
|
}
|
|
476
|
-
}, [
|
|
541
|
+
}, [
|
|
542
|
+
applyPendingPose,
|
|
543
|
+
cancelPoseApplication,
|
|
544
|
+
freezeActivePoseInterpolation,
|
|
545
|
+
isFirstPersonMode,
|
|
546
|
+
isPreviewMode,
|
|
547
|
+
])
|
|
477
548
|
|
|
478
549
|
useEffect(() => cancelPoseApplication, [cancelPoseApplication])
|
|
479
550
|
|
|
@@ -484,33 +555,60 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
484
555
|
if (process.env.NODE_ENV !== 'development') return
|
|
485
556
|
const w = window as typeof window & {
|
|
486
557
|
__pascalCameraControls?: (() => CameraControlsImpl | null) | null
|
|
558
|
+
__pascalScene?: (() => unknown) | null
|
|
559
|
+
__pascalCapture?: (() => Promise<string>) | null
|
|
487
560
|
}
|
|
488
561
|
w.__pascalCameraControls = () => controls.current
|
|
562
|
+
// the live three.js scene, for the same tooling (a probe can walk the
|
|
563
|
+
// meshes for an empty geometry the WebGPU validator complains about)
|
|
564
|
+
w.__pascalScene = () => scene
|
|
565
|
+
// a fresh frame read back as a PNG data URL: the canvas is cleared after
|
|
566
|
+
// each presented frame, so a capture has to render and read in one task
|
|
567
|
+
w.__pascalCapture = async () => {
|
|
568
|
+
const r = gl as unknown as {
|
|
569
|
+
renderAsync?: (s: unknown, c: unknown) => Promise<void>
|
|
570
|
+
render: (s: unknown, c: unknown) => void
|
|
571
|
+
domElement: HTMLCanvasElement
|
|
572
|
+
}
|
|
573
|
+
if (r.renderAsync) await r.renderAsync(scene, camera)
|
|
574
|
+
else r.render(scene, camera)
|
|
575
|
+
return r.domElement.toDataURL('image/png')
|
|
576
|
+
}
|
|
489
577
|
return () => {
|
|
490
578
|
w.__pascalCameraControls = null
|
|
579
|
+
w.__pascalScene = null
|
|
580
|
+
w.__pascalCapture = null
|
|
491
581
|
}
|
|
492
|
-
}, [])
|
|
582
|
+
}, [scene, gl, camera])
|
|
493
583
|
|
|
584
|
+
// `camera` too: drei builds a fresh controls instance (pivot at the origin)
|
|
585
|
+
// when the default camera mounts or changes; it is rested on the floor too.
|
|
586
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: camera re-runs this on a new instance
|
|
494
587
|
useEffect(() => {
|
|
495
588
|
if (isPreviewMode || isFirstPersonMode || isRestoringFirstPersonPose()) return
|
|
496
589
|
// Analytic destination, not `sceneRegistry` mesh position: a level created
|
|
497
590
|
// this frame still sits at y=0 (LevelSystem lerps it later), and a mode
|
|
498
591
|
// switch leaves every level mid-lerp — the camera must pan to where the
|
|
499
592
|
// level will settle, in the CURRENT presentation mode.
|
|
500
|
-
const targetY = currentLevelId
|
|
501
|
-
? getLevelPresentationY(currentLevelId, useScene.getState().nodes, levelMode)
|
|
502
|
-
: 0
|
|
593
|
+
const targetY = orbitGroundY(currentLevelId, levelMode)
|
|
503
594
|
if (!controls.current) return
|
|
504
595
|
if (firstLoad.current) {
|
|
505
596
|
firstLoad.current = false
|
|
506
|
-
controls.current.setLookAt(20, 20, 20, 0,
|
|
597
|
+
controls.current.setLookAt(20, 20 + targetY, 20, 0, targetY, 0, true)
|
|
507
598
|
}
|
|
508
|
-
controls.current.
|
|
599
|
+
controls.current.getOrbitTarget(currentTarget)
|
|
509
600
|
// Idempotence guard: skip when already there — also swallows the thumbnail
|
|
510
601
|
// generator's synchronous stacked→restore levelMode round-trip.
|
|
511
602
|
if (Math.abs(currentTarget.y - targetY) < 1e-3) return
|
|
512
603
|
controls.current.moveTo(currentTarget.x, targetY, currentTarget.z, true)
|
|
513
|
-
}, [
|
|
604
|
+
}, [
|
|
605
|
+
camera,
|
|
606
|
+
currentLevelId,
|
|
607
|
+
levelMode,
|
|
608
|
+
isPreviewMode,
|
|
609
|
+
isFirstPersonMode,
|
|
610
|
+
isRestoringFirstPersonPose,
|
|
611
|
+
])
|
|
514
612
|
|
|
515
613
|
useEffect(() => {
|
|
516
614
|
if (isFirstPersonMode || !controls.current) return
|
|
@@ -523,6 +621,15 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
523
621
|
}
|
|
524
622
|
}, [isFirstPersonMode, maxPolarAngle])
|
|
525
623
|
|
|
624
|
+
const floorSurface = useMemo(() => new FloorSurface(), [])
|
|
625
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: drei builds a new controls instance per camera
|
|
626
|
+
useLayoutEffect(() => {
|
|
627
|
+
floorSurface.levelId = currentLevelId ?? null
|
|
628
|
+
if (!controls.current) return
|
|
629
|
+
controls.current.floorMinY = floorMinY
|
|
630
|
+
controls.current.floorSurface = floorSurface
|
|
631
|
+
}, [camera, currentLevelId, floorMinY, floorSurface, isFirstPersonMode])
|
|
632
|
+
|
|
526
633
|
const focusNode = useCallback(
|
|
527
634
|
(nodeId: string) => {
|
|
528
635
|
if (isPreviewMode || isFirstPersonMode || !controls.current) return
|
|
@@ -530,17 +637,34 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
530
637
|
const object3D = sceneRegistry.nodes.get(nodeId)
|
|
531
638
|
if (!object3D) return
|
|
532
639
|
|
|
640
|
+
const node = useScene.getState().nodes[nodeId as AnyNodeId]
|
|
641
|
+
const groundFocus = node !== undefined && GROUND_FOCUS_TYPES.has(node.type)
|
|
533
642
|
tempBox.setFromObject(object3D)
|
|
534
|
-
if (tempBox.isEmpty())
|
|
535
|
-
|
|
536
|
-
|
|
643
|
+
if (!tempBox.isEmpty()) {
|
|
644
|
+
tempBox.getCenter(tempCenter)
|
|
645
|
+
} else if (groundFocus && node.type === 'zone' && node.polygon.length > 0) {
|
|
646
|
+
// A room draws nothing measurable in some display modes: aim at the
|
|
647
|
+
// middle of its outline instead.
|
|
648
|
+
tempBox.makeEmpty()
|
|
649
|
+
for (const [x, z] of node.polygon) tempBox.expandByPoint(tempCenter.set(x, 0, z))
|
|
650
|
+
tempBox.getCenter(tempCenter)
|
|
651
|
+
object3D.updateWorldMatrix(true, false)
|
|
652
|
+
tempCenter.applyMatrix4(object3D.matrixWorld)
|
|
653
|
+
} else {
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
if (groundFocus) {
|
|
657
|
+
tempCenter.y = orbitGroundY(levelOf(nodeId) ?? lastLevelId.current)
|
|
658
|
+
}
|
|
537
659
|
controls.current.getPosition(tempPosition)
|
|
538
660
|
controls.current.getTarget(tempTarget)
|
|
539
661
|
tempDelta.copy(tempCenter).sub(tempTarget)
|
|
540
662
|
|
|
663
|
+
const focusedY = tempPosition.y + tempDelta.y
|
|
664
|
+
const floorMinY = isPerspectiveCamera(camera) ? controls.current.floorLimitY : null
|
|
541
665
|
controls.current.setLookAt(
|
|
542
666
|
tempPosition.x + tempDelta.x,
|
|
543
|
-
|
|
667
|
+
floorMinY === null ? focusedY : Math.max(focusedY, floorMinY),
|
|
544
668
|
tempPosition.z + tempDelta.z,
|
|
545
669
|
tempCenter.x,
|
|
546
670
|
tempCenter.y,
|
|
@@ -548,7 +672,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
548
672
|
true,
|
|
549
673
|
)
|
|
550
674
|
},
|
|
551
|
-
[isPreviewMode, isFirstPersonMode],
|
|
675
|
+
[camera, isPreviewMode, isFirstPersonMode],
|
|
552
676
|
)
|
|
553
677
|
|
|
554
678
|
const publishCurrentPose = useCallback(() => {
|
|
@@ -630,18 +754,18 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
630
754
|
}
|
|
631
755
|
}
|
|
632
756
|
|
|
633
|
-
const
|
|
634
|
-
if (
|
|
757
|
+
const motion = keyboardPanMotion.current
|
|
758
|
+
if (!advanceKeyboardPanMotion(motion, keyboardPanKeys.current, performance.now())) return
|
|
635
759
|
|
|
636
760
|
const control = controls.current
|
|
637
761
|
|
|
638
762
|
control.getSpherical(keyboardPanSpherical, false)
|
|
639
763
|
const viewWidth = getCameraViewWidth(camera, keyboardPanSpherical.radius, viewportSize)
|
|
640
764
|
const speed = keyboardPanSpeed(viewWidth)
|
|
641
|
-
const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
|
|
642
765
|
|
|
643
|
-
|
|
644
|
-
if (
|
|
766
|
+
// The motion already eases; the controls' own smoothing would add a glide on top.
|
|
767
|
+
if (motion.stepX !== 0) control.truck(motion.stepX * speed, 0, false)
|
|
768
|
+
if (motion.stepY !== 0) control.forward(motion.stepY * speed, false)
|
|
645
769
|
}, 0)
|
|
646
770
|
|
|
647
771
|
// Configure mouse buttons based on control mode and camera mode
|
|
@@ -652,13 +776,14 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
652
776
|
? CameraControlsImpl.ACTION.ZOOM
|
|
653
777
|
: CameraControlsImpl.ACTION.DOLLY
|
|
654
778
|
|
|
779
|
+
// Left click selects, in Preview as in the published viewer.
|
|
655
780
|
return {
|
|
656
|
-
left:
|
|
781
|
+
left: CameraControlsImpl.ACTION.NONE,
|
|
657
782
|
middle: CameraControlsImpl.ACTION.SCREEN_PAN,
|
|
658
783
|
right: CameraControlsImpl.ACTION.ROTATE,
|
|
659
784
|
wheel: wheelAction,
|
|
660
785
|
}
|
|
661
|
-
}, [cameraMode
|
|
786
|
+
}, [cameraMode])
|
|
662
787
|
|
|
663
788
|
// Touch gestures (mobile / trackpad).
|
|
664
789
|
// - One finger drag → rotate by default (much easier on a phone), but
|
|
@@ -774,10 +899,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
774
899
|
controls.current.mouseButtons.wheel = wheelAction
|
|
775
900
|
controls.current.mouseButtons.middle = CameraControlsImpl.ACTION.SCREEN_PAN
|
|
776
901
|
controls.current.mouseButtons.right = CameraControlsImpl.ACTION.ROTATE
|
|
777
|
-
if (
|
|
778
|
-
// In preview mode, left-click is always pan (viewer-style)
|
|
779
|
-
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
|
|
780
|
-
} else if (space) {
|
|
902
|
+
if (space) {
|
|
781
903
|
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
|
|
782
904
|
} else {
|
|
783
905
|
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE
|
|
@@ -787,6 +909,11 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
787
909
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
788
910
|
if (isKeyboardPanKey(event.code)) {
|
|
789
911
|
if (acceptsKeyboardPan(event) && !planOwnsNavigation()) {
|
|
912
|
+
syncKeyboardPanMotion(
|
|
913
|
+
keyboardPanMotion.current,
|
|
914
|
+
keyboardPanKeys.current,
|
|
915
|
+
performance.now(),
|
|
916
|
+
)
|
|
790
917
|
const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
|
|
791
918
|
if (changed) beginLocalCameraInteraction()
|
|
792
919
|
event.preventDefault()
|
|
@@ -818,6 +945,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
818
945
|
|
|
819
946
|
const onKeyUp = (event: KeyboardEvent) => {
|
|
820
947
|
if (isKeyboardPanKey(event.code)) {
|
|
948
|
+
syncKeyboardPanMotion(keyboardPanMotion.current, keyboardPanKeys.current, performance.now())
|
|
821
949
|
const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, false)
|
|
822
950
|
if (changed) {
|
|
823
951
|
if (!hasKeyboardPanInput(keyboardPanKeys.current)) {
|
|
@@ -907,14 +1035,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
907
1035
|
clearNavigationCursor()
|
|
908
1036
|
cameraDraggingLifecycle.end()
|
|
909
1037
|
}
|
|
910
|
-
}, [
|
|
911
|
-
beginLocalCameraInteraction,
|
|
912
|
-
cameraDraggingLifecycle,
|
|
913
|
-
cameraMode,
|
|
914
|
-
gl,
|
|
915
|
-
isPreviewMode,
|
|
916
|
-
isFirstPersonMode,
|
|
917
|
-
])
|
|
1038
|
+
}, [beginLocalCameraInteraction, cameraDraggingLifecycle, cameraMode, gl, isFirstPersonMode])
|
|
918
1039
|
|
|
919
1040
|
// `controlstart` fires only for user pointer interactions. Pointerdowns
|
|
920
1041
|
// mapped to ACTION.NONE must not flag the camera as dragging because no
|
|
@@ -927,18 +1048,18 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
927
1048
|
})
|
|
928
1049
|
}, [beginLocalCameraInteraction])
|
|
929
1050
|
|
|
930
|
-
// Preview
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
: null
|
|
934
|
-
|
|
1051
|
+
// Preview opens on the editor's floor (or building, or site) framed the
|
|
1052
|
+
// viewer's way; afterwards only the viewer navigation moves the camera
|
|
1053
|
+
// (`camera-controls:frame`), never a canvas selection.
|
|
935
1054
|
useEffect(() => {
|
|
936
1055
|
if (!(isPreviewMode && controls.current) || isFirstPersonMode) return
|
|
937
1056
|
|
|
938
1057
|
const nodes = useScene.getState().nodes
|
|
939
|
-
|
|
1058
|
+
const { selection: opening } = useViewer.getState()
|
|
1059
|
+
const openingNodeId = opening.levelId ?? opening.buildingId
|
|
1060
|
+
let node = openingNodeId ? nodes[openingNodeId as AnyNodeId] : null
|
|
940
1061
|
|
|
941
|
-
if (!
|
|
1062
|
+
if (!openingNodeId) {
|
|
942
1063
|
const site = Object.values(nodes).find((n) => n.type === 'site')
|
|
943
1064
|
node = site || null
|
|
944
1065
|
}
|
|
@@ -971,10 +1092,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
971
1092
|
return
|
|
972
1093
|
}
|
|
973
1094
|
|
|
974
|
-
if (!
|
|
1095
|
+
if (!openingNodeId) return
|
|
975
1096
|
|
|
976
1097
|
// Calculate camera position from bounding box
|
|
977
|
-
const object3D = sceneRegistry.nodes.get(
|
|
1098
|
+
const object3D = sceneRegistry.nodes.get(openingNodeId)
|
|
978
1099
|
if (!object3D) return
|
|
979
1100
|
|
|
980
1101
|
tempBox.setFromObject(object3D)
|
|
@@ -993,7 +1114,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
993
1114
|
tempCenter.z,
|
|
994
1115
|
true,
|
|
995
1116
|
)
|
|
996
|
-
}, [isPreviewMode, isFirstPersonMode
|
|
1117
|
+
}, [isPreviewMode, isFirstPersonMode])
|
|
997
1118
|
|
|
998
1119
|
// Preset capture auto-framing — when `setCaptureMode({ mode: 'preset',
|
|
999
1120
|
// isolated })` fires, fly the camera to a pose that fits the union
|
|
@@ -1159,7 +1280,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1159
1280
|
if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
|
|
1160
1281
|
|
|
1161
1282
|
const currentAzimuth = controls.current.azimuthAngle
|
|
1162
|
-
const currentPolar = controls.current.
|
|
1283
|
+
const currentPolar = controls.current.orbitPolarAngle
|
|
1163
1284
|
// Round to nearest 90° increment, then rotate 90° clockwise
|
|
1164
1285
|
const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
|
|
1165
1286
|
const target = rounded - Math.PI / 2
|
|
@@ -1171,7 +1292,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1171
1292
|
if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
|
|
1172
1293
|
|
|
1173
1294
|
const currentAzimuth = controls.current.azimuthAngle
|
|
1174
|
-
const currentPolar = controls.current.
|
|
1295
|
+
const currentPolar = controls.current.orbitPolarAngle
|
|
1175
1296
|
// Round to nearest 90° increment, then rotate 90° counter-clockwise
|
|
1176
1297
|
const rounded = Math.round(currentAzimuth / (Math.PI / 2)) * (Math.PI / 2)
|
|
1177
1298
|
const target = rounded + Math.PI / 2
|
|
@@ -1183,11 +1304,18 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1183
1304
|
focusNode(nodeId)
|
|
1184
1305
|
}
|
|
1185
1306
|
|
|
1307
|
+
const handleFrame = (event: CameraControlFrameEvent) => {
|
|
1308
|
+
if (isFirstPersonMode || !controls.current) return
|
|
1309
|
+
cancelPoseApplication()
|
|
1310
|
+
frameViewerCamera(controls.current, camera, event)
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1186
1313
|
const handleFitScene = ({ bounds }: CameraControlFitSceneEvent) => {
|
|
1187
1314
|
if (isFirstPersonMode || !controls.current || isPreviewMode) return
|
|
1315
|
+
const groundY = orbitGroundY(lastLevelId.current)
|
|
1188
1316
|
if (!bounds) {
|
|
1189
1317
|
// Restore default framing pose when no bounds were computed.
|
|
1190
|
-
controls.current.setLookAt(20, 20, 20, 0,
|
|
1318
|
+
controls.current.setLookAt(20, 20 + groundY, 20, 0, groundY, 0, true)
|
|
1191
1319
|
return
|
|
1192
1320
|
}
|
|
1193
1321
|
const [cx, cz] = bounds.center
|
|
@@ -1197,11 +1325,20 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1197
1325
|
const maxExtent = Math.max(w, d)
|
|
1198
1326
|
const distance = Math.max(maxExtent * 1.4, 15)
|
|
1199
1327
|
const height = Math.max(maxExtent * 0.8, 10)
|
|
1200
|
-
controls.current.setLookAt(
|
|
1328
|
+
controls.current.setLookAt(
|
|
1329
|
+
cx + distance * 0.7,
|
|
1330
|
+
height + groundY,
|
|
1331
|
+
cz + distance * 0.7,
|
|
1332
|
+
cx,
|
|
1333
|
+
groundY,
|
|
1334
|
+
cz,
|
|
1335
|
+
true,
|
|
1336
|
+
)
|
|
1201
1337
|
}
|
|
1202
1338
|
|
|
1203
1339
|
emitter.on('camera-controls:capture', handleNodeCapture)
|
|
1204
1340
|
emitter.on('camera-controls:focus', handleNodeFocus)
|
|
1341
|
+
emitter.on('camera-controls:frame', handleFrame)
|
|
1205
1342
|
emitter.on('camera-controls:view', handleNodeView)
|
|
1206
1343
|
emitter.on('camera-controls:top-view', handleTopView)
|
|
1207
1344
|
emitter.on('camera-controls:orbit-cw', handleOrbitCW)
|
|
@@ -1211,13 +1348,14 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1211
1348
|
return () => {
|
|
1212
1349
|
emitter.off('camera-controls:capture', handleNodeCapture)
|
|
1213
1350
|
emitter.off('camera-controls:focus', handleNodeFocus)
|
|
1351
|
+
emitter.off('camera-controls:frame', handleFrame)
|
|
1214
1352
|
emitter.off('camera-controls:view', handleNodeView)
|
|
1215
1353
|
emitter.off('camera-controls:top-view', handleTopView)
|
|
1216
1354
|
emitter.off('camera-controls:orbit-cw', handleOrbitCW)
|
|
1217
1355
|
emitter.off('camera-controls:orbit-ccw', handleOrbitCCW)
|
|
1218
1356
|
emitter.off('camera-controls:fit-scene', handleFitScene)
|
|
1219
1357
|
}
|
|
1220
|
-
}, [focusNode, isPreviewMode, isFirstPersonMode])
|
|
1358
|
+
}, [camera, cancelPoseApplication, focusNode, isPreviewMode, isFirstPersonMode])
|
|
1221
1359
|
|
|
1222
1360
|
const onTransitionStart = useCallback(() => {
|
|
1223
1361
|
cameraDraggingLifecycle.begin()
|
|
@@ -1248,6 +1386,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1248
1386
|
// navigation guardrails.
|
|
1249
1387
|
const isPresetCapture = captureMode.mode === 'preset'
|
|
1250
1388
|
const minDistance = isPresetCapture ? 0.5 : 2
|
|
1389
|
+
// Over modeled or mapped surroundings (a neighbourhood instead of the
|
|
1390
|
+
// horizon disc) the camera may pull back far enough to take it all in.
|
|
1391
|
+
const groundReplaced = useSceneGroundReplacement()
|
|
1392
|
+
const maxDistance = groundReplaced ? 1500 : 100
|
|
1251
1393
|
|
|
1252
1394
|
if (isFirstPersonMode) {
|
|
1253
1395
|
return null
|
|
@@ -1255,8 +1397,10 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1255
1397
|
|
|
1256
1398
|
return (
|
|
1257
1399
|
<CameraControls
|
|
1400
|
+
azimuthRotateSpeed={ORBIT_ROTATE_SPEED}
|
|
1258
1401
|
makeDefault
|
|
1259
|
-
maxDistance={
|
|
1402
|
+
maxDistance={maxDistance}
|
|
1403
|
+
impl={FloorAwareCameraControls}
|
|
1260
1404
|
maxPolarAngle={maxPolarAngle}
|
|
1261
1405
|
minDistance={minDistance}
|
|
1262
1406
|
minPolarAngle={0}
|
|
@@ -1265,6 +1409,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1265
1409
|
onControlStart={handleControlStart}
|
|
1266
1410
|
onUpdate={handleCameraUpdate}
|
|
1267
1411
|
onRest={onRest}
|
|
1412
|
+
polarRotateSpeed={ORBIT_ROTATE_SPEED}
|
|
1268
1413
|
onSleep={onRest}
|
|
1269
1414
|
onTransitionStart={onTransitionStart}
|
|
1270
1415
|
ref={controls}
|