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