@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,417 +0,0 @@
1
- import {
2
- type AnyNode,
3
- area,
4
- type BoundaryNode,
5
- type BoundarySpan,
6
- containsPoint,
7
- createRoomTopologyIndex,
8
- getWallArcData,
9
- getWallFaceOffsets,
10
- type MultiPolygon,
11
- roomClearPolygon,
12
- type TopologyRoom,
13
- type WallNode,
14
- type ZoneNode,
15
- } from '@pascal-app/core'
16
- import type { SelectionModifierKeys } from './selection-routing'
17
- import { polygonMatchesZoneFootprint } from './zone-content'
18
-
19
- export type RoomKey = { levelId: string; zoneId: string }
20
-
21
- export function sameRoom(a: RoomKey | null, b: RoomKey | null): boolean {
22
- return a === b || (!!a && !!b && a.levelId === b.levelId && a.zoneId === b.zoneId)
23
- }
24
-
25
- export function shouldSelectRoom(
26
- selected: RoomKey | null,
27
- hit: RoomKey | null,
28
- modifiers: SelectionModifierKeys,
29
- ): boolean {
30
- return (
31
- !!hit &&
32
- !sameRoom(selected, hit) &&
33
- !(modifiers.alt || modifiers.shift || modifiers.ctrl || modifiers.meta)
34
- )
35
- }
36
-
37
- export type RoomSelectionGeometry = TopologyRoom & {
38
- key: RoomKey
39
- boundaryWallIds: string[]
40
- clearPolygon: ReturnType<typeof roomClearPolygon>
41
- area: number
42
- hitPolygon: MultiPolygon
43
- /** A stacked open-below room (`floor.support: 'open'`) inside its host room. */
44
- mezzanine: { hostZoneId: string } | null
45
- }
46
-
47
- export type RoomSelectionRecord = RoomSelectionGeometry & {
48
- geometry: RoomSelectionGeometry
49
- name: string
50
- zoneId: string
51
- slabId: string | null
52
- slabName: string | null
53
- ceilingId: string | null
54
- ceilingName: string | null
55
- }
56
-
57
- type Nodes = Record<string, AnyNode>
58
-
59
- function describeRoomGeometry(
60
- levelId: string,
61
- room: TopologyRoom,
62
- zone: ZoneNode,
63
- ): RoomSelectionGeometry {
64
- const clearPolygon = roomClearPolygon(room)
65
- return {
66
- ...room,
67
- key: { levelId, zoneId: zone.id },
68
- boundaryWallIds: [
69
- ...new Set(room.spans.filter((span) => span.kind === 'wall').map((span) => span.boundaryId)),
70
- ],
71
- clearPolygon,
72
- area: area([{ outer: zone.polygon, holes: zone.holes }]),
73
- hitPolygon: [{ outer: room.polygon, holes: room.holes }],
74
- mezzanine: null,
75
- }
76
- }
77
-
78
- export function isMezzanineZone(node: AnyNode | undefined): node is ZoneNode {
79
- return node?.type === 'zone' && node.floor?.support === 'open'
80
- }
81
-
82
- /**
83
- * A mezzanine as a selectable room. It has no walls of its own: its outline is
84
- * the zone's polygon, its floor the zone's own plate, and it bounds nothing.
85
- */
86
- function describeMezzanineGeometry(
87
- levelId: string,
88
- zone: ZoneNode,
89
- context: TopologyRoom['context'],
90
- ): RoomSelectionGeometry {
91
- const polygon = zone.polygon.map(([x, z]) => [x, z] as [number, number])
92
- const holes = (zone.holes ?? []).map((ring) => ring.map(([x, z]) => [x, z] as [number, number]))
93
- const outline = [{ outer: polygon, holes }]
94
- return {
95
- id: `mezzanine:${zone.id}`,
96
- polygon,
97
- holes,
98
- spans: [],
99
- context,
100
- key: { levelId, zoneId: zone.id },
101
- boundaryWallIds: [],
102
- clearPolygon: outline,
103
- area: area(outline),
104
- hitPolygon: outline,
105
- mezzanine: { hostZoneId: zone.hostZoneId ?? '' },
106
- }
107
- }
108
-
109
- const NO_WALLS: TopologyRoom['context'] = {
110
- revision: -1,
111
- walls: new Map(),
112
- wallFootprints: new Map(),
113
- }
114
-
115
- /**
116
- * A zone that bounds no room (a drawn area), outlined the way a room is: its
117
- * polygon as the floor, no walls of its own.
118
- */
119
- export function describeZoneOutline(levelId: string, zone: ZoneNode): RoomSelectionRecord {
120
- const geometry: RoomSelectionGeometry = {
121
- ...describeMezzanineGeometry(levelId, zone, NO_WALLS),
122
- id: `zone:${zone.id}`,
123
- mezzanine: null,
124
- }
125
- return {
126
- ...geometry,
127
- geometry,
128
- name: zone.name || 'Zone',
129
- zoneId: zone.id,
130
- slabId: null,
131
- slabName: null,
132
- ceilingId: null,
133
- ceilingName: null,
134
- }
135
- }
136
-
137
- function describeRoom(
138
- geometry: RoomSelectionGeometry,
139
- candidates: readonly AnyNode[],
140
- ): RoomSelectionRecord {
141
- const { clearPolygon } = geometry
142
- const matches = (node: AnyNode) =>
143
- (node.type === 'zone' || node.type === 'slab' || node.type === 'ceiling') &&
144
- (polygonMatchesZoneFootprint(node.polygon, geometry.polygon) ||
145
- clearPolygon.some(({ outer }) => polygonMatchesZoneFootprint(node.polygon, outer)))
146
- const slab = geometry.mezzanine
147
- ? candidates.find(
148
- (node) =>
149
- node.type === 'slab' &&
150
- node.support === 'open' &&
151
- !!node.zoneIds?.includes(geometry.key.zoneId),
152
- )
153
- : candidates.find(
154
- (node) =>
155
- node.type === 'slab' && node.support !== 'open' && node.autoFromWalls && matches(node),
156
- )
157
- const ceiling = candidates.find(
158
- (node) => node.type === 'ceiling' && node.zoneId === geometry.key.zoneId,
159
- )
160
- const zone = candidates.find(
161
- (node): node is ZoneNode => node.type === 'zone' && node.id === geometry.key.zoneId,
162
- )!
163
- return {
164
- ...geometry,
165
- geometry,
166
- name: zone.name || 'Room',
167
- area: area([{ outer: zone.polygon, holes: zone.holes }]),
168
- zoneId: zone.id,
169
- slabId: slab?.id ?? null,
170
- slabName: slab ? slab.name || 'Slab' : null,
171
- ceilingId: ceiling?.id ?? null,
172
- ceilingName: ceiling ? ceiling.name || 'Ceiling' : null,
173
- }
174
- }
175
-
176
- type RoomHitIndex = Pick<
177
- ReturnType<typeof createRoomTopologyIndex>,
178
- 'roomAtPoint' | 'roomForWallHit' | 'spansForWall'
179
- > & {
180
- /** The selectable room a zone is (a mezzanine's own plate or ceiling points at it). */
181
- roomForZone?: (levelId: string, zoneId: string) => TopologyRoom | null
182
- }
183
- const wallFrames = new WeakMap<WallNode, ReturnType<typeof wallHitFrame>>()
184
-
185
- function wallHitFrame(wall: WallNode) {
186
- const dx = wall.end[0] - wall.start[0]
187
- const dz = wall.end[1] - wall.start[1]
188
- const length = Math.hypot(dx, dz)
189
- const offsets = getWallFaceOffsets(wall)
190
- return {
191
- dx,
192
- dz,
193
- lengthSq: length * length,
194
- nx: -dz / length,
195
- nz: dx / length,
196
- arc: getWallArcData(wall),
197
- center: (offsets.a + offsets.b) / 2,
198
- }
199
- }
200
-
201
- export function resolveRoomHit<T extends RoomHitIndex>(
202
- index: T,
203
- levelId: string,
204
- node: AnyNode | null,
205
- point: [number, number],
206
- view?: '2d' | '3d',
207
- ): ReturnType<T['roomAtPoint']>
208
- export function resolveRoomHit(
209
- index: RoomHitIndex,
210
- levelId: string,
211
- node: AnyNode | null,
212
- point: [number, number],
213
- view: '2d' | '3d' = '2d',
214
- ): TopologyRoom | null {
215
- // A mezzanine's plate (railing included) and ceiling name their own room; the
216
- // host floor under it and every other surface resolve by the point, which
217
- // never lands in a mezzanine.
218
- if (node?.type === 'slab' && node.support === 'open')
219
- return index.roomForZone?.(levelId, node.zoneIds?.[0] ?? '') ?? null
220
- if (node?.type === 'ceiling' && node.zoneId) {
221
- const own = index.roomForZone?.(levelId, node.zoneId)
222
- if (own) return own
223
- }
224
- if (!node || node.type === 'slab' || node.type === 'ceiling')
225
- return index.roomAtPoint(levelId, point)
226
- if (node.type !== 'wall') return null
227
- let frame = wallFrames.get(node)
228
- if (!frame) {
229
- frame = wallHitFrame(node)
230
- wallFrames.set(node, frame)
231
- }
232
- const { dx, dz, lengthSq, arc } = frame
233
- if (!lengthSq) return null
234
- const station = Math.max(
235
- 0,
236
- Math.min(1, ((point[0] - node.start[0]) * dx + (point[1] - node.start[1]) * dz) / lengthSq),
237
- )
238
- let x = node.start[0] + station * dx
239
- let z = node.start[1] + station * dz
240
- let nx = frame.nx
241
- let nz = frame.nz
242
- if (arc) {
243
- const angle = Math.atan2(point[1] - arc.center.y, point[0] - arc.center.x)
244
- const delta = Math.atan2(Math.sin(angle - arc.startAngle), Math.cos(angle - arc.startAngle))
245
- const t = Math.max(0, Math.min(1, delta / arc.delta))
246
- const theta = arc.startAngle + arc.delta * t
247
- const cos = Math.cos(theta)
248
- const sin = Math.sin(theta)
249
- x = arc.center.x + cos * arc.radius
250
- z = arc.center.y + sin * arc.radius
251
- nx = -cos * arc.direction
252
- nz = -sin * arc.direction
253
- }
254
- const localZ = (point[0] - x) * nx + (point[1] - z) * nz
255
- // Justified bodies can lie entirely on one side of the reference line.
256
- const center = view === '3d' ? frame.center : 0
257
- const face = localZ >= center ? 'a' : 'b'
258
- // Room first: a face with no room behind it (the exterior) still resolves
259
- // to the room on the other face, or to the wall's only room; only walls
260
- // bounding no room reach the wall itself.
261
- return (
262
- index.roomForWallHit(levelId, node.id, face, station) ??
263
- index.roomForWallHit(levelId, node.id, face === 'a' ? 'b' : 'a', station) ??
264
- soleWallRoom(index, levelId, node.id)
265
- )
266
- }
267
-
268
- function soleWallRoom(index: RoomHitIndex, levelId: string, wallId: string) {
269
- let sole: BoundarySpan | null = null
270
- for (const span of index.spansForWall(levelId, wallId)) {
271
- if (sole && sole.roomId !== span.roomId) return null
272
- sole ??= span
273
- }
274
- return sole && index.roomForWallHit(levelId, wallId, sole.face, (sole.t0 + sole.t1) / 2)
275
- }
276
-
277
- function isBoundary(node: AnyNode): node is BoundaryNode {
278
- return node.type === 'wall' || node.type === 'separator'
279
- }
280
-
281
- function sameBoundary(a: BoundaryNode, b: AnyNode | undefined) {
282
- return (
283
- !!b &&
284
- isBoundary(b) &&
285
- a.type === b.type &&
286
- a.parentId === b.parentId &&
287
- a.start[0] === b.start[0] &&
288
- a.start[1] === b.start[1] &&
289
- a.end[0] === b.end[0] &&
290
- a.end[1] === b.end[1] &&
291
- (a.type === 'separator' ||
292
- (b.type === 'wall' &&
293
- a.curveOffset === b.curveOffset &&
294
- a.thickness === b.thickness &&
295
- a.justification === b.justification))
296
- )
297
- }
298
-
299
- export class RoomSelectionIndex {
300
- readonly topology = createRoomTopologyIndex()
301
- private nodes: Nodes | null = null
302
- private records: RoomSelectionRecord[] = []
303
- private geometry: RoomSelectionGeometry[] = []
304
- private candidates: AnyNode[] = []
305
- private revision = -1
306
- private readonly roomsById = new Map<string, RoomSelectionGeometry>()
307
-
308
- roomAtPoint(_levelId: string, point: [number, number]): RoomSelectionGeometry | null {
309
- for (const room of this.geometry)
310
- if (!room.mezzanine && containsPoint(room.hitPolygon, point)) return room
311
- return null
312
- }
313
-
314
- roomForZone(_levelId: string, zoneId: string): RoomSelectionGeometry | null {
315
- return this.geometry.find((room) => room.key.zoneId === zoneId) ?? null
316
- }
317
-
318
- roomForWallHit(
319
- levelId: string,
320
- wallId: string,
321
- face: 'a' | 'b',
322
- t: number,
323
- ): RoomSelectionGeometry | null {
324
- const spans = this.topology.getLevelTopology(levelId)?.spansByBoundary.get(wallId)
325
- if (!spans) return null
326
- for (const span of spans) {
327
- if (span.face === face && t >= span.t0 && (t < span.t1 || (t === 1 && span.t1 === 1)))
328
- return this.roomsById.get(span.roomId) ?? null
329
- }
330
- return null
331
- }
332
-
333
- spansForWall(levelId: string, wallId: string) {
334
- return this.topology.spansForWall(levelId, wallId)
335
- }
336
-
337
- constructor(readonly levelId: string) {}
338
-
339
- update(nodes: Nodes): RoomSelectionRecord[] {
340
- if (nodes === this.nodes) return this.records
341
- if (!this.nodes) this.topology.rebuildLevel(this.levelId, nodes)
342
- else {
343
- const changed = new Set<string>()
344
- for (const snapshot of [this.nodes, nodes]) {
345
- for (const node of Object.values(snapshot)) {
346
- if (
347
- isBoundary(node) &&
348
- node.parentId === this.levelId &&
349
- !sameBoundary(node, snapshot === nodes ? this.nodes[node.id] : nodes[node.id])
350
- )
351
- changed.add(node.id)
352
- }
353
- }
354
- if (changed.size) this.topology.applyWallDelta(this.levelId, changed, this.nodes, nodes)
355
- }
356
- this.nodes = nodes
357
- const topology = this.topology.getLevelTopology(this.levelId)
358
- const geometryChanged = topology?.revision !== this.revision
359
- const candidates = Object.values(nodes).filter(
360
- (node) =>
361
- node.parentId === this.levelId &&
362
- (node.type === 'slab' || node.type === 'ceiling' || node.type === 'zone'),
363
- )
364
- const candidatesChanged =
365
- candidates.length !== this.candidates.length ||
366
- candidates.some((node, i) => node !== this.candidates[i])
367
- if (!geometryChanged && !candidatesChanged) return this.records
368
- if (geometryChanged || candidatesChanged) {
369
- this.revision = topology?.revision ?? -1
370
- const previous = new Map(this.geometry.map((room) => [room.key.zoneId, room]))
371
- const matched = new Set<string>()
372
- const zones = candidates
373
- .filter(
374
- (node): node is ZoneNode =>
375
- node.type === 'zone' &&
376
- node.spaceRole === 'room' &&
377
- node.enclosureStatus !== 'open' &&
378
- !isMezzanineZone(node),
379
- )
380
- .sort((a, b) => a.id.localeCompare(b.id))
381
- this.geometry = (topology?.rooms ?? []).flatMap((room) => {
382
- const boundaryIds = [...new Set(room.spans.map((span) => span.boundaryId))].sort().join('|')
383
- const available = zones.filter((zone) => !matched.has(zone.id))
384
- const zone =
385
- available.find(
386
- (node) =>
387
- [...node.boundaryWallIds, ...node.boundarySeparatorIds].sort().join('|') ===
388
- boundaryIds,
389
- ) ??
390
- available.find(
391
- (node) =>
392
- !!node.seed && containsPoint([{ outer: room.polygon, holes: room.holes }], node.seed),
393
- )
394
- if (!zone) return []
395
- matched.add(zone.id)
396
- return [
397
- (!geometryChanged && previous.get(zone.id)) ||
398
- describeRoomGeometry(this.levelId, room, zone),
399
- ]
400
- })
401
- const context = topology?.rooms[0]?.context
402
- if (context)
403
- for (const zone of candidates) {
404
- if (!isMezzanineZone(zone) || zone.spaceRole !== 'room') continue
405
- const hosted =
406
- zone.hostZoneId && this.geometry.some((room) => room.key.zoneId === zone.hostZoneId)
407
- if (!hosted) continue
408
- this.geometry.push(describeMezzanineGeometry(this.levelId, zone, context))
409
- }
410
- this.roomsById.clear()
411
- for (const room of this.geometry) this.roomsById.set(room.id, room)
412
- }
413
- this.candidates = candidates
414
- this.records = this.geometry.map((room) => describeRoom(room, candidates))
415
- return this.records
416
- }
417
- }