@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.
Files changed (260) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +11 -11
  5. package/src/components/editor/editor-layout-v2.tsx +1 -1
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +20 -1981
  8. package/src/components/editor/floating-action-menu.tsx +1 -1
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floor-region-controls.tsx +6 -2
  11. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  12. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  13. package/src/components/editor/floorplan-panel.tsx +26 -22
  14. package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
  15. package/src/components/editor/follow-pascal.test.tsx +1 -2
  16. package/src/components/editor/follow-pascal.tsx +1 -1
  17. package/src/components/editor/footprint-height-handle.tsx +1 -1
  18. package/src/components/editor/group-actions.ts +1 -2
  19. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  20. package/src/components/editor/group-move-3d.ts +1 -2
  21. package/src/components/editor/group-rotate-handle.tsx +1 -2
  22. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  23. package/src/components/editor/index.tsx +24 -14
  24. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  25. package/src/components/editor/node-arrow-handles.tsx +1 -1
  26. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  27. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  28. package/src/components/editor/room-controls.tsx +7 -4
  29. package/src/components/editor/room-handles-2d.tsx +2 -3
  30. package/src/components/editor/room-handles.tsx +1 -2
  31. package/src/components/editor/room-highlight.tsx +6 -105
  32. package/src/components/editor/room-labels.test.ts +8 -822
  33. package/src/components/editor/room-labels.tsx +40 -1671
  34. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  35. package/src/components/editor/selection-manager.tsx +5 -3
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  38. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  39. package/src/components/editor/views/plugins-view.tsx +1 -1
  40. package/src/components/editor/views/view-bar.tsx +9 -9
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +1 -1
  43. package/src/components/editor/wall-pull-grip.tsx +2 -2
  44. package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
  45. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
  46. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
  47. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
  48. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  49. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  50. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  51. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
  52. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
  53. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
  54. package/src/components/systems/roof/roof-edit-system.tsx +4 -3
  55. package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
  56. package/src/components/tools/item/placement-history.test.tsx +1 -1
  57. package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
  58. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
  59. package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
  60. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  61. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  62. package/src/components/tools/shared/placement-box.tsx +1 -1
  63. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  64. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  65. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  66. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  67. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  68. package/src/components/ui/action-menu/index.tsx +3 -3
  69. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  70. package/src/components/ui/action-menu/placement.ts +0 -18
  71. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  72. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  73. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  74. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  75. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  76. package/src/components/ui/controls/select-control.tsx +1 -1
  77. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  78. package/src/components/ui/floating-level-selector.tsx +3 -3
  79. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  80. package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
  81. package/src/components/ui/helpers/helper-manager.tsx +1 -1
  82. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  83. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  84. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  85. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  86. package/src/components/ui/panels/node-display.ts +1 -1
  87. package/src/components/ui/panels/panel-manager.tsx +1 -1
  88. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  89. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  90. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  91. package/src/components/ui/panels/room-name-options.ts +1 -5
  92. package/src/components/ui/panels/room-panel.tsx +6 -8
  93. package/src/components/ui/primitives/color-dot.tsx +2 -2
  94. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  95. package/src/components/ui/primitives/sidebar.tsx +3 -3
  96. package/src/components/ui/right-stack.tsx +1 -1
  97. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  98. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  99. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  100. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  104. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  107. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  109. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  110. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  112. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  113. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  114. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  115. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  116. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  117. package/src/components/viewer-overlay.tsx +1 -2
  118. package/src/hooks/use-auto-frame.ts +1 -2
  119. package/src/hooks/use-keyboard.ts +1 -2
  120. package/src/hooks/use-selected-room.ts +5 -7
  121. package/src/i18n/floor-footprint-name.ts +1 -1
  122. package/src/i18n/locales/en/messages.po +180 -182
  123. package/src/i18n/locales/es/messages.po +180 -182
  124. package/src/i18n/locales/fr/messages.po +180 -182
  125. package/src/i18n/locales/ja/messages.po +180 -182
  126. package/src/i18n/locales/zh-CN/messages.po +180 -182
  127. package/src/i18n/locales.test.ts +1 -1
  128. package/src/index.tsx +109 -131
  129. package/src/lib/below-grade.test.tsx +4 -4
  130. package/src/lib/camera-facing.test.ts +1 -1
  131. package/src/lib/camera-facing.ts +2 -3
  132. package/src/lib/editor-level-display.ts +1 -1
  133. package/src/lib/floor-footprints.test.ts +2 -2
  134. package/src/lib/floor-opening-draft.ts +1 -1
  135. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  136. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  137. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  138. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  139. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  140. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  141. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  142. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  143. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  144. package/src/lib/floorplan/geometry.ts +2 -112
  145. package/src/lib/floorplan/index.ts +0 -9
  146. package/src/lib/floorplan/items.ts +2 -1
  147. package/src/lib/floorplan/schedules.ts +6 -3
  148. package/src/lib/floorplan/sheet-export.ts +7 -3
  149. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  150. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  151. package/src/lib/follow-pascal-presentation.ts +1 -1
  152. package/src/lib/glb-export.ts +1 -1
  153. package/src/lib/material-paint.ts +1 -1
  154. package/src/lib/measurements.test.ts +2 -1
  155. package/src/lib/mezzanine-selection.test.ts +1 -1
  156. package/src/lib/paint-region-mode.ts +1 -1
  157. package/src/lib/point-ask/session.ts +1 -2
  158. package/src/lib/roof-duplication.test.ts +100 -0
  159. package/src/lib/roof-duplication.ts +30 -5
  160. package/src/lib/room-handle-drag.ts +2 -12
  161. package/src/lib/room-name-catalog.test.ts +2 -1
  162. package/src/lib/room-name-messages.test.ts +2 -1
  163. package/src/lib/room-name-messages.ts +1 -118
  164. package/src/lib/room-name-storage.test.ts +2 -1
  165. package/src/lib/room-selection-commands.ts +2 -2
  166. package/src/lib/room-selection.test.ts +6 -6
  167. package/src/lib/room-transform-session.ts +1 -2
  168. package/src/lib/room-zone-routing.ts +2 -2
  169. package/src/lib/selection-routing.test.ts +0 -28
  170. package/src/lib/selection-routing.ts +1 -40
  171. package/src/lib/sfx-bus.ts +8 -32
  172. package/src/lib/sfx-player.test.ts +72 -1
  173. package/src/lib/sfx-player.ts +49 -337
  174. package/src/lib/viewer-selection.test.ts +38 -10
  175. package/src/lib/wall-pull.test.ts +1 -1
  176. package/src/lib/wall-pull.ts +1 -27
  177. package/src/store/use-editor.tsx +17 -26
  178. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  179. package/src/store/use-wall-region-handles-selection.ts +1 -2
  180. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  181. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  182. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  183. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  184. package/src/components/editor/first-person/ground-holes.ts +0 -130
  185. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  186. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  187. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  188. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  189. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  190. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  191. package/src/components/editor/localized-html.tsx +0 -22
  192. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  193. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  194. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  195. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  196. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  197. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  198. package/src/components/editor-2d/svg-paths.ts +0 -124
  199. package/src/components/ui/action-menu/action-button.tsx +0 -89
  200. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  201. package/src/components/ui/primitives/button.tsx +0 -69
  202. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  203. package/src/components/ui/primitives/popover.tsx +0 -42
  204. package/src/components/ui/primitives/switch.tsx +0 -29
  205. package/src/components/ui/primitives/tooltip.tsx +0 -57
  206. package/src/components/ui/scene-loader.tsx +0 -73
  207. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  208. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  209. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  210. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  211. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  212. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  213. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  214. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  215. package/src/components/viewer/render-settings-menu.tsx +0 -294
  216. package/src/components/viewer/sky-control.tsx +0 -710
  217. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  218. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  219. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  220. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  221. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  222. package/src/components/viewer/viewer-rooms.tsx +0 -248
  223. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  224. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  225. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  226. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  227. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  228. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  229. package/src/components/viewer/viewer-stage.tsx +0 -221
  230. package/src/components/walkthrough-hud.tsx +0 -193
  231. package/src/hooks/use-mobile.ts +0 -19
  232. package/src/i18n/level-name.ts +0 -39
  233. package/src/i18n/scene-theme-name.ts +0 -16
  234. package/src/lib/below-grade.ts +0 -133
  235. package/src/lib/camera-pose.test.ts +0 -237
  236. package/src/lib/camera-pose.ts +0 -172
  237. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  238. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  239. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  240. package/src/lib/floorplan/marks.test.ts +0 -213
  241. package/src/lib/floorplan/marks.ts +0 -238
  242. package/src/lib/measurements.ts +0 -8
  243. package/src/lib/node-display-label.ts +0 -37
  244. package/src/lib/room-assembly-overlay.test.ts +0 -275
  245. package/src/lib/room-assembly-overlay.ts +0 -265
  246. package/src/lib/room-selection.ts +0 -417
  247. package/src/lib/scene-bounds.test.ts +0 -327
  248. package/src/lib/scene-bounds.ts +0 -233
  249. package/src/lib/typing-target.ts +0 -9
  250. package/src/lib/viewer-selection.ts +0 -595
  251. package/src/lib/zone-content.ts +0 -133
  252. package/src/store/camera-pose-store.test.ts +0 -29
  253. package/src/store/camera-pose-store.ts +0 -27
  254. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  255. package/src/store/navigation-sync-pose-store.ts +0 -27
  256. package/src/store/use-audio.test.ts +0 -146
  257. package/src/store/use-audio.tsx +0 -169
  258. package/src/store/use-first-person-hud.ts +0 -40
  259. package/src/store/use-follow-pascal.test.ts +0 -62
  260. package/src/store/use-follow-pascal.ts +0 -53
@@ -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
- })
@@ -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
- })