@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,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
- }