@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,327 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import type { AnyNode } from '@pascal-app/core/schema'
3
- import { computeSceneBoundsXZ } from './scene-bounds'
4
-
5
- function makeWall(start: [number, number], end: [number, number]): AnyNode {
6
- return {
7
- object: 'node',
8
- id: `wall_${start.join('_')}_${end.join('_')}`,
9
- type: 'wall',
10
- parentId: null,
11
- visible: true,
12
- metadata: {},
13
- children: [],
14
- start,
15
- end,
16
- frontSide: 'unknown',
17
- backSide: 'unknown',
18
- } as unknown as AnyNode
19
- }
20
-
21
- function makeZone(polygon: [number, number][]): AnyNode {
22
- return {
23
- object: 'node',
24
- id: `zone_${polygon.length}_${polygon[0]?.[0] ?? 0}`,
25
- type: 'zone',
26
- parentId: null,
27
- visible: true,
28
- metadata: {},
29
- name: 'Zone',
30
- polygon,
31
- color: '#000000',
32
- } as unknown as AnyNode
33
- }
34
-
35
- function makeSite(points: [number, number][]): AnyNode {
36
- return {
37
- object: 'node',
38
- id: 'site_test',
39
- type: 'site',
40
- parentId: null,
41
- visible: true,
42
- metadata: {},
43
- polygon: { type: 'polygon', points },
44
- children: [],
45
- } as unknown as AnyNode
46
- }
47
-
48
- describe('computeSceneBoundsXZ', () => {
49
- test('returns null when given an empty array', () => {
50
- expect(computeSceneBoundsXZ([])).toBeNull()
51
- })
52
-
53
- test('returns null when no geometry is found on any node', () => {
54
- const barren = [
55
- {
56
- object: 'node',
57
- id: 'building_1',
58
- type: 'building',
59
- parentId: null,
60
- visible: true,
61
- metadata: {},
62
- children: [],
63
- } as unknown as AnyNode,
64
- ]
65
- expect(computeSceneBoundsXZ(barren)).toBeNull()
66
- })
67
-
68
- test('computes bounds from wall endpoints', () => {
69
- const nodes: AnyNode[] = [makeWall([0, 0], [4, 0]), makeWall([4, 0], [4, 3])]
70
- const bounds = computeSceneBoundsXZ(nodes)
71
- expect(bounds).not.toBeNull()
72
- expect(bounds!.min).toEqual([0, 0])
73
- expect(bounds!.max).toEqual([4, 3])
74
- expect(bounds!.size).toEqual([4, 3])
75
- expect(bounds!.center).toEqual([2, 1.5])
76
- })
77
-
78
- test('includes zone polygons', () => {
79
- const nodes: AnyNode[] = [
80
- makeZone([
81
- [-10, -5],
82
- [10, -5],
83
- [10, 5],
84
- [-10, 5],
85
- ]),
86
- ]
87
- const bounds = computeSceneBoundsXZ(nodes)
88
- expect(bounds).not.toBeNull()
89
- expect(bounds!.min).toEqual([-10, -5])
90
- expect(bounds!.max).toEqual([10, 5])
91
- expect(bounds!.size).toEqual([20, 10])
92
- })
93
-
94
- test('ignores the default 30×30 site bootstrap polygon', () => {
95
- const nodes: AnyNode[] = [
96
- makeSite([
97
- [-15, -15],
98
- [15, -15],
99
- [15, 15],
100
- [-15, 15],
101
- ]),
102
- makeWall([1, 1], [2, 2]),
103
- ]
104
- const bounds = computeSceneBoundsXZ(nodes)
105
- expect(bounds).not.toBeNull()
106
- // Only the wall should count — the default site polygon is skipped.
107
- expect(bounds!.min).toEqual([1, 1])
108
- expect(bounds!.max).toEqual([2, 2])
109
- })
110
-
111
- test('honours a non-default site polygon', () => {
112
- const nodes: AnyNode[] = [
113
- makeSite([
114
- [-25, -20],
115
- [25, -20],
116
- [25, 20],
117
- [-25, 20],
118
- ]),
119
- ]
120
- const bounds = computeSceneBoundsXZ(nodes)
121
- expect(bounds).not.toBeNull()
122
- expect(bounds!.min).toEqual([-25, -20])
123
- expect(bounds!.max).toEqual([25, 20])
124
- })
125
-
126
- test('combines walls, zones and positions across the flat dict', () => {
127
- const nodes: Record<string, AnyNode> = {
128
- wallA: makeWall([-8, -3], [4, -3]),
129
- wallB: makeWall([4, -3], [4, 6]),
130
- zoneA: makeZone([
131
- [-8, -3],
132
- [4, -3],
133
- [4, 6],
134
- [-8, 6],
135
- ]),
136
- item1: {
137
- object: 'node',
138
- id: 'item_1',
139
- type: 'item',
140
- parentId: null,
141
- visible: true,
142
- metadata: {},
143
- position: [7, 0, 8],
144
- rotation: [0, 0, 0],
145
- scale: [1, 1, 1],
146
- children: [],
147
- asset: {
148
- id: 'a',
149
- category: 'furniture',
150
- name: 'Chair',
151
- thumbnail: '',
152
- src: '',
153
- dimensions: [1, 1, 1],
154
- offset: [0, 0, 0],
155
- rotation: [0, 0, 0],
156
- scale: [1, 1, 1],
157
- },
158
- } as unknown as AnyNode,
159
- }
160
- const bounds = computeSceneBoundsXZ(nodes)
161
- expect(bounds).not.toBeNull()
162
- expect(bounds!.min).toEqual([-8, -3])
163
- expect(bounds!.max).toEqual([7, 8])
164
- })
165
-
166
- test('handles a single degenerate point with a minimum extent', () => {
167
- const nodes: AnyNode[] = [makeWall([2, 2], [2, 2])]
168
- const bounds = computeSceneBoundsXZ(nodes)
169
- expect(bounds).not.toBeNull()
170
- expect(bounds!.size[0]).toBeGreaterThan(0)
171
- expect(bounds!.size[1]).toBeGreaterThan(0)
172
- expect(bounds!.center).toEqual([2, 2])
173
- })
174
-
175
- describe('ancestor transforms', () => {
176
- function makeBuilding(
177
- id: string,
178
- position: [number, number, number],
179
- rotationY = 0,
180
- parentId: string | null = null,
181
- ): AnyNode {
182
- return {
183
- object: 'node',
184
- id,
185
- type: 'building',
186
- parentId,
187
- visible: true,
188
- metadata: {},
189
- children: [],
190
- position,
191
- rotation: [0, rotationY, 0],
192
- } as unknown as AnyNode
193
- }
194
-
195
- function makeLevel(id: string, parentId: string): AnyNode {
196
- return {
197
- object: 'node',
198
- id,
199
- type: 'level',
200
- parentId,
201
- visible: true,
202
- metadata: {},
203
- children: [],
204
- level: 0,
205
- } as unknown as AnyNode
206
- }
207
-
208
- function child(node: AnyNode, parentId: string): AnyNode {
209
- return { ...node, parentId } as AnyNode
210
- }
211
-
212
- function expectClose(actual: [number, number], expected: [number, number]) {
213
- expect(actual[0]).toBeCloseTo(expected[0], 9)
214
- expect(actual[1]).toBeCloseTo(expected[1], 9)
215
- }
216
-
217
- test('carries building-local walls and zones through a translated building', () => {
218
- const nodes = [
219
- makeBuilding('building_a', [100, 0, 100]),
220
- makeLevel('level_a', 'building_a'),
221
- child(makeWall([-5, -4], [5, -4]), 'level_a'),
222
- child(
223
- makeZone([
224
- [-5, -4],
225
- [5, -4],
226
- [5, 4],
227
- [-5, 4],
228
- ]),
229
- 'level_a',
230
- ),
231
- ]
232
- const bounds = computeSceneBoundsXZ(nodes)!
233
- expectClose(bounds.min, [95, 96])
234
- expectClose(bounds.max, [105, 104])
235
- expectClose(bounds.center, [100, 100])
236
- })
237
-
238
- test('turns building-local geometry by the building yaw', () => {
239
- // three.js Y rotation by +90°: local (x, z) → (z, −x).
240
- const nodes = [
241
- makeBuilding('building_r', [10, 0, 0], Math.PI / 2),
242
- makeLevel('level_r', 'building_r'),
243
- child(makeWall([0, 0], [8, 0]), 'level_r'),
244
- child(makeWall([8, 0], [8, 2]), 'level_r'),
245
- ]
246
- const bounds = computeSceneBoundsXZ(nodes)!
247
- expectClose(bounds.min, [10, -8])
248
- expectClose(bounds.max, [12, 0])
249
- expectClose(bounds.size, [2, 8])
250
- })
251
-
252
- test('composes nested positioned and rotated ancestors', () => {
253
- const item = {
254
- object: 'node',
255
- id: 'item_host',
256
- type: 'item',
257
- parentId: 'level_n',
258
- visible: true,
259
- metadata: {},
260
- children: [],
261
- position: [2, 0, 0],
262
- rotation: [0, Math.PI / 2, 0],
263
- } as unknown as AnyNode
264
- const hosted = {
265
- object: 'node',
266
- id: 'item_hosted',
267
- type: 'item',
268
- parentId: 'item_host',
269
- visible: true,
270
- metadata: {},
271
- children: [],
272
- position: [1, 0, 0],
273
- rotation: [0, 0, 0],
274
- } as unknown as AnyNode
275
- // Building at (50, 50) turned 90°: the host lands at (50, 48), and the
276
- // hosted item (1, 0) under a total 180° yaw lands one metre further −x.
277
- const nodes = [
278
- makeBuilding('building_n', [50, 0, 50], Math.PI / 2),
279
- makeLevel('level_n', 'building_n'),
280
- item,
281
- hosted,
282
- ]
283
- const bounds = computeSceneBoundsXZ(nodes)!
284
- expectClose(bounds.min, [49, 48])
285
- expectClose(bounds.max, [50, 50])
286
- })
287
-
288
- test('leaves geometry under an untransformed building unchanged', () => {
289
- const nodes = [
290
- makeBuilding('building_0', [0, 0, 0]),
291
- makeLevel('level_0', 'building_0'),
292
- child(makeWall([0, 0], [4, 0]), 'level_0'),
293
- child(makeWall([4, 0], [4, 3]), 'level_0'),
294
- ]
295
- const bounds = computeSceneBoundsXZ(nodes)!
296
- expect(bounds.min).toEqual([0, 0])
297
- expect(bounds.max).toEqual([4, 3])
298
- })
299
- })
300
-
301
- test('a door on a wall is bounded by the wall, not its wall-local position', () => {
302
- const wall = makeWall([50, 50], [60, 50])
303
- const door = {
304
- object: 'node',
305
- id: 'door_1',
306
- type: 'door',
307
- parentId: wall.id,
308
- visible: true,
309
- metadata: {},
310
- position: [2, 1, 0],
311
- } as unknown as AnyNode
312
- const bounds = computeSceneBoundsXZ([wall, door])
313
- expect(bounds).not.toBeNull()
314
- expect(bounds!.min[0]).toBe(50)
315
- expect(bounds!.max[0]).toBe(60)
316
- expect(bounds!.center).toEqual([55, 50])
317
- })
318
-
319
- test('skips non-finite coordinates', () => {
320
- const nodes: AnyNode[] = [makeWall([Number.NaN, 0], [4, 2]), makeWall([0, 0], [1, 1])]
321
- const bounds = computeSceneBoundsXZ(nodes)
322
- expect(bounds).not.toBeNull()
323
- // NaN should be ignored; the usable points are (4,2), (0,0), (1,1).
324
- expect(bounds!.min).toEqual([0, 0])
325
- expect(bounds!.max).toEqual([4, 2])
326
- })
327
- })
@@ -1,233 +0,0 @@
1
- /**
2
- * Scene bounds in the X/Z plane.
3
- *
4
- * Used by the auto-frame hook to fit the camera onto a freshly loaded scene
5
- * (see `../hooks/use-auto-frame`). The hook subscribes to the core scene
6
- * store and, when `nodes` transitions from empty → non-empty, fires a
7
- * `camera-controls:fit-scene` event on the core event bus carrying the
8
- * computed bounds.
9
- *
10
- * This module contains no rendering code: it only walks the flat-dict node
11
- * tree and derives an axis-aligned bounding box on the XZ (plan) plane.
12
- */
13
-
14
- import { turnPlanPoint, yawOf } from '@pascal-app/core'
15
- import type { AnyNode } from '@pascal-app/core/schema'
16
-
17
- export type SceneBoundsXZ = {
18
- /** Min [x, z] in world units (meters). */
19
- min: [number, number]
20
- /** Max [x, z] in world units (meters). */
21
- max: [number, number]
22
- /** Center [x, z] = (min + max) / 2. */
23
- center: [number, number]
24
- /** Size [w, d] = max - min. */
25
- size: [number, number]
26
- }
27
-
28
- // A very small guard against degenerate bounds (e.g. a single wall of zero length).
29
- const MIN_BOUNDS_EXTENT = 0.0001
30
-
31
- /** A node's plan frame in world XZ: local (x, z) → origin + turnPlanPoint(x, z, yaw). */
32
- type PlanFrame = { x: number; z: number; yaw: number }
33
-
34
- const WORLD_FRAME: PlanFrame = { x: 0, z: 0, yaw: 0 }
35
-
36
- type BoundsAcc = { minX: number; minZ: number; maxX: number; maxZ: number; hasPoint: boolean }
37
-
38
- function extendPoint(acc: BoundsAcc, frame: PlanFrame, localX: unknown, localZ: unknown): void {
39
- if (typeof localX !== 'number' || typeof localZ !== 'number') return
40
- if (!(Number.isFinite(localX) && Number.isFinite(localZ))) return
41
- const [tx, tz] = frame.yaw === 0 ? [localX, localZ] : turnPlanPoint(localX, localZ, frame.yaw)
42
- const x = frame.x + tx
43
- const z = frame.z + tz
44
- if (x < acc.minX) acc.minX = x
45
- if (x > acc.maxX) acc.maxX = x
46
- if (z < acc.minZ) acc.minZ = z
47
- if (z > acc.maxZ) acc.maxZ = z
48
- acc.hasPoint = true
49
- }
50
-
51
- /**
52
- * Compute the axis-aligned XZ bounds of a scene.
53
- *
54
- * Walks every node and extracts 2D footprint points from the fields most
55
- * nodes carry:
56
- * - `start`/`end` → wall and fence endpoints in level coordinates.
57
- * - `polygon` → zone, slab, site-boundary polygons.
58
- * - `position` → building/item/door/window position; uses [x, z] only.
59
- *
60
- * Each point is in its parent's frame, so it is carried to world XZ through
61
- * the ancestors' `position` and `rotation` yaw (a moved or turned building).
62
- *
63
- * Site-node polygons are intentionally excluded when they are the default
64
- * 30×30 bootstrap polygon — otherwise a brand-new empty scene would frame
65
- * an empty square around the origin. We still include site polygons that
66
- * look intentional (> 4 points, or any point outside the ±15 m default).
67
- *
68
- * Returns `null` if no usable geometry was found.
69
- */
70
- export function computeSceneBoundsXZ(
71
- nodes: AnyNode[] | Record<string, AnyNode>,
72
- ): SceneBoundsXZ | null {
73
- const list: AnyNode[] = Array.isArray(nodes) ? nodes : Object.values(nodes)
74
- if (list.length === 0) return null
75
-
76
- const acc: BoundsAcc = {
77
- minX: Number.POSITIVE_INFINITY,
78
- minZ: Number.POSITIVE_INFINITY,
79
- maxX: Number.NEGATIVE_INFINITY,
80
- maxZ: Number.NEGATIVE_INFINITY,
81
- hasPoint: false,
82
- }
83
-
84
- const byId = new Map<string, AnyNode>()
85
- for (const node of list) {
86
- if (node && typeof node === 'object' && typeof node.id === 'string') byId.set(node.id, node)
87
- }
88
- const frameOf = createFrameResolver(byId)
89
-
90
- for (const node of list) {
91
- if (!node || typeof node !== 'object') continue
92
- const anyNode = node as unknown as Record<string, unknown>
93
- // A node's own fields live in its parent's frame (a building's walls are
94
- // building-local), so every point goes through the ancestors' transforms.
95
- const frame = frameOf(node.parentId ?? null)
96
-
97
- // Wall / fence endpoints in level coordinates.
98
- const start = anyNode.start as unknown
99
- const end = anyNode.end as unknown
100
- if (Array.isArray(start) && start.length >= 2) extendPoint(acc, frame, start[0], start[1])
101
- if (Array.isArray(end) && end.length >= 2) extendPoint(acc, frame, end[0], end[1])
102
-
103
- // Zone / slab polygons (and explicit polygon-shaped site boundaries).
104
- const polygon = anyNode.polygon as unknown
105
- if (Array.isArray(polygon)) {
106
- // Zones/slabs expose a plain array of [x,z] tuples. Site nodes nest the
107
- // points under `polygon.points` (a discriminated PropertyLineData shape).
108
- for (const point of polygon) {
109
- if (Array.isArray(point) && point.length >= 2) {
110
- extendPoint(acc, frame, point[0], point[1])
111
- }
112
- }
113
- } else if (
114
- polygon &&
115
- typeof polygon === 'object' &&
116
- Array.isArray((polygon as { points?: unknown }).points)
117
- ) {
118
- // Site nodes only: skip the default bootstrap square so a blank scene
119
- // isn't auto-framed around an empty ±15 m box. Include any other site
120
- // polygon (more than 4 points, or any coordinate beyond the default).
121
- const points = (polygon as { points: unknown[] }).points
122
- if (node.type === 'site' && isDefaultSitePolygon(points)) {
123
- // Skip — default bootstrap polygon.
124
- } else {
125
- for (const point of points) {
126
- if (Array.isArray(point) && point.length >= 2) {
127
- extendPoint(acc, frame, point[0], point[1])
128
- }
129
- }
130
- }
131
- }
132
-
133
- // Position on the XZ plane (3D position = [x, y, z]). A node hosted on a
134
- // wall (door, window, wall item) has a wall-local position, and the
135
- // wall's endpoints already bound it.
136
- const position = anyNode.position as unknown
137
- if (Array.isArray(position) && position.length >= 3 && !isWallHosted(node, byId)) {
138
- extendPoint(acc, frame, position[0], position[2])
139
- }
140
- }
141
-
142
- if (!acc.hasPoint) return null
143
-
144
- // Ensure a minimum extent so a single-point scene still yields a box.
145
- let minX = acc.minX
146
- let minZ = acc.minZ
147
- let maxX = acc.maxX
148
- let maxZ = acc.maxZ
149
- if (maxX - minX < MIN_BOUNDS_EXTENT) {
150
- const cx = (minX + maxX) / 2
151
- minX = cx - MIN_BOUNDS_EXTENT / 2
152
- maxX = cx + MIN_BOUNDS_EXTENT / 2
153
- }
154
- if (maxZ - minZ < MIN_BOUNDS_EXTENT) {
155
- const cz = (minZ + maxZ) / 2
156
- minZ = cz - MIN_BOUNDS_EXTENT / 2
157
- maxZ = cz + MIN_BOUNDS_EXTENT / 2
158
- }
159
-
160
- const centerX = (minX + maxX) / 2
161
- const centerZ = (minZ + maxZ) / 2
162
- return {
163
- min: [minX, minZ],
164
- max: [maxX, maxZ],
165
- center: [centerX, centerZ],
166
- size: [maxX - minX, maxZ - minZ],
167
- }
168
- }
169
-
170
- /**
171
- * World plan frame of each node, composed down the parent chain the way the
172
- * renderers nest groups: a node with a `position` (building, item) translates
173
- * its children and turns them by its `rotation` yaw; nodes without one (site,
174
- * level) pass their parent's frame through.
175
- */
176
- function createFrameResolver(byId: Map<string, AnyNode>): (id: string | null) => PlanFrame {
177
- const cache = new Map<string, PlanFrame>()
178
- const resolving = new Set<string>()
179
-
180
- const resolve = (id: string | null): PlanFrame => {
181
- if (!id) return WORLD_FRAME
182
- const cached = cache.get(id)
183
- if (cached) return cached
184
- const node = byId.get(id)
185
- if (!node || resolving.has(id)) return WORLD_FRAME
186
- resolving.add(id)
187
- const parent = resolve(node.parentId ?? null)
188
- resolving.delete(id)
189
-
190
- const anyNode = node as unknown as Record<string, unknown>
191
- const position = anyNode.position
192
- let frame = parent
193
- if (Array.isArray(position) && position.length >= 3) {
194
- const px = Number.isFinite(position[0]) ? (position[0] as number) : 0
195
- const pz = Number.isFinite(position[2]) ? (position[2] as number) : 0
196
- const [tx, tz] = turnPlanPoint(px, pz, parent.yaw)
197
- frame = { x: parent.x + tx, z: parent.z + tz, yaw: parent.yaw + yawOf(anyNode.rotation) }
198
- }
199
- cache.set(id, frame)
200
- return frame
201
- }
202
-
203
- return resolve
204
- }
205
-
206
- function isWallHosted(node: AnyNode, byId: Map<string, AnyNode>): boolean {
207
- const parent = node.parentId ? byId.get(node.parentId) : undefined
208
- if (!parent) return false
209
- const anyParent = parent as unknown as Record<string, unknown>
210
- return Array.isArray(anyParent.start) && Array.isArray(anyParent.end)
211
- }
212
-
213
- /**
214
- * Matches the `SiteNode` bootstrap polygon defined in
215
- * `packages/core/src/schema/nodes/site.ts` (a 30×30 square at the origin).
216
- * We ignore it so the default scene doesn't "auto-frame" onto an empty box.
217
- */
218
- function isDefaultSitePolygon(points: unknown[]): boolean {
219
- if (points.length !== 4) return false
220
- const expected: [number, number][] = [
221
- [-15, -15],
222
- [15, -15],
223
- [15, 15],
224
- [-15, 15],
225
- ]
226
- for (let i = 0; i < 4; i++) {
227
- const p = points[i]
228
- const e = expected[i]!
229
- if (!Array.isArray(p) || p.length < 2) return false
230
- if (p[0] !== e[0] || p[1] !== e[1]) return false
231
- }
232
- return true
233
- }
@@ -1,9 +0,0 @@
1
- // Keys typed into a field (the room-name combobox, any input, an open dialog)
2
- // belong to that field, never to a canvas gesture.
3
- const TYPING_TARGET =
4
- 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="combobox"], [role="textbox"], [role="dialog"], [role="alertdialog"]'
5
-
6
- export function isTypingTarget(target: EventTarget | null) {
7
- const element = target as { closest?: (selector: string) => unknown; isContentEditable?: boolean }
8
- return !!element && (element.isContentEditable === true || !!element.closest?.(TYPING_TARGET))
9
- }