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