@pascal-app/editor 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +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/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
package/src/lib/zone-content.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyNode,
|
|
3
|
-
type AnyNodeId,
|
|
4
|
-
type CeilingNode,
|
|
5
|
-
type ItemNode,
|
|
6
|
-
pointInPolygon2D,
|
|
7
|
-
pointOnSegment,
|
|
8
|
-
type SlabNode,
|
|
9
|
-
type WallNode,
|
|
10
|
-
type ZoneNode,
|
|
11
|
-
} from '@pascal-app/core'
|
|
12
|
-
|
|
13
|
-
type Point2D = [number, number]
|
|
14
|
-
|
|
15
|
-
const POINT_TOLERANCE = 0.5
|
|
16
|
-
const COLLINEAR_TOLERANCE = 1e-6
|
|
17
|
-
const SURFACE_POLYGON_TOLERANCE = 0.15
|
|
18
|
-
|
|
19
|
-
function getPointToSegmentDistance(point: Point2D, start: Point2D, end: Point2D): number {
|
|
20
|
-
const dx = end[0] - start[0]
|
|
21
|
-
const dz = end[1] - start[1]
|
|
22
|
-
const lengthSq = dx * dx + dz * dz
|
|
23
|
-
if (lengthSq === 0) return Math.hypot(point[0] - start[0], point[1] - start[1])
|
|
24
|
-
|
|
25
|
-
const rawT = ((point[0] - start[0]) * dx + (point[1] - start[1]) * dz) / lengthSq
|
|
26
|
-
const t = Math.max(0, Math.min(1, rawT))
|
|
27
|
-
const projected: Point2D = [start[0] + t * dx, start[1] + t * dz]
|
|
28
|
-
return Math.hypot(point[0] - projected[0], point[1] - projected[1])
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function pointInPolygonWithTolerance(point: Point2D, polygon: Point2D[]): boolean {
|
|
32
|
-
if (pointInPolygon2D(point, polygon, { includeBoundary: true })) return true
|
|
33
|
-
return polygon.some((start, index) => {
|
|
34
|
-
const end = polygon[(index + 1) % polygon.length]
|
|
35
|
-
return end ? getPointToSegmentDistance(point, start, end) <= POINT_TOLERANCE : false
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function polygonContainsWithTolerance(
|
|
40
|
-
outer: Point2D[],
|
|
41
|
-
inner: Point2D[],
|
|
42
|
-
tolerance: number,
|
|
43
|
-
): boolean {
|
|
44
|
-
return inner.every((point) => {
|
|
45
|
-
if (pointInPolygon2D(point, outer, { includeBoundary: true })) return true
|
|
46
|
-
return outer.some((start, index) => {
|
|
47
|
-
const end = outer[(index + 1) % outer.length]
|
|
48
|
-
return end ? getPointToSegmentDistance(point, start, end) <= tolerance : false
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function polygonMatchesZoneFootprint(
|
|
54
|
-
surfacePolygon: Point2D[],
|
|
55
|
-
footprint: Point2D[],
|
|
56
|
-
): boolean {
|
|
57
|
-
if (surfacePolygon.length < 3) return false
|
|
58
|
-
return (
|
|
59
|
-
polygonContainsWithTolerance(footprint, surfacePolygon, SURFACE_POLYGON_TOLERANCE) &&
|
|
60
|
-
polygonContainsWithTolerance(surfacePolygon, footprint, SURFACE_POLYGON_TOLERANCE)
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function areSegmentsCollinear(a: Point2D, b: Point2D, c: Point2D, d: Point2D): boolean {
|
|
65
|
-
const abx = b[0] - a[0]
|
|
66
|
-
const abz = b[1] - a[1]
|
|
67
|
-
const acx = c[0] - a[0]
|
|
68
|
-
const acz = c[1] - a[1]
|
|
69
|
-
const adx = d[0] - a[0]
|
|
70
|
-
const adz = d[1] - a[1]
|
|
71
|
-
const crossC = abx * acz - abz * acx
|
|
72
|
-
const crossD = abx * adz - abz * adx
|
|
73
|
-
return Math.abs(crossC) <= COLLINEAR_TOLERANCE && Math.abs(crossD) <= COLLINEAR_TOLERANCE
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function segmentsOverlap(a: Point2D, b: Point2D, c: Point2D, d: Point2D): boolean {
|
|
77
|
-
const useX = Math.abs(b[0] - a[0]) >= Math.abs(b[1] - a[1])
|
|
78
|
-
const a0 = useX ? a[0] : a[1]
|
|
79
|
-
const a1 = useX ? b[0] : b[1]
|
|
80
|
-
const c0 = useX ? c[0] : c[1]
|
|
81
|
-
const c1 = useX ? d[0] : d[1]
|
|
82
|
-
const minA = Math.min(a0, a1)
|
|
83
|
-
const maxA = Math.max(a0, a1)
|
|
84
|
-
const minC = Math.min(c0, c1)
|
|
85
|
-
const maxC = Math.max(c0, c1)
|
|
86
|
-
return Math.max(minA, minC) <= Math.min(maxA, maxC) + COLLINEAR_TOLERANCE
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function wallLiesOnZoneBoundary(wall: WallNode, polygon: Point2D[]): boolean {
|
|
90
|
-
return polygon.some((start, index) => {
|
|
91
|
-
const end = polygon[(index + 1) % polygon.length]
|
|
92
|
-
if (!end) return false
|
|
93
|
-
return (
|
|
94
|
-
areSegmentsCollinear(wall.start, wall.end, start, end) &&
|
|
95
|
-
segmentsOverlap(wall.start, wall.end, start, end) &&
|
|
96
|
-
pointOnSegment(wall.start, start, end, POINT_TOLERANCE) &&
|
|
97
|
-
pointOnSegment(wall.end, start, end, POINT_TOLERANCE)
|
|
98
|
-
)
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function collectZoneContentIds(
|
|
103
|
-
nodes: Readonly<Record<AnyNodeId, AnyNode>>,
|
|
104
|
-
zone: ZoneNode,
|
|
105
|
-
): AnyNodeId[] {
|
|
106
|
-
const levelId = zone.parentId
|
|
107
|
-
if (!levelId) return []
|
|
108
|
-
|
|
109
|
-
const footprint = zone.polygon.map((point) => [point[0], point[1]] as Point2D)
|
|
110
|
-
const boundaryWalls = Object.values(nodes)
|
|
111
|
-
.filter((node): node is WallNode => node.type === 'wall' && node.parentId === levelId)
|
|
112
|
-
.filter((wall) => wallLiesOnZoneBoundary(wall, footprint))
|
|
113
|
-
const surfaces = Object.values(nodes)
|
|
114
|
-
.filter(
|
|
115
|
-
(node): node is SlabNode | CeilingNode =>
|
|
116
|
-
(node.type === 'slab' || node.type === 'ceiling') && node.parentId === levelId,
|
|
117
|
-
)
|
|
118
|
-
.filter((surface) => {
|
|
119
|
-
const polygon = surface.polygon.map((point) => [point[0], point[1]] as Point2D)
|
|
120
|
-
return polygonMatchesZoneFootprint(polygon, footprint)
|
|
121
|
-
})
|
|
122
|
-
const floorItems = Object.values(nodes)
|
|
123
|
-
.filter((node): node is ItemNode => node.type === 'item' && node.parentId === levelId)
|
|
124
|
-
.filter((item) => pointInPolygonWithTolerance([item.position[0], item.position[2]], footprint))
|
|
125
|
-
|
|
126
|
-
return Array.from(
|
|
127
|
-
new Set<AnyNodeId>([
|
|
128
|
-
...boundaryWalls.map((wall) => wall.id as AnyNodeId),
|
|
129
|
-
...surfaces.map((surface) => surface.id as AnyNodeId),
|
|
130
|
-
...floorItems.map((item) => item.id as AnyNodeId),
|
|
131
|
-
]),
|
|
132
|
-
)
|
|
133
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { CameraPose } from '@pascal-app/core'
|
|
3
|
-
import { cameraPoseStore, publishCameraPose, subscribeCameraPose } from './camera-pose-store'
|
|
4
|
-
|
|
5
|
-
const pose: CameraPose = {
|
|
6
|
-
position: [3, 4, 5],
|
|
7
|
-
projection: 'perspective',
|
|
8
|
-
target: [0, 1, 0],
|
|
9
|
-
viewWidth: 12,
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
cameraPoseStore.setState({ pose: null })
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe('camera pose store', () => {
|
|
17
|
-
test('replays the latest pose and streams later poses to focused subscribers', () => {
|
|
18
|
-
publishCameraPose(pose)
|
|
19
|
-
const received: CameraPose[] = []
|
|
20
|
-
const unsubscribe = subscribeCameraPose((nextPose) => received.push(nextPose))
|
|
21
|
-
const nextPose = { ...pose, viewWidth: 8 }
|
|
22
|
-
|
|
23
|
-
publishCameraPose(nextPose)
|
|
24
|
-
unsubscribe()
|
|
25
|
-
publishCameraPose({ ...pose, viewWidth: 4 })
|
|
26
|
-
|
|
27
|
-
expect(received).toEqual([pose, nextPose])
|
|
28
|
-
})
|
|
29
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { CameraPose } from '@pascal-app/core'
|
|
2
|
-
import { subscribeWithSelector } from 'zustand/middleware'
|
|
3
|
-
import { createStore } from 'zustand/vanilla'
|
|
4
|
-
|
|
5
|
-
type CameraPoseState = {
|
|
6
|
-
pose: CameraPose | null
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const cameraPoseStore = createStore<CameraPoseState>()(
|
|
10
|
-
subscribeWithSelector<CameraPoseState>(() => ({ pose: null })),
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
export function publishCameraPose(pose: CameraPose) {
|
|
14
|
-
cameraPoseStore.setState({ pose })
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function subscribeCameraPose(listener: (pose: CameraPose) => void) {
|
|
18
|
-
const currentPose = cameraPoseStore.getState().pose
|
|
19
|
-
if (currentPose) listener(currentPose)
|
|
20
|
-
|
|
21
|
-
return cameraPoseStore.subscribe(
|
|
22
|
-
(state) => state.pose,
|
|
23
|
-
(pose) => {
|
|
24
|
-
if (pose) listener(pose)
|
|
25
|
-
},
|
|
26
|
-
)
|
|
27
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
navigationSyncPoseStore,
|
|
4
|
-
publishNavigationSyncPoseToStore,
|
|
5
|
-
subscribeNavigationSyncPose,
|
|
6
|
-
} from './navigation-sync-pose-store'
|
|
7
|
-
import type { NavigationSyncPose } from './use-editor'
|
|
8
|
-
|
|
9
|
-
const pose = {
|
|
10
|
-
source: '2d' as const,
|
|
11
|
-
revision: 1,
|
|
12
|
-
target: [1, 2, 3] as [number, number, number],
|
|
13
|
-
azimuth: 0.5,
|
|
14
|
-
viewWidth: 12,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
navigationSyncPoseStore.setState({ pose: null })
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
describe('navigation sync pose store', () => {
|
|
22
|
-
test('replays the latest pose and streams only later pose writes', () => {
|
|
23
|
-
publishNavigationSyncPoseToStore(pose)
|
|
24
|
-
const received: NavigationSyncPose[] = []
|
|
25
|
-
const unsubscribe = subscribeNavigationSyncPose((nextPose) => received.push(nextPose))
|
|
26
|
-
const nextPose = { ...pose, revision: 2, viewWidth: 8 }
|
|
27
|
-
|
|
28
|
-
publishNavigationSyncPoseToStore(nextPose)
|
|
29
|
-
unsubscribe()
|
|
30
|
-
publishNavigationSyncPoseToStore({ ...pose, revision: 3 })
|
|
31
|
-
|
|
32
|
-
expect(received).toEqual([pose, nextPose])
|
|
33
|
-
})
|
|
34
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { subscribeWithSelector } from 'zustand/middleware'
|
|
2
|
-
import { createStore } from 'zustand/vanilla'
|
|
3
|
-
import type { NavigationSyncPose } from './use-editor'
|
|
4
|
-
|
|
5
|
-
type NavigationSyncPoseState = {
|
|
6
|
-
pose: NavigationSyncPose | null
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const navigationSyncPoseStore = createStore<NavigationSyncPoseState>()(
|
|
10
|
-
subscribeWithSelector<NavigationSyncPoseState>(() => ({ pose: null })),
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
export function publishNavigationSyncPoseToStore(pose: NavigationSyncPose) {
|
|
14
|
-
navigationSyncPoseStore.setState({ pose })
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function subscribeNavigationSyncPose(listener: (pose: NavigationSyncPose) => void) {
|
|
18
|
-
const currentPose = navigationSyncPoseStore.getState().pose
|
|
19
|
-
if (currentPose) listener(currentPose)
|
|
20
|
-
|
|
21
|
-
return navigationSyncPoseStore.subscribe(
|
|
22
|
-
(state) => state.pose,
|
|
23
|
-
(pose) => {
|
|
24
|
-
if (pose) listener(pose)
|
|
25
|
-
},
|
|
26
|
-
)
|
|
27
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import useAudio, { AUDIO_MUTED_STORAGE_KEY, AUDIO_SETTINGS_STORAGE_KEY } from './use-audio'
|
|
3
|
-
|
|
4
|
-
function memoryStorage() {
|
|
5
|
-
const items = new Map<string, string>()
|
|
6
|
-
return {
|
|
7
|
-
items,
|
|
8
|
-
getItem: (key: string) => items.get(key) ?? null,
|
|
9
|
-
setItem: (key: string, value: string) => void items.set(key, value),
|
|
10
|
-
removeItem: (key: string) => void items.delete(key),
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const originals = {
|
|
15
|
-
localStorage: Object.getOwnPropertyDescriptor(globalThis, 'localStorage'),
|
|
16
|
-
sessionStorage: Object.getOwnPropertyDescriptor(globalThis, 'sessionStorage'),
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function use(name: 'localStorage' | 'sessionStorage', storage: ReturnType<typeof memoryStorage>) {
|
|
20
|
-
Object.defineProperty(globalThis, name, { configurable: true, value: storage })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** One browser profile: its tabs share `localStorage`, each has its own `sessionStorage`. */
|
|
24
|
-
let browser = memoryStorage()
|
|
25
|
-
|
|
26
|
-
/** Another tab of the profile saves the settings: the browser stores them and tells this tab. */
|
|
27
|
-
function savedInAnotherTab(state: Record<string, unknown>) {
|
|
28
|
-
const value = JSON.stringify({ state, version: 0 })
|
|
29
|
-
browser.setItem(AUDIO_SETTINGS_STORAGE_KEY, value)
|
|
30
|
-
globalThis.dispatchEvent(
|
|
31
|
-
Object.assign(new Event('storage'), { key: AUDIO_SETTINGS_STORAGE_KEY, newValue: value }),
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** The editor loading in a tab: a new one, or the same one reloaded (its session kept). */
|
|
36
|
-
async function loadIn(tab: ReturnType<typeof memoryStorage>) {
|
|
37
|
-
use('sessionStorage', tab)
|
|
38
|
-
await useAudio.persist.rehydrate()
|
|
39
|
-
return useAudio.getState()
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
browser = memoryStorage()
|
|
44
|
-
use('localStorage', browser)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
afterAll(() => {
|
|
48
|
-
for (const [name, descriptor] of Object.entries(originals)) {
|
|
49
|
-
if (descriptor) Object.defineProperty(globalThis, name, descriptor)
|
|
50
|
-
else Reflect.deleteProperty(globalThis, name)
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('muting a tab', () => {
|
|
55
|
-
test('a tab keeps its own mute across reloads, and a new tab starts from the last choice', async () => {
|
|
56
|
-
const first = memoryStorage()
|
|
57
|
-
const second = memoryStorage()
|
|
58
|
-
|
|
59
|
-
expect((await loadIn(first)).muted).toBe(false)
|
|
60
|
-
useAudio.getState().toggleMute()
|
|
61
|
-
|
|
62
|
-
expect((await loadIn(second)).muted).toBe(true)
|
|
63
|
-
useAudio.getState().setMuted(false)
|
|
64
|
-
|
|
65
|
-
expect((await loadIn(first)).muted).toBe(true)
|
|
66
|
-
expect((await loadIn(memoryStorage())).muted).toBe(false)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test("another tab's choice does not change a tab that never chose", async () => {
|
|
70
|
-
const quiet = memoryStorage()
|
|
71
|
-
expect((await loadIn(quiet)).muted).toBe(false)
|
|
72
|
-
|
|
73
|
-
await loadIn(memoryStorage())
|
|
74
|
-
useAudio.getState().setMuted(true)
|
|
75
|
-
|
|
76
|
-
expect((await loadIn(quiet)).muted).toBe(false)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test("volumes are every tab's and never carry a mute", async () => {
|
|
80
|
-
const muting = memoryStorage()
|
|
81
|
-
await loadIn(muting)
|
|
82
|
-
useAudio.getState().setMuted(true)
|
|
83
|
-
|
|
84
|
-
await loadIn(memoryStorage())
|
|
85
|
-
useAudio.getState().setMuted(false)
|
|
86
|
-
await loadIn(muting)
|
|
87
|
-
useAudio.getState().setSfxVolume(20)
|
|
88
|
-
|
|
89
|
-
expect(JSON.parse(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)!).state).not.toHaveProperty(
|
|
90
|
-
'muted',
|
|
91
|
-
)
|
|
92
|
-
expect(browser.getItem(AUDIO_MUTED_STORAGE_KEY)).toBe('false')
|
|
93
|
-
const next = await loadIn(memoryStorage())
|
|
94
|
-
expect(next.sfxVolume).toBe(20)
|
|
95
|
-
expect(next.muted).toBe(false)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test("a volume another tab sets reaches this one at once, and this tab's saves carry it", async () => {
|
|
99
|
-
await loadIn(memoryStorage())
|
|
100
|
-
useAudio.getState().setSfxVolume(30)
|
|
101
|
-
useAudio.getState().setRadioPlaying(true)
|
|
102
|
-
const stored = JSON.parse(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)!)
|
|
103
|
-
|
|
104
|
-
savedInAnotherTab({ ...stored.state, isRadioPlaying: false, muted: true, sfxVolume: 80 })
|
|
105
|
-
|
|
106
|
-
expect(useAudio.getState()).toMatchObject({ isRadioPlaying: true, muted: false, sfxVolume: 80 })
|
|
107
|
-
const setElsewhere = browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)
|
|
108
|
-
useAudio.getState().setMuted(true)
|
|
109
|
-
expect(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)).toBe(setElsewhere)
|
|
110
|
-
// The radio pausing as the tab is hidden saves every setting: the other tab's volume stays.
|
|
111
|
-
useAudio.getState().setRadioPlaying(false)
|
|
112
|
-
useAudio.getState().setMasterVolume(60)
|
|
113
|
-
expect(JSON.parse(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)!).state).toMatchObject({
|
|
114
|
-
masterVolume: 60,
|
|
115
|
-
sfxVolume: 80,
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
test("a mute saved when it was every tab's still holds, in settings the old build still reads", async () => {
|
|
120
|
-
browser.setItem(
|
|
121
|
-
AUDIO_SETTINGS_STORAGE_KEY,
|
|
122
|
-
JSON.stringify({ state: { masterVolume: 40, muted: true, sfxVolume: 50 }, version: 0 }),
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
const state = await loadIn(memoryStorage())
|
|
126
|
-
|
|
127
|
-
expect(state.muted).toBe(true)
|
|
128
|
-
expect(state.masterVolume).toBe(40)
|
|
129
|
-
expect((await loadIn(memoryStorage())).muted).toBe(true)
|
|
130
|
-
useAudio.getState().setSfxVolume(45)
|
|
131
|
-
const saved = JSON.parse(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)!)
|
|
132
|
-
expect(saved).toMatchObject({ state: { masterVolume: 40, sfxVolume: 45 }, version: 0 })
|
|
133
|
-
expect(saved.state).not.toHaveProperty('muted')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
test('a tab still on the old settings does not undo a choice made since', async () => {
|
|
137
|
-
await loadIn(memoryStorage())
|
|
138
|
-
useAudio.getState().setMuted(false)
|
|
139
|
-
browser.setItem(
|
|
140
|
-
AUDIO_SETTINGS_STORAGE_KEY,
|
|
141
|
-
JSON.stringify({ state: { muted: true, sfxVolume: 50 }, version: 0 }),
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
expect((await loadIn(memoryStorage())).muted).toBe(false)
|
|
145
|
-
})
|
|
146
|
-
})
|
package/src/store/use-audio.tsx
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { create } from 'zustand'
|
|
4
|
-
import { createJSONStorage, persist } from 'zustand/middleware'
|
|
5
|
-
|
|
6
|
-
export const AUDIO_SETTINGS_STORAGE_KEY = 'pascal-audio-settings'
|
|
7
|
-
/**
|
|
8
|
-
* Mute is each tab's own, so someone working in two projects can silence one and hear the other.
|
|
9
|
-
* Under this key `sessionStorage` holds this tab's mute (kept across its reloads) and
|
|
10
|
-
* `localStorage` the last choice made in any tab, which a new tab starts from.
|
|
11
|
-
*/
|
|
12
|
-
export const AUDIO_MUTED_STORAGE_KEY = 'pascal-audio-muted'
|
|
13
|
-
|
|
14
|
-
interface AudioState {
|
|
15
|
-
masterVolume: number
|
|
16
|
-
sfxVolume: number
|
|
17
|
-
radioVolume: number
|
|
18
|
-
isRadioPlaying: boolean
|
|
19
|
-
/** This tab's mute; the volumes are every tab's. */
|
|
20
|
-
muted: boolean
|
|
21
|
-
autoplay: boolean
|
|
22
|
-
setMasterVolume: (v: number) => void
|
|
23
|
-
setSfxVolume: (v: number) => void
|
|
24
|
-
setRadioVolume: (v: number) => void
|
|
25
|
-
setRadioPlaying: (v: boolean) => void
|
|
26
|
-
toggleRadioPlaying: () => void
|
|
27
|
-
setMuted: (muted: boolean) => void
|
|
28
|
-
toggleMute: () => void
|
|
29
|
-
setAutoplay: (v: boolean) => void
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type StoredSettings = Pick<
|
|
33
|
-
AudioState,
|
|
34
|
-
'masterVolume' | 'sfxVolume' | 'radioVolume' | 'isRadioPlaying' | 'autoplay'
|
|
35
|
-
>
|
|
36
|
-
|
|
37
|
-
type BrowserStorage = 'localStorage' | 'sessionStorage'
|
|
38
|
-
|
|
39
|
-
// Storage can be missing (a test, a server render) or refuse access (site data blocked): the page
|
|
40
|
-
// then keeps its choices in memory only.
|
|
41
|
-
function readStored(storage: BrowserStorage, key: string) {
|
|
42
|
-
try {
|
|
43
|
-
return globalThis[storage]?.getItem(key) ?? null
|
|
44
|
-
} catch {
|
|
45
|
-
return null
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function writeStored(storage: BrowserStorage, key: string, value: string) {
|
|
50
|
-
try {
|
|
51
|
-
globalThis[storage]?.setItem(key, value)
|
|
52
|
-
} catch {}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** This tab's mute. A tab without one takes the last choice made in any tab, and keeps it. */
|
|
56
|
-
function tabMuted(): boolean {
|
|
57
|
-
const own = readStored('sessionStorage', AUDIO_MUTED_STORAGE_KEY)
|
|
58
|
-
if (own !== null) return own === 'true'
|
|
59
|
-
const muted = readStored('localStorage', AUDIO_MUTED_STORAGE_KEY) === 'true'
|
|
60
|
-
writeStored('sessionStorage', AUDIO_MUTED_STORAGE_KEY, String(muted))
|
|
61
|
-
return muted
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** A mute chosen in this tab: this tab's, and where the next new tab starts. Open tabs keep theirs. */
|
|
65
|
-
function chooseMuted(muted: boolean) {
|
|
66
|
-
writeStored('sessionStorage', AUDIO_MUTED_STORAGE_KEY, String(muted))
|
|
67
|
-
writeStored('localStorage', AUDIO_MUTED_STORAGE_KEY, String(muted))
|
|
68
|
-
return { muted }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// The settings as this tab last read, wrote or took from another tab. A save carries every
|
|
72
|
-
// setting, so a save that changes none (a mute, which is this tab's own) is skipped.
|
|
73
|
-
let heldSettings: string | null = null
|
|
74
|
-
// Set while this tab takes another tab's volumes: that is no choice of its own to save back.
|
|
75
|
-
let adopting = false
|
|
76
|
-
|
|
77
|
-
const settingsStorage = createJSONStorage<StoredSettings>(() => ({
|
|
78
|
-
getItem: (key) => {
|
|
79
|
-
heldSettings = readStored('localStorage', key)
|
|
80
|
-
return heldSettings
|
|
81
|
-
},
|
|
82
|
-
setItem: (key, value) => {
|
|
83
|
-
if (value === heldSettings) return
|
|
84
|
-
heldSettings = value
|
|
85
|
-
if (!adopting) writeStored('localStorage', key, value)
|
|
86
|
-
},
|
|
87
|
-
removeItem: (key) => {
|
|
88
|
-
try {
|
|
89
|
-
globalThis.localStorage?.removeItem(key)
|
|
90
|
-
} catch {}
|
|
91
|
-
},
|
|
92
|
-
}))
|
|
93
|
-
|
|
94
|
-
const useAudio = create<AudioState>()(
|
|
95
|
-
persist(
|
|
96
|
-
(set) => ({
|
|
97
|
-
masterVolume: 70,
|
|
98
|
-
sfxVolume: 50,
|
|
99
|
-
radioVolume: 25,
|
|
100
|
-
isRadioPlaying: false,
|
|
101
|
-
muted: false,
|
|
102
|
-
autoplay: true,
|
|
103
|
-
setMasterVolume: (v) => set({ masterVolume: v }),
|
|
104
|
-
setSfxVolume: (v) => set({ sfxVolume: v }),
|
|
105
|
-
setRadioVolume: (v) => set({ radioVolume: v }),
|
|
106
|
-
setRadioPlaying: (v) => set({ isRadioPlaying: v }),
|
|
107
|
-
toggleRadioPlaying: () => set((state) => ({ isRadioPlaying: !state.isRadioPlaying })),
|
|
108
|
-
setMuted: (muted) => set(chooseMuted(muted)),
|
|
109
|
-
toggleMute: () => set((state) => chooseMuted(!state.muted)),
|
|
110
|
-
setAutoplay: (v) => set({ autoplay: v }),
|
|
111
|
-
}),
|
|
112
|
-
{
|
|
113
|
-
name: AUDIO_SETTINGS_STORAGE_KEY,
|
|
114
|
-
storage: settingsStorage,
|
|
115
|
-
partialize: ({ masterVolume, sfxVolume, radioVolume, isRadioPlaying, autoplay }) => ({
|
|
116
|
-
masterVolume,
|
|
117
|
-
sfxVolume,
|
|
118
|
-
radioVolume,
|
|
119
|
-
isRadioPlaying,
|
|
120
|
-
autoplay,
|
|
121
|
-
}),
|
|
122
|
-
// The settings once held one mute for every tab: whoever had muted stays muted, unless a tab
|
|
123
|
-
// has chosen since (a tab still on that build can write it again). The format is unchanged,
|
|
124
|
-
// so that build reads these settings back with only the mute gone.
|
|
125
|
-
merge: (persisted, current) => {
|
|
126
|
-
const { muted, ...settings } = (persisted ?? {}) as Partial<StoredSettings> & {
|
|
127
|
-
muted?: unknown
|
|
128
|
-
}
|
|
129
|
-
if (
|
|
130
|
-
typeof muted === 'boolean' &&
|
|
131
|
-
readStored('localStorage', AUDIO_MUTED_STORAGE_KEY) === null
|
|
132
|
-
) {
|
|
133
|
-
writeStored('localStorage', AUDIO_MUTED_STORAGE_KEY, String(muted))
|
|
134
|
-
}
|
|
135
|
-
return { ...current, ...settings, muted: tabMuted() }
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
),
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Another tab saved the settings: the volumes are every tab's, so this tab takes them at once, and
|
|
143
|
-
* a later save of its own carries them. Its mute and whether its radio plays stay its own.
|
|
144
|
-
*/
|
|
145
|
-
function adoptOtherTabSettings(event: StorageEvent) {
|
|
146
|
-
if (event.key !== AUDIO_SETTINGS_STORAGE_KEY || event.newValue === null) return
|
|
147
|
-
let stored: Partial<Record<keyof StoredSettings, unknown>> | undefined
|
|
148
|
-
try {
|
|
149
|
-
stored = JSON.parse(event.newValue)?.state
|
|
150
|
-
} catch {
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
const shared: Partial<StoredSettings> = {}
|
|
154
|
-
for (const key of ['masterVolume', 'sfxVolume', 'radioVolume'] as const) {
|
|
155
|
-
const value = stored?.[key]
|
|
156
|
-
if (typeof value === 'number') shared[key] = value
|
|
157
|
-
}
|
|
158
|
-
if (typeof stored?.autoplay === 'boolean') shared.autoplay = stored.autoplay
|
|
159
|
-
adopting = true
|
|
160
|
-
try {
|
|
161
|
-
useAudio.setState(shared)
|
|
162
|
-
} finally {
|
|
163
|
-
adopting = false
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
globalThis.addEventListener?.('storage', adoptOtherTabSettings)
|
|
168
|
-
|
|
169
|
-
export default useAudio
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { create } from 'zustand'
|
|
2
|
-
|
|
3
|
-
export type WalkthroughInteract = { label: string; verb: string } | null
|
|
4
|
-
|
|
5
|
-
export type FirstPersonHudState = {
|
|
6
|
-
floorLabel: string | null
|
|
7
|
-
zoneLabel: string | null
|
|
8
|
-
interact: WalkthroughInteract
|
|
9
|
-
setHud: (hud: Partial<Pick<FirstPersonHudState, 'floorLabel' | 'zoneLabel' | 'interact'>>) => void
|
|
10
|
-
reset: () => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const useFirstPersonHud = create<FirstPersonHudState>((set) => ({
|
|
14
|
-
floorLabel: null,
|
|
15
|
-
zoneLabel: null,
|
|
16
|
-
interact: null,
|
|
17
|
-
setHud: (hud) =>
|
|
18
|
-
set((state) => {
|
|
19
|
-
const floorLabel = hud.floorLabel === undefined ? state.floorLabel : hud.floorLabel
|
|
20
|
-
const zoneLabel = hud.zoneLabel === undefined ? state.zoneLabel : hud.zoneLabel
|
|
21
|
-
const interact = hud.interact === undefined ? state.interact : hud.interact
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
floorLabel === state.floorLabel &&
|
|
25
|
-
zoneLabel === state.zoneLabel &&
|
|
26
|
-
interact?.label === state.interact?.label &&
|
|
27
|
-
interact?.verb === state.interact?.verb
|
|
28
|
-
) {
|
|
29
|
-
return state
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return { floorLabel, zoneLabel, interact }
|
|
33
|
-
}),
|
|
34
|
-
reset: () =>
|
|
35
|
-
set((state) =>
|
|
36
|
-
state.floorLabel === null && state.zoneLabel === null && state.interact === null
|
|
37
|
-
? state
|
|
38
|
-
: { floorLabel: null, zoneLabel: null, interact: null },
|
|
39
|
-
),
|
|
40
|
-
}))
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { FOLLOW_PASCAL_STORAGE_KEY, useFollowPascal } from './use-follow-pascal'
|
|
3
|
-
|
|
4
|
-
// Follow Pascal's switch and its pause: touching the camera pauses it and offers "Resume following";
|
|
5
|
-
// a new build starts it again; the switch itself is the person's, kept across sessions.
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
useFollowPascal.setState({ on: true, paused: false, building: false })
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
describe('Follow Pascal', () => {
|
|
11
|
-
test('is on until the person turns it off, and turning it off clears a pause', () => {
|
|
12
|
-
expect(useFollowPascal.getState().on).toBe(true)
|
|
13
|
-
useFollowPascal.getState().pause()
|
|
14
|
-
expect(useFollowPascal.getState().paused).toBe(true)
|
|
15
|
-
useFollowPascal.getState().setOn(false)
|
|
16
|
-
expect(useFollowPascal.getState()).toMatchObject({ on: false, paused: false })
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('a pause waits for the person, and only matters while it is on', () => {
|
|
20
|
-
useFollowPascal.setState({ on: false })
|
|
21
|
-
useFollowPascal.getState().pause()
|
|
22
|
-
expect(useFollowPascal.getState().paused).toBe(false)
|
|
23
|
-
useFollowPascal.setState({ on: true })
|
|
24
|
-
useFollowPascal.getState().pause()
|
|
25
|
-
expect(useFollowPascal.getState().paused).toBe(true)
|
|
26
|
-
useFollowPascal.getState().resume()
|
|
27
|
-
expect(useFollowPascal.getState().paused).toBe(false)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('a build that begins takes the camera again, even after a pause', () => {
|
|
31
|
-
useFollowPascal.getState().pause()
|
|
32
|
-
useFollowPascal.getState().beginBuild()
|
|
33
|
-
expect(useFollowPascal.getState()).toMatchObject({ paused: false, building: true })
|
|
34
|
-
useFollowPascal.getState().endBuild()
|
|
35
|
-
expect(useFollowPascal.getState().building).toBe(false)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test('only the switch is kept across sessions', () => {
|
|
39
|
-
const persisted = (
|
|
40
|
-
useFollowPascal as unknown as {
|
|
41
|
-
persist: { getOptions(): { partialize?: (state: unknown) => unknown; name?: string } }
|
|
42
|
-
}
|
|
43
|
-
).persist.getOptions()
|
|
44
|
-
expect(persisted.name).toBe(FOLLOW_PASCAL_STORAGE_KEY)
|
|
45
|
-
expect(persisted.partialize?.({ on: false, paused: true, building: true })).toEqual({
|
|
46
|
-
on: false,
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('the pill offers itself only to a paused follower during a build', () => {
|
|
51
|
-
const shows = () => {
|
|
52
|
-
const { on, paused, building } = useFollowPascal.getState()
|
|
53
|
-
return on && paused && building
|
|
54
|
-
}
|
|
55
|
-
useFollowPascal.getState().beginBuild()
|
|
56
|
-
expect(shows()).toBe(false)
|
|
57
|
-
useFollowPascal.getState().pause()
|
|
58
|
-
expect(shows()).toBe(true)
|
|
59
|
-
useFollowPascal.getState().endBuild()
|
|
60
|
-
expect(shows()).toBe(false)
|
|
61
|
-
})
|
|
62
|
-
})
|