@pascal-app/editor 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +11 -11
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1981
- package/src/components/editor/floating-action-menu.tsx +1 -1
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +6 -2
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +26 -22
- package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
- package/src/components/editor/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +24 -14
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/room-controls.tsx +7 -4
- package/src/components/editor/room-handles-2d.tsx +2 -3
- package/src/components/editor/room-handles.tsx +1 -2
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +40 -1671
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +1 -1
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
- package/src/components/systems/roof/roof-edit-system.tsx +4 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +1 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.test.tsx +46 -0
- package/src/components/ui/node-icon-image.tsx +33 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
- package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +1 -1
- package/src/components/ui/panels/panel-wrapper.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +6 -6
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-panel.tsx +6 -8
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +180 -182
- package/src/i18n/locales/es/messages.po +180 -182
- package/src/i18n/locales/fr/messages.po +180 -182
- package/src/i18n/locales/ja/messages.po +180 -182
- package/src/i18n/locales/zh-CN/messages.po +180 -182
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +109 -131
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/floor-footprints.test.ts +2 -2
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurements.test.ts +2 -1
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-handle-drag.ts +2 -12
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/store/use-editor.tsx +17 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- package/src/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurements.ts +0 -8
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -327
- package/src/lib/scene-bounds.ts +0 -233
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The sculpted ground as walkthrough collision geometry.
|
|
3
|
-
*
|
|
4
|
-
* The first-person collider world is a single merged BVH mesh, and before terrain
|
|
5
|
-
* the ground contributed one flat 2 km box to it. That box is wrong in both
|
|
6
|
-
* directions on a sculpted site: it holds the player up at the datum inside an
|
|
7
|
-
* excavation, and buries them inside a hill. This module replaces it.
|
|
8
|
-
*
|
|
9
|
-
* Two properties it has to hold, because the whole point is that walking agrees
|
|
10
|
-
* with every other consumer of the field:
|
|
11
|
-
*
|
|
12
|
-
* - **The surface is the field**, sampled at every vertex — the same
|
|
13
|
-
* `heightAtSample`/`normalAt` the rendered mesh uses (`terrain-geometry.ts`),
|
|
14
|
-
* so what you see is what you stand on. It is deliberately not a decimated
|
|
15
|
-
* proxy at realistic field sizes.
|
|
16
|
-
* - **It extends past the field**, because `heightAt` clamps to the border
|
|
17
|
-
* outside it. A collider that stopped at the field edge would drop the player
|
|
18
|
-
* into the void one step past the site, which is exactly the bug the flat box
|
|
19
|
-
* existed to prevent. The skirt extrudes each border sample outward at its own
|
|
20
|
-
* height, so the collider and `heightAt` agree everywhere, not just inside.
|
|
21
|
-
*
|
|
22
|
-
* Non-indexed triangles with position + normal, matching what the other collider
|
|
23
|
-
* contributors emit — `mergeGeometries` requires every input to agree on both.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { heightAtSample, normalAt, type TerrainField } from '@pascal-app/core'
|
|
27
|
-
import * as THREE from 'three'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* How far past the field the ground keeps holding the player, in metres. Matches
|
|
31
|
-
* the flat box's half-extent so a site's collider bounds do not change size when
|
|
32
|
-
* it gains terrain.
|
|
33
|
-
*/
|
|
34
|
-
const SKIRT_REACH = 1000
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Cell budget before the collider decimates.
|
|
38
|
-
*
|
|
39
|
-
* 66k cells is a 257² field — the largest anything realistic produces, and ~131k
|
|
40
|
-
* triangles, which the BVH builds in well under a frame's worth of budget. The
|
|
41
|
-
* schema allows up to 4097² (a decode-bomb guard, not a real terrain); at that
|
|
42
|
-
* size an exact collider would be 100M triangles, so past the budget this steps
|
|
43
|
-
* the sample grid and the collider becomes a faithful-but-coarser version of the
|
|
44
|
-
* same surface rather than an out-of-memory crash.
|
|
45
|
-
*/
|
|
46
|
-
const MAX_COLLIDER_CELLS = 66_000
|
|
47
|
-
|
|
48
|
-
function colliderStride(field: TerrainField): number {
|
|
49
|
-
const cells = Math.max(1, (field.cols - 1) * (field.rows - 1))
|
|
50
|
-
if (cells <= MAX_COLLIDER_CELLS) return 1
|
|
51
|
-
return Math.ceil(Math.sqrt(cells / MAX_COLLIDER_CELLS))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The sample indices the collider walks: every `stride`-th, always including the border. */
|
|
55
|
-
function sampleLine(count: number, stride: number): number[] {
|
|
56
|
-
const line: number[] = []
|
|
57
|
-
for (let i = 0; i < count - 1; i += stride) line.push(i)
|
|
58
|
-
line.push(count - 1)
|
|
59
|
-
return line
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
type Vertex = { x: number; y: number; z: number }
|
|
63
|
-
|
|
64
|
-
class TriangleSink {
|
|
65
|
-
private readonly positions: number[] = []
|
|
66
|
-
private readonly normals: number[] = []
|
|
67
|
-
|
|
68
|
-
constructor(private readonly field: TerrainField) {}
|
|
69
|
-
|
|
70
|
-
private push(v: Vertex): void {
|
|
71
|
-
this.positions.push(v.x, v.y, v.z)
|
|
72
|
-
const [nx, ny, nz] = normalAt(this.field, v.x, v.z)
|
|
73
|
-
this.normals.push(nx, ny, nz)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* One cell, given its four corners named by axis order. Winding matches
|
|
78
|
-
* `buildTerrainMesh`: `(p00, p01, p10)` and `(p10, p01, p11)` faces +Y, which is
|
|
79
|
-
* what the spawn resolver's `normal.dot(UP) > 0.2` floor test needs.
|
|
80
|
-
*/
|
|
81
|
-
quad(p00: Vertex, p10: Vertex, p01: Vertex, p11: Vertex): void {
|
|
82
|
-
this.push(p00)
|
|
83
|
-
this.push(p01)
|
|
84
|
-
this.push(p10)
|
|
85
|
-
this.push(p10)
|
|
86
|
-
this.push(p01)
|
|
87
|
-
this.push(p11)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
build(): THREE.BufferGeometry | null {
|
|
91
|
-
if (this.positions.length === 0) return null
|
|
92
|
-
const geometry = new THREE.BufferGeometry()
|
|
93
|
-
geometry.setAttribute(
|
|
94
|
-
'position',
|
|
95
|
-
new THREE.BufferAttribute(new Float32Array(this.positions), 3),
|
|
96
|
-
)
|
|
97
|
-
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.normals), 3))
|
|
98
|
-
return geometry
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Terrain collision geometry in the site's own frame — the caller applies the
|
|
104
|
-
* site's world matrix, exactly as it did for the flat box it replaces.
|
|
105
|
-
*/
|
|
106
|
-
export function createTerrainColliderGeometry(field: TerrainField): THREE.BufferGeometry | null {
|
|
107
|
-
if (field.cols < 2 || field.rows < 2) return null
|
|
108
|
-
|
|
109
|
-
const stride = colliderStride(field)
|
|
110
|
-
const cols = sampleLine(field.cols, stride)
|
|
111
|
-
const rows = sampleLine(field.rows, stride)
|
|
112
|
-
const sink = new TriangleSink(field)
|
|
113
|
-
|
|
114
|
-
const xAt = (col: number) => field.origin[0] + col * field.spacing
|
|
115
|
-
const zAt = (row: number) => field.origin[1] + row * field.spacing
|
|
116
|
-
const at = (col: number, row: number): Vertex => ({
|
|
117
|
-
x: xAt(col),
|
|
118
|
-
y: heightAtSample(field, col, row),
|
|
119
|
-
z: zAt(row),
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
for (let ri = 0; ri < rows.length - 1; ri++) {
|
|
123
|
-
const r0 = rows[ri] as number
|
|
124
|
-
const r1 = rows[ri + 1] as number
|
|
125
|
-
for (let ci = 0; ci < cols.length - 1; ci++) {
|
|
126
|
-
const c0 = cols[ci] as number
|
|
127
|
-
const c1 = cols[ci + 1] as number
|
|
128
|
-
sink.quad(at(c0, r0), at(c1, r0), at(c0, r1), at(c1, r1))
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// The skirt: each border sample extruded outward at its own height, so stepping
|
|
133
|
-
// off the field lands on the border height rather than on nothing. This is the
|
|
134
|
-
// collider spelling of `heightAt`'s edge clamp.
|
|
135
|
-
const firstCol = 0
|
|
136
|
-
const lastCol = field.cols - 1
|
|
137
|
-
const firstRow = 0
|
|
138
|
-
const lastRow = field.rows - 1
|
|
139
|
-
const westX = xAt(firstCol) - SKIRT_REACH
|
|
140
|
-
const eastX = xAt(lastCol) + SKIRT_REACH
|
|
141
|
-
const northZ = zAt(firstRow) - SKIRT_REACH
|
|
142
|
-
const southZ = zAt(lastRow) + SKIRT_REACH
|
|
143
|
-
|
|
144
|
-
for (let ci = 0; ci < cols.length - 1; ci++) {
|
|
145
|
-
const c0 = cols[ci] as number
|
|
146
|
-
const c1 = cols[ci + 1] as number
|
|
147
|
-
const north0 = at(c0, firstRow)
|
|
148
|
-
const north1 = at(c1, firstRow)
|
|
149
|
-
sink.quad(
|
|
150
|
-
{ x: north0.x, y: north0.y, z: northZ },
|
|
151
|
-
{ x: north1.x, y: north1.y, z: northZ },
|
|
152
|
-
north0,
|
|
153
|
-
north1,
|
|
154
|
-
)
|
|
155
|
-
const south0 = at(c0, lastRow)
|
|
156
|
-
const south1 = at(c1, lastRow)
|
|
157
|
-
sink.quad(
|
|
158
|
-
south0,
|
|
159
|
-
south1,
|
|
160
|
-
{ x: south0.x, y: south0.y, z: southZ },
|
|
161
|
-
{
|
|
162
|
-
x: south1.x,
|
|
163
|
-
y: south1.y,
|
|
164
|
-
z: southZ,
|
|
165
|
-
},
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
for (let ri = 0; ri < rows.length - 1; ri++) {
|
|
170
|
-
const r0 = rows[ri] as number
|
|
171
|
-
const r1 = rows[ri + 1] as number
|
|
172
|
-
const west0 = at(firstCol, r0)
|
|
173
|
-
const west1 = at(firstCol, r1)
|
|
174
|
-
sink.quad(
|
|
175
|
-
{ x: westX, y: west0.y, z: west0.z },
|
|
176
|
-
west0,
|
|
177
|
-
{ x: westX, y: west1.y, z: west1.z },
|
|
178
|
-
west1,
|
|
179
|
-
)
|
|
180
|
-
const east0 = at(lastCol, r0)
|
|
181
|
-
const east1 = at(lastCol, r1)
|
|
182
|
-
sink.quad(east0, { x: eastX, y: east0.y, z: east0.z }, east1, {
|
|
183
|
-
x: eastX,
|
|
184
|
-
y: east1.y,
|
|
185
|
-
z: east1.z,
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Corner quads, flat at the corner sample's height — the diagonal region no edge
|
|
190
|
-
// strip covers, and where `heightAt` also reports the corner value.
|
|
191
|
-
const corners: Array<[Vertex, number, number]> = [
|
|
192
|
-
[at(firstCol, firstRow), westX, northZ],
|
|
193
|
-
[at(lastCol, firstRow), eastX, northZ],
|
|
194
|
-
[at(firstCol, lastRow), westX, southZ],
|
|
195
|
-
[at(lastCol, lastRow), eastX, southZ],
|
|
196
|
-
]
|
|
197
|
-
for (const [corner, farX, farZ] of corners) {
|
|
198
|
-
const minX = Math.min(corner.x, farX)
|
|
199
|
-
const maxX = Math.max(corner.x, farX)
|
|
200
|
-
const minZ = Math.min(corner.z, farZ)
|
|
201
|
-
const maxZ = Math.max(corner.z, farZ)
|
|
202
|
-
const y = corner.y
|
|
203
|
-
sink.quad(
|
|
204
|
-
{ x: minX, y, z: minZ },
|
|
205
|
-
{ x: maxX, y, z: minZ },
|
|
206
|
-
{ x: minX, y, z: maxZ },
|
|
207
|
-
{ x: maxX, y, z: maxZ },
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return sink.build()
|
|
212
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { CameraPose } from '@pascal-app/core'
|
|
3
|
-
import { publishInitialCameraPose } from '../../lib/camera-pose'
|
|
4
|
-
import type { NavigationSyncPose } from '../../store/use-editor'
|
|
5
|
-
import {
|
|
6
|
-
cameraPoseToFloorplanNavigationPose,
|
|
7
|
-
createFloorplanCameraNavigationChannel,
|
|
8
|
-
createFloorplanCameraSyncBridge,
|
|
9
|
-
floorplanNavigationPoseToCameraPose,
|
|
10
|
-
} from './floorplan-camera-sync'
|
|
11
|
-
|
|
12
|
-
const cameraPose: CameraPose = {
|
|
13
|
-
fov: 50,
|
|
14
|
-
position: [3, 4, 4],
|
|
15
|
-
projection: 'perspective',
|
|
16
|
-
target: [0, 1, 0],
|
|
17
|
-
viewWidth: 12,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function cameraPoseAtAzimuth(
|
|
21
|
-
azimuth: number,
|
|
22
|
-
target: [number, number, number] = [0, 1, 0],
|
|
23
|
-
): CameraPose {
|
|
24
|
-
const horizontalDistance = 5
|
|
25
|
-
return {
|
|
26
|
-
...cameraPose,
|
|
27
|
-
position: [
|
|
28
|
-
target[0] + Math.sin(azimuth) * horizontalDistance,
|
|
29
|
-
4,
|
|
30
|
-
target[2] + Math.cos(azimuth) * horizontalDistance,
|
|
31
|
-
],
|
|
32
|
-
target,
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('floorplan camera sync', () => {
|
|
37
|
-
test('derives the floor-plan navigation pose from the generic camera pose', () => {
|
|
38
|
-
expect(
|
|
39
|
-
cameraPoseToFloorplanNavigationPose({
|
|
40
|
-
position: [10, 5, 0],
|
|
41
|
-
projection: 'perspective',
|
|
42
|
-
target: [0, 0, 0],
|
|
43
|
-
viewWidth: 20,
|
|
44
|
-
}),
|
|
45
|
-
).toEqual({
|
|
46
|
-
source: '3d',
|
|
47
|
-
target: [0, 0, 0],
|
|
48
|
-
azimuth: Math.PI / 2,
|
|
49
|
-
viewWidth: 20,
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('applies a floor-plan pose while preserving the generic camera elevation and projection', () => {
|
|
54
|
-
expect(
|
|
55
|
-
floorplanNavigationPoseToCameraPose(
|
|
56
|
-
{
|
|
57
|
-
source: '2d',
|
|
58
|
-
revision: 4,
|
|
59
|
-
target: [10, 2, 20],
|
|
60
|
-
azimuth: Math.PI / 2,
|
|
61
|
-
viewWidth: 8,
|
|
62
|
-
},
|
|
63
|
-
cameraPose,
|
|
64
|
-
),
|
|
65
|
-
).toEqual({
|
|
66
|
-
fov: 50,
|
|
67
|
-
position: [15, 5, 20],
|
|
68
|
-
projection: 'perspective',
|
|
69
|
-
target: [10, 2, 20],
|
|
70
|
-
viewWidth: 8,
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('owns two-way synchronization without echoing tiny camera changes', () => {
|
|
75
|
-
const published: Array<Omit<NavigationSyncPose, 'revision'>> = []
|
|
76
|
-
const applied: CameraPose[] = []
|
|
77
|
-
const bridge = createFloorplanCameraSyncBridge({
|
|
78
|
-
applyCameraPose: (pose) => applied.push(pose),
|
|
79
|
-
publishNavigationPose: (pose) => published.push(pose),
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
bridge.receiveCameraPose(cameraPose)
|
|
83
|
-
bridge.receiveCameraPose({
|
|
84
|
-
...cameraPose,
|
|
85
|
-
position: [3.0001, 4, 4],
|
|
86
|
-
})
|
|
87
|
-
bridge.receiveNavigationPose({
|
|
88
|
-
source: '2d',
|
|
89
|
-
revision: 1,
|
|
90
|
-
target: [10, 2, 20],
|
|
91
|
-
azimuth: Math.PI / 2,
|
|
92
|
-
viewWidth: 8,
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
expect(published).toHaveLength(1)
|
|
96
|
-
expect(published[0]?.source).toBe('3d')
|
|
97
|
-
expect(applied).toHaveLength(1)
|
|
98
|
-
expect(applied[0]).toMatchObject({
|
|
99
|
-
fov: 50,
|
|
100
|
-
projection: 'perspective',
|
|
101
|
-
target: [10, 2, 20],
|
|
102
|
-
viewWidth: 8,
|
|
103
|
-
})
|
|
104
|
-
expect(applied[0]?.position[0]).toBeCloseTo(15)
|
|
105
|
-
expect(applied[0]?.position[1]).toBeCloseTo(5)
|
|
106
|
-
expect(applied[0]?.position[2]).toBeCloseTo(20)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
test('publishes sub-degree camera rotation for a real-time compass', () => {
|
|
110
|
-
const published: Array<Omit<NavigationSyncPose, 'revision'>> = []
|
|
111
|
-
const bridge = createFloorplanCameraSyncBridge({
|
|
112
|
-
applyCameraPose: () => {},
|
|
113
|
-
publishNavigationPose: (pose) => published.push(pose),
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
bridge.receiveCameraPose(cameraPoseAtAzimuth(0))
|
|
117
|
-
bridge.receiveCameraPose(cameraPoseAtAzimuth((0.9 * Math.PI) / 180))
|
|
118
|
-
expect(published).toHaveLength(2)
|
|
119
|
-
expect(published[1]?.azimuth).toBeCloseTo((0.9 * Math.PI) / 180)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
test('keeps a sub-degree camera rotation when it arrives with a pan', () => {
|
|
123
|
-
const published: Array<Omit<NavigationSyncPose, 'revision'>> = []
|
|
124
|
-
const bridge = createFloorplanCameraSyncBridge({
|
|
125
|
-
applyCameraPose: () => {},
|
|
126
|
-
publishNavigationPose: (pose) => published.push(pose),
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
bridge.receiveCameraPose(cameraPoseAtAzimuth(0))
|
|
130
|
-
bridge.receiveCameraPose(cameraPoseAtAzimuth((0.5 * Math.PI) / 180, [1, 1, 0]))
|
|
131
|
-
|
|
132
|
-
expect(published).toHaveLength(2)
|
|
133
|
-
expect(published[1]).toMatchObject({
|
|
134
|
-
target: [1, 1, 0],
|
|
135
|
-
})
|
|
136
|
-
expect(published[1]?.azimuth).toBeCloseTo((0.5 * Math.PI) / 180)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('keeps streaming live camera headings in 3D-only mode', () => {
|
|
140
|
-
const published: Array<Omit<NavigationSyncPose, 'revision'>> = []
|
|
141
|
-
const applied: CameraPose[] = []
|
|
142
|
-
const bridge = createFloorplanCameraSyncBridge({
|
|
143
|
-
active: false,
|
|
144
|
-
applyCameraPose: (pose) => applied.push(pose),
|
|
145
|
-
publishNavigationPose: (pose) => published.push(pose),
|
|
146
|
-
})
|
|
147
|
-
const latestPose = cameraPoseAtAzimuth(Math.PI / 2, [8, 1, 4])
|
|
148
|
-
|
|
149
|
-
bridge.receiveCameraPose(cameraPoseAtAzimuth(0))
|
|
150
|
-
bridge.receiveCameraPose(latestPose)
|
|
151
|
-
bridge.receiveNavigationPose({
|
|
152
|
-
source: '2d',
|
|
153
|
-
revision: 1,
|
|
154
|
-
target: [10, 2, 20],
|
|
155
|
-
azimuth: Math.PI / 2,
|
|
156
|
-
viewWidth: 8,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
expect(published).toEqual([
|
|
160
|
-
{
|
|
161
|
-
source: '3d',
|
|
162
|
-
target: cameraPose.target,
|
|
163
|
-
azimuth: 0,
|
|
164
|
-
viewWidth: 12,
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
source: '3d',
|
|
168
|
-
target: latestPose.target,
|
|
169
|
-
azimuth: Math.PI / 2,
|
|
170
|
-
viewWidth: 12,
|
|
171
|
-
},
|
|
172
|
-
])
|
|
173
|
-
expect(applied).toEqual([])
|
|
174
|
-
|
|
175
|
-
bridge.setActive(true)
|
|
176
|
-
expect(published).toHaveLength(2)
|
|
177
|
-
expect(applied).toEqual([])
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
test('applies the north-alignment pose used by the compass in 3D view', () => {
|
|
181
|
-
const applied: CameraPose[] = []
|
|
182
|
-
const bridge = createFloorplanCameraSyncBridge({
|
|
183
|
-
applyCameraPose: (pose) => applied.push(pose),
|
|
184
|
-
publishNavigationPose: () => {},
|
|
185
|
-
})
|
|
186
|
-
const currentPose = cameraPoseAtAzimuth(Math.PI / 2, [8, 1, 4])
|
|
187
|
-
|
|
188
|
-
bridge.receiveCameraPose(currentPose)
|
|
189
|
-
bridge.receiveNavigationPose({
|
|
190
|
-
source: '2d',
|
|
191
|
-
revision: 1,
|
|
192
|
-
target: [...currentPose.target],
|
|
193
|
-
azimuth: 0,
|
|
194
|
-
viewWidth: currentPose.viewWidth!,
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
expect(applied).toHaveLength(1)
|
|
198
|
-
expect(applied[0]).toMatchObject({
|
|
199
|
-
target: currentPose.target,
|
|
200
|
-
projection: currentPose.projection,
|
|
201
|
-
viewWidth: currentPose.viewWidth,
|
|
202
|
-
})
|
|
203
|
-
expect(applied[0]?.position[0]).toBeCloseTo(currentPose.target[0])
|
|
204
|
-
expect(applied[0]?.position[1]).toBeCloseTo(currentPose.position[1])
|
|
205
|
-
expect(applied[0]?.position[2]).toBeCloseTo(currentPose.target[2] + 5)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('applies 2D-first navigation on initial load without a 3D interaction', () => {
|
|
209
|
-
const applied: CameraPose[] = []
|
|
210
|
-
const bridge = createFloorplanCameraSyncBridge({
|
|
211
|
-
applyCameraPose: (pose) => applied.push(pose),
|
|
212
|
-
publishNavigationPose: () => {},
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
bridge.receiveNavigationPose({
|
|
216
|
-
source: '2d',
|
|
217
|
-
revision: 1,
|
|
218
|
-
target: [10, 2, 20],
|
|
219
|
-
azimuth: Math.PI / 2,
|
|
220
|
-
viewWidth: 8,
|
|
221
|
-
})
|
|
222
|
-
expect(applied).toEqual([])
|
|
223
|
-
|
|
224
|
-
publishInitialCameraPose(() => bridge.receiveCameraPose(cameraPose))
|
|
225
|
-
expect(applied).toHaveLength(1)
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
test('delivers the live camera stream through transient subscribers', () => {
|
|
229
|
-
const channel = createFloorplanCameraNavigationChannel()
|
|
230
|
-
const received: NavigationSyncPose[] = []
|
|
231
|
-
const unsubscribe = channel.subscribe((pose) => received.push(pose))
|
|
232
|
-
const input = {
|
|
233
|
-
source: '3d' as const,
|
|
234
|
-
target: [0, 1, 2] as [number, number, number],
|
|
235
|
-
azimuth: 0.5,
|
|
236
|
-
viewWidth: 12,
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
channel.publish(input)
|
|
240
|
-
channel.publish({ ...input, azimuth: 0.75 })
|
|
241
|
-
unsubscribe()
|
|
242
|
-
channel.publish({ ...input, azimuth: 1 })
|
|
243
|
-
|
|
244
|
-
expect(received).toEqual([
|
|
245
|
-
{ ...input, revision: 1 },
|
|
246
|
-
{ ...input, azimuth: 0.75, revision: 2 },
|
|
247
|
-
])
|
|
248
|
-
})
|
|
249
|
-
})
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { type CameraPose, emitter } from '@pascal-app/core'
|
|
4
|
-
import { useEffect, useRef } from 'react'
|
|
5
|
-
import { subscribeCameraPose } from '../../store/camera-pose-store'
|
|
6
|
-
import { subscribeNavigationSyncPose } from '../../store/navigation-sync-pose-store'
|
|
7
|
-
import type { NavigationSyncPose, NavigationSyncPoseInput } from '../../store/use-editor'
|
|
8
|
-
|
|
9
|
-
const POSITION_EPSILON = 0.001
|
|
10
|
-
const AZIMUTH_EPSILON = 1e-4
|
|
11
|
-
const VIEW_WIDTH_EPSILON = 0.001
|
|
12
|
-
|
|
13
|
-
type FloorplanNavigationSnapshot = Omit<NavigationSyncPoseInput, 'source'>
|
|
14
|
-
|
|
15
|
-
function angleDeltaRadians(a: number, b: number) {
|
|
16
|
-
return Math.atan2(Math.sin(a - b), Math.cos(a - b))
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function normalizeNearZero(value: number) {
|
|
20
|
-
return Math.abs(value) < Number.EPSILON * 10 ? 0 : value
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function navigationSnapshot(pose: NavigationSyncPoseInput): FloorplanNavigationSnapshot {
|
|
24
|
-
return {
|
|
25
|
-
target: [...pose.target],
|
|
26
|
-
azimuth: pose.azimuth,
|
|
27
|
-
viewWidth: pose.viewWidth,
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function navigationSnapshotsEqual(
|
|
32
|
-
previous: FloorplanNavigationSnapshot,
|
|
33
|
-
next: FloorplanNavigationSnapshot,
|
|
34
|
-
) {
|
|
35
|
-
return (
|
|
36
|
-
Math.abs(previous.target[0] - next.target[0]) < POSITION_EPSILON &&
|
|
37
|
-
Math.abs(previous.target[1] - next.target[1]) < POSITION_EPSILON &&
|
|
38
|
-
Math.abs(previous.target[2] - next.target[2]) < POSITION_EPSILON &&
|
|
39
|
-
Math.abs(angleDeltaRadians(previous.azimuth, next.azimuth)) < AZIMUTH_EPSILON &&
|
|
40
|
-
Math.abs(previous.viewWidth - next.viewWidth) < VIEW_WIDTH_EPSILON
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function cameraPoseToFloorplanNavigationPose(
|
|
45
|
-
pose: CameraPose,
|
|
46
|
-
): NavigationSyncPoseInput | null {
|
|
47
|
-
if (!(pose.viewWidth !== undefined && Number.isFinite(pose.viewWidth) && pose.viewWidth > 0)) {
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
source: '3d',
|
|
53
|
-
target: [...pose.target],
|
|
54
|
-
azimuth: Math.atan2(pose.position[0] - pose.target[0], pose.position[2] - pose.target[2]),
|
|
55
|
-
viewWidth: pose.viewWidth,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function floorplanNavigationPoseToCameraPose(
|
|
60
|
-
navigationPose: NavigationSyncPose,
|
|
61
|
-
cameraPose: CameraPose,
|
|
62
|
-
): CameraPose {
|
|
63
|
-
const offsetX = cameraPose.position[0] - cameraPose.target[0]
|
|
64
|
-
const offsetY = cameraPose.position[1] - cameraPose.target[1]
|
|
65
|
-
const offsetZ = cameraPose.position[2] - cameraPose.target[2]
|
|
66
|
-
const horizontalDistance = Math.hypot(offsetX, offsetZ)
|
|
67
|
-
const horizontalX = normalizeNearZero(Math.sin(navigationPose.azimuth) * horizontalDistance)
|
|
68
|
-
const horizontalZ = normalizeNearZero(Math.cos(navigationPose.azimuth) * horizontalDistance)
|
|
69
|
-
const target: [number, number, number] = [...navigationPose.target]
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
...(cameraPose.fov === undefined ? {} : { fov: cameraPose.fov }),
|
|
73
|
-
position: [target[0] + horizontalX, target[1] + offsetY, target[2] + horizontalZ],
|
|
74
|
-
projection: cameraPose.projection,
|
|
75
|
-
target,
|
|
76
|
-
viewWidth: navigationPose.viewWidth,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export type FloorplanCameraSyncBridge = {
|
|
81
|
-
receiveCameraPose: (pose: CameraPose) => void
|
|
82
|
-
receiveNavigationPose: (pose: NavigationSyncPose | null) => void
|
|
83
|
-
setActive: (active: boolean) => void
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export type FloorplanCameraNavigationChannel = {
|
|
87
|
-
publish: (pose: NavigationSyncPoseInput) => void
|
|
88
|
-
subscribe: (listener: (pose: NavigationSyncPose) => void) => () => void
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function createFloorplanCameraNavigationChannel(): FloorplanCameraNavigationChannel {
|
|
92
|
-
const listeners = new Set<(pose: NavigationSyncPose) => void>()
|
|
93
|
-
let revision = 0
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
publish: (pose) => {
|
|
97
|
-
revision += 1
|
|
98
|
-
const revisedPose = { ...pose, revision }
|
|
99
|
-
for (const listener of listeners) {
|
|
100
|
-
listener(revisedPose)
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
subscribe: (listener) => {
|
|
104
|
-
listeners.add(listener)
|
|
105
|
-
return () => listeners.delete(listener)
|
|
106
|
-
},
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const liveCameraNavigation = createFloorplanCameraNavigationChannel()
|
|
111
|
-
|
|
112
|
-
export function subscribeFloorplanCameraNavigation(listener: (pose: NavigationSyncPose) => void) {
|
|
113
|
-
return liveCameraNavigation.subscribe(listener)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function createFloorplanCameraSyncBridge({
|
|
117
|
-
active: initialActive = true,
|
|
118
|
-
applyCameraPose,
|
|
119
|
-
publishNavigationPose,
|
|
120
|
-
}: {
|
|
121
|
-
active?: boolean
|
|
122
|
-
applyCameraPose: (pose: CameraPose) => void
|
|
123
|
-
publishNavigationPose: (pose: NavigationSyncPoseInput) => void
|
|
124
|
-
}): FloorplanCameraSyncBridge {
|
|
125
|
-
let active = initialActive
|
|
126
|
-
let latestCameraPose: CameraPose | null = null
|
|
127
|
-
let pendingNavigationPose: NavigationSyncPose | null = null
|
|
128
|
-
let lastAppliedNavigationRevision = 0
|
|
129
|
-
let lastPublishedNavigation: FloorplanNavigationSnapshot | null = null
|
|
130
|
-
|
|
131
|
-
const applyPendingNavigationPose = () => {
|
|
132
|
-
if (!(latestCameraPose && pendingNavigationPose)) return false
|
|
133
|
-
if (pendingNavigationPose.revision === lastAppliedNavigationRevision) {
|
|
134
|
-
pendingNavigationPose = null
|
|
135
|
-
return false
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const appliedPose = floorplanNavigationPoseToCameraPose(pendingNavigationPose, latestCameraPose)
|
|
139
|
-
const appliedNavigationPose = cameraPoseToFloorplanNavigationPose(appliedPose)
|
|
140
|
-
latestCameraPose = appliedPose
|
|
141
|
-
lastAppliedNavigationRevision = pendingNavigationPose.revision
|
|
142
|
-
pendingNavigationPose = null
|
|
143
|
-
if (appliedNavigationPose) {
|
|
144
|
-
lastPublishedNavigation = navigationSnapshot(appliedNavigationPose)
|
|
145
|
-
}
|
|
146
|
-
applyCameraPose(appliedPose)
|
|
147
|
-
return true
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const publishCameraNavigationPose = (pose: CameraPose) => {
|
|
151
|
-
const navigationPose = cameraPoseToFloorplanNavigationPose(pose)
|
|
152
|
-
if (!navigationPose) return
|
|
153
|
-
const nextSnapshot = navigationSnapshot(navigationPose)
|
|
154
|
-
if (
|
|
155
|
-
lastPublishedNavigation &&
|
|
156
|
-
navigationSnapshotsEqual(lastPublishedNavigation, nextSnapshot)
|
|
157
|
-
) {
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
lastPublishedNavigation = nextSnapshot
|
|
162
|
-
publishNavigationPose(navigationPose)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
receiveCameraPose: (pose) => {
|
|
167
|
-
latestCameraPose = pose
|
|
168
|
-
if (active && applyPendingNavigationPose()) return
|
|
169
|
-
|
|
170
|
-
publishCameraNavigationPose(pose)
|
|
171
|
-
},
|
|
172
|
-
receiveNavigationPose: (pose) => {
|
|
173
|
-
if (
|
|
174
|
-
!active ||
|
|
175
|
-
pose?.source !== '2d' ||
|
|
176
|
-
pose.revision === lastAppliedNavigationRevision ||
|
|
177
|
-
pose.revision === pendingNavigationPose?.revision
|
|
178
|
-
) {
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
pendingNavigationPose = pose
|
|
183
|
-
applyPendingNavigationPose()
|
|
184
|
-
},
|
|
185
|
-
setActive: (nextActive) => {
|
|
186
|
-
if (active === nextActive) return
|
|
187
|
-
active = nextActive
|
|
188
|
-
if (!active) {
|
|
189
|
-
pendingNavigationPose = null
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
if (latestCameraPose) publishCameraNavigationPose(latestCameraPose)
|
|
193
|
-
},
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function useFloorplanCameraSyncBridge() {
|
|
198
|
-
// The compass is portalled into the always-visible viewer area, so its
|
|
199
|
-
// orientation-only 2D pose must still reach the camera while the floorplan
|
|
200
|
-
// itself is hidden in 3D view.
|
|
201
|
-
const bridgeRef = useRef<FloorplanCameraSyncBridge | null>(null)
|
|
202
|
-
if (!bridgeRef.current) {
|
|
203
|
-
bridgeRef.current = createFloorplanCameraSyncBridge({
|
|
204
|
-
applyCameraPose: (pose) => emitter.emit('camera-controls:apply-pose', pose),
|
|
205
|
-
publishNavigationPose: (pose) => liveCameraNavigation.publish(pose),
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
const bridge = bridgeRef.current
|
|
209
|
-
|
|
210
|
-
useEffect(() => subscribeCameraPose(bridge.receiveCameraPose), [bridge])
|
|
211
|
-
|
|
212
|
-
useEffect(() => subscribeNavigationSyncPose(bridge.receiveNavigationPose), [bridge])
|
|
213
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { PascalI18nProvider, usePascalI18n } from '@pascal-app/core'
|
|
4
|
-
import { Html as DreiHtml } from '@react-three/drei'
|
|
5
|
-
import { type ComponentProps, forwardRef } from 'react'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* drei's `Html`, keeping the editor's i18n scope. `Html` renders its children
|
|
9
|
-
* in a separate React root, which sees none of the surrounding context, so a
|
|
10
|
-
* component inside it would translate with the default (English) scope instead
|
|
11
|
-
* of the editor's `?lang=` or `<Editor locale>` one.
|
|
12
|
-
*/
|
|
13
|
-
export const Html = forwardRef<HTMLDivElement, Omit<ComponentProps<typeof DreiHtml>, 'ref'>>(
|
|
14
|
-
function Html({ children, ...props }, ref) {
|
|
15
|
-
const i18n = usePascalI18n()
|
|
16
|
-
return (
|
|
17
|
-
<DreiHtml {...props} ref={ref}>
|
|
18
|
-
<PascalI18nProvider i18n={i18n}>{children}</PascalI18nProvider>
|
|
19
|
-
</DreiHtml>
|
|
20
|
-
)
|
|
21
|
-
},
|
|
22
|
-
)
|