@pascal-app/editor 0.5.1 → 0.7.0

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 (150) hide show
  1. package/package.json +12 -7
  2. package/src/components/editor/bottom-sheet.tsx +149 -0
  3. package/src/components/editor/custom-camera-controls.tsx +75 -7
  4. package/src/components/editor/editor-layout-mobile.tsx +264 -0
  5. package/src/components/editor/editor-layout-v2.tsx +29 -0
  6. package/src/components/editor/first-person/build-collider-world.ts +365 -0
  7. package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
  8. package/src/components/editor/first-person-controls.tsx +496 -143
  9. package/src/components/editor/floating-action-menu.tsx +281 -83
  10. package/src/components/editor/floating-building-action-menu.tsx +4 -3
  11. package/src/components/editor/floorplan-background-selection.ts +113 -0
  12. package/src/components/editor/floorplan-panel.tsx +10442 -3275
  13. package/src/components/editor/index.tsx +270 -20
  14. package/src/components/editor/node-action-menu.tsx +14 -1
  15. package/src/components/editor/selection-manager.tsx +766 -12
  16. package/src/components/editor/site-edge-labels.tsx +9 -3
  17. package/src/components/editor/thumbnail-generator.tsx +350 -157
  18. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  19. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  20. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  21. package/src/components/editor/wall-measurement-label.tsx +377 -58
  22. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  23. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  24. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  25. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
  26. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  27. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
  28. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  29. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  30. package/src/components/editor-2d/svg-paths.ts +119 -0
  31. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
  32. package/src/components/systems/roof/roof-edit-system.tsx +5 -5
  33. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  34. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
  35. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  36. package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
  37. package/src/components/tools/column/column-tool.tsx +97 -0
  38. package/src/components/tools/column/move-column-tool.tsx +105 -0
  39. package/src/components/tools/door/door-tool.tsx +19 -0
  40. package/src/components/tools/door/move-door-tool.tsx +38 -8
  41. package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
  42. package/src/components/tools/fence/fence-drafting.ts +27 -8
  43. package/src/components/tools/fence/fence-tool.tsx +159 -3
  44. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
  45. package/src/components/tools/fence/move-fence-tool.tsx +102 -27
  46. package/src/components/tools/item/move-tool.tsx +19 -1
  47. package/src/components/tools/item/placement-math.ts +44 -7
  48. package/src/components/tools/item/placement-strategies.ts +111 -33
  49. package/src/components/tools/item/placement-types.ts +7 -0
  50. package/src/components/tools/item/use-draft-node.ts +2 -0
  51. package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
  52. package/src/components/tools/roof/move-roof-tool.tsx +111 -43
  53. package/src/components/tools/shared/polygon-editor.tsx +244 -29
  54. package/src/components/tools/shared/segment-angle.ts +156 -0
  55. package/src/components/tools/slab/move-slab-tool.tsx +182 -0
  56. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  57. package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
  58. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  59. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  60. package/src/components/tools/stair/stair-tool.tsx +11 -3
  61. package/src/components/tools/tool-manager.tsx +30 -3
  62. package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
  63. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
  64. package/src/components/tools/wall/move-wall-tool.tsx +356 -0
  65. package/src/components/tools/wall/wall-drafting.ts +348 -17
  66. package/src/components/tools/wall/wall-tool.tsx +134 -2
  67. package/src/components/tools/window/move-window-tool.tsx +28 -0
  68. package/src/components/tools/window/window-tool.tsx +17 -0
  69. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  70. package/src/components/ui/action-menu/control-modes.tsx +37 -5
  71. package/src/components/ui/action-menu/index.tsx +91 -1
  72. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  73. package/src/components/ui/action-menu/view-toggles.tsx +424 -35
  74. package/src/components/ui/command-palette/editor-commands.tsx +27 -5
  75. package/src/components/ui/command-palette/index.tsx +0 -1
  76. package/src/components/ui/controls/material-picker.tsx +189 -169
  77. package/src/components/ui/controls/slider-control.tsx +88 -26
  78. package/src/components/ui/floating-level-selector.tsx +286 -55
  79. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  80. package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
  81. package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
  82. package/src/components/ui/level-duplicate-dialog.tsx +115 -0
  83. package/src/components/ui/panels/ceiling-panel.tsx +47 -27
  84. package/src/components/ui/panels/column-panel.tsx +715 -0
  85. package/src/components/ui/panels/door-panel.tsx +986 -294
  86. package/src/components/ui/panels/fence-panel.tsx +55 -12
  87. package/src/components/ui/panels/item-panel.tsx +5 -5
  88. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  89. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  90. package/src/components/ui/panels/node-display.ts +39 -0
  91. package/src/components/ui/panels/paint-panel.tsx +138 -0
  92. package/src/components/ui/panels/panel-manager.tsx +241 -30
  93. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  94. package/src/components/ui/panels/reference-panel.tsx +243 -9
  95. package/src/components/ui/panels/roof-panel.tsx +30 -62
  96. package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
  97. package/src/components/ui/panels/slab-panel.tsx +46 -24
  98. package/src/components/ui/panels/spawn-panel.tsx +155 -0
  99. package/src/components/ui/panels/stair-panel.tsx +117 -69
  100. package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
  101. package/src/components/ui/panels/wall-panel.tsx +71 -17
  102. package/src/components/ui/panels/window-panel.tsx +665 -146
  103. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
  106. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
  107. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  108. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
  109. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
  110. package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
  111. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
  112. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
  113. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  114. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
  115. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
  116. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
  117. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
  118. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
  119. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
  120. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
  121. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
  122. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  123. package/src/components/ui/viewer-toolbar.tsx +96 -2
  124. package/src/components/viewer-overlay.tsx +25 -19
  125. package/src/hooks/use-auto-frame.ts +45 -0
  126. package/src/hooks/use-contextual-tools.ts +14 -13
  127. package/src/hooks/use-keyboard.ts +67 -9
  128. package/src/hooks/use-mobile.ts +12 -12
  129. package/src/index.tsx +2 -1
  130. package/src/lib/door-interaction.ts +88 -0
  131. package/src/lib/floorplan/geometry.ts +263 -0
  132. package/src/lib/floorplan/index.ts +38 -0
  133. package/src/lib/floorplan/items.ts +179 -0
  134. package/src/lib/floorplan/selection-tool.ts +231 -0
  135. package/src/lib/floorplan/stairs.ts +478 -0
  136. package/src/lib/floorplan/types.ts +57 -0
  137. package/src/lib/floorplan/walls.ts +23 -0
  138. package/src/lib/guide-events.ts +10 -0
  139. package/src/lib/history.ts +20 -0
  140. package/src/lib/level-duplication.test.ts +72 -0
  141. package/src/lib/level-duplication.ts +153 -0
  142. package/src/lib/local-guide-image.ts +42 -0
  143. package/src/lib/material-paint.ts +284 -0
  144. package/src/lib/roof-duplication.ts +214 -0
  145. package/src/lib/scene-bounds.test.ts +183 -0
  146. package/src/lib/scene-bounds.ts +169 -0
  147. package/src/lib/sfx-player.ts +96 -13
  148. package/src/lib/stair-duplication.ts +126 -0
  149. package/src/lib/window-interaction.ts +86 -0
  150. package/src/store/use-editor.tsx +279 -15
@@ -0,0 +1,179 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ getScaledDimensions,
5
+ type ItemNode,
6
+ type LevelNode,
7
+ useLiveTransforms,
8
+ } from '@pascal-app/core'
9
+ import { getRotatedRectanglePolygon, rotatePlanVector } from './geometry'
10
+ import type { FloorplanItemEntry, FloorplanNodeTransform, LevelDescendantMap } from './types'
11
+
12
+ export function collectLevelDescendants(
13
+ levelNode: LevelNode,
14
+ nodes: Record<string, AnyNode>,
15
+ ): AnyNode[] {
16
+ const descendants: AnyNode[] = []
17
+ const stack = [...levelNode.children].reverse() as AnyNodeId[]
18
+
19
+ while (stack.length > 0) {
20
+ const nodeId = stack.pop()
21
+ if (!nodeId) {
22
+ continue
23
+ }
24
+
25
+ const node = nodes[nodeId]
26
+ if (!node) {
27
+ continue
28
+ }
29
+
30
+ descendants.push(node)
31
+
32
+ if ('children' in node && Array.isArray(node.children) && node.children.length > 0) {
33
+ for (let index = node.children.length - 1; index >= 0; index -= 1) {
34
+ stack.push(node.children[index] as AnyNodeId)
35
+ }
36
+ }
37
+ }
38
+
39
+ return descendants
40
+ }
41
+
42
+ export function getItemFloorplanTransform(
43
+ item: ItemNode,
44
+ nodeById: LevelDescendantMap,
45
+ cache: Map<string, FloorplanNodeTransform | null>,
46
+ ): FloorplanNodeTransform | null {
47
+ const cached = cache.get(item.id)
48
+ if (cached !== undefined) {
49
+ return cached
50
+ }
51
+
52
+ const localRotation = item.rotation[1] ?? 0
53
+ let result: FloorplanNodeTransform | null = null
54
+ const itemMetadata =
55
+ typeof item.metadata === 'object' && item.metadata !== null && !Array.isArray(item.metadata)
56
+ ? (item.metadata as Record<string, unknown>)
57
+ : null
58
+
59
+ if (itemMetadata?.isTransient === true) {
60
+ const live = useLiveTransforms.getState().get(item.id)
61
+ if (live) {
62
+ result = {
63
+ position: {
64
+ x: live.position[0],
65
+ y: live.position[2],
66
+ },
67
+ rotation: live.rotation,
68
+ }
69
+
70
+ cache.set(item.id, result)
71
+ return result
72
+ }
73
+ }
74
+
75
+ if (item.parentId) {
76
+ const parentNode = nodeById.get(item.parentId as AnyNodeId)
77
+
78
+ if (parentNode?.type === 'wall') {
79
+ const wallRotation = -Math.atan2(
80
+ parentNode.end[1] - parentNode.start[1],
81
+ parentNode.end[0] - parentNode.start[0],
82
+ )
83
+ const wallLocalZ =
84
+ item.asset.attachTo === 'wall-side'
85
+ ? ((parentNode.thickness ?? 0.1) / 2) * (item.side === 'back' ? -1 : 1)
86
+ : item.position[2]
87
+ const [offsetX, offsetY] = rotatePlanVector(item.position[0], wallLocalZ, wallRotation)
88
+
89
+ result = {
90
+ position: {
91
+ x: parentNode.start[0] + offsetX,
92
+ y: parentNode.start[1] + offsetY,
93
+ },
94
+ rotation: wallRotation + localRotation,
95
+ }
96
+ } else if (parentNode?.type === 'item') {
97
+ const parentTransform = getItemFloorplanTransform(parentNode, nodeById, cache)
98
+ if (parentTransform) {
99
+ const [offsetX, offsetY] = rotatePlanVector(
100
+ item.position[0],
101
+ item.position[2],
102
+ parentTransform.rotation,
103
+ )
104
+ result = {
105
+ position: {
106
+ x: parentTransform.position.x + offsetX,
107
+ y: parentTransform.position.y + offsetY,
108
+ },
109
+ rotation: parentTransform.rotation + localRotation,
110
+ }
111
+ }
112
+ } else {
113
+ result = {
114
+ position: { x: item.position[0], y: item.position[2] },
115
+ rotation: localRotation,
116
+ }
117
+ }
118
+ } else {
119
+ result = {
120
+ position: { x: item.position[0], y: item.position[2] },
121
+ rotation: localRotation,
122
+ }
123
+ }
124
+
125
+ cache.set(item.id, result)
126
+ return result
127
+ }
128
+
129
+ export function buildFloorplanItemEntry(
130
+ item: ItemNode,
131
+ nodeById: LevelDescendantMap,
132
+ cache: Map<string, FloorplanNodeTransform | null>,
133
+ ): FloorplanItemEntry | null {
134
+ const transform = getItemFloorplanTransform(item, nodeById, cache)
135
+ if (!transform) {
136
+ return null
137
+ }
138
+
139
+ // Polygon is derived purely from `dimensions` — the same source of truth the
140
+ // editor uses for placement / collision. Previously we ran a per-frame
141
+ // convex-hull / minimum-area-rect pass over the loaded mesh's vertices to
142
+ // produce a tighter polygon, but that's expensive and disagrees with what
143
+ // the user sees on the 3D side (their dimensions are intentionally the
144
+ // bounding box, sometimes hand-tuned).
145
+ const dimensionPolygon = getItemDimensionPolygon(item, transform)
146
+ const [width, , depth] = getScaledDimensions(item)
147
+
148
+ return {
149
+ dimensionPolygon,
150
+ item,
151
+ polygon: dimensionPolygon,
152
+ usesRealMesh: false,
153
+ center: transform.position,
154
+ rotation: transform.rotation,
155
+ width,
156
+ depth,
157
+ }
158
+ }
159
+
160
+ type Point = {
161
+ x: number
162
+ y: number
163
+ }
164
+
165
+ function getItemDimensionPolygon(item: ItemNode, transform: FloorplanNodeTransform): Point[] {
166
+ const [width, , depth] = getScaledDimensions(item)
167
+ const centerLocalZ = item.asset.attachTo === 'wall-side' ? -depth / 2 : 0
168
+ const [offsetX, offsetY] = rotatePlanVector(0, centerLocalZ, transform.rotation)
169
+
170
+ return getRotatedRectanglePolygon(
171
+ {
172
+ x: transform.position.x + offsetX,
173
+ y: transform.position.y + offsetY,
174
+ },
175
+ width,
176
+ depth,
177
+ transform.rotation,
178
+ )
179
+ }
@@ -0,0 +1,231 @@
1
+ import type {
2
+ CeilingNode,
3
+ DoorNode,
4
+ ItemNode,
5
+ Point2D,
6
+ RoofNode,
7
+ RoofSegmentNode,
8
+ SlabNode,
9
+ StairNode,
10
+ WallNode,
11
+ WindowNode,
12
+ } from '@pascal-app/core'
13
+ import {
14
+ doesPolygonIntersectSelectionBounds,
15
+ getDistanceToWallSegment,
16
+ isPointInsidePolygon,
17
+ isPointInsidePolygonWithHoles,
18
+ } from './geometry'
19
+ import type { FloorplanSelectionBounds } from './types'
20
+
21
+ type OpeningNode = WindowNode | DoorNode
22
+
23
+ type OpeningPolygonEntry = {
24
+ opening: OpeningNode
25
+ polygon: Point2D[]
26
+ }
27
+
28
+ type ItemEntry = {
29
+ item: ItemNode
30
+ polygon: Point2D[]
31
+ }
32
+
33
+ type StairEntry = {
34
+ hitPolygons: Point2D[][]
35
+ stair: StairNode
36
+ segments: Array<{ polygon: Point2D[] }>
37
+ }
38
+
39
+ type WallEntry = {
40
+ wall: WallNode
41
+ polygon: Point2D[]
42
+ }
43
+
44
+ type SlabEntry = {
45
+ slab: SlabNode
46
+ polygon: Point2D[]
47
+ holes: Point2D[][]
48
+ }
49
+
50
+ type CeilingEntry = {
51
+ ceiling: CeilingNode
52
+ polygon: Point2D[]
53
+ holes: Point2D[][]
54
+ }
55
+
56
+ type RoofEntry = {
57
+ roof: RoofNode
58
+ segments: Array<{
59
+ polygon: Point2D[]
60
+ segment: RoofSegmentNode
61
+ }>
62
+ }
63
+
64
+ type FloorplanSelectionToolContext = {
65
+ point: Point2D
66
+ phase: 'site' | 'structure' | 'furnish'
67
+ isItemContextActive: boolean
68
+ items: ItemEntry[]
69
+ openings: OpeningPolygonEntry[]
70
+ stairs: StairEntry[]
71
+ walls: WallEntry[]
72
+ slabs: SlabEntry[]
73
+ ceilings: CeilingEntry[]
74
+ roofs: RoofEntry[]
75
+ openingHitTolerance: number
76
+ wallHitTolerance: number
77
+ getOpeningCenterLine: (polygon: Point2D[]) => { start: Point2D; end: Point2D } | null
78
+ }
79
+
80
+ function getItemHitId(context: FloorplanSelectionToolContext) {
81
+ if (!context.isItemContextActive) {
82
+ return null
83
+ }
84
+
85
+ const itemHit = context.items.find(({ polygon }) => isPointInsidePolygon(context.point, polygon))
86
+ return itemHit?.item.id ?? null
87
+ }
88
+
89
+ function getStairHitPolygons(stair: StairEntry) {
90
+ return stair.hitPolygons.length > 0
91
+ ? stair.hitPolygons
92
+ : stair.segments.map(({ polygon }) => polygon)
93
+ }
94
+
95
+ export function getFloorplanHitNodeId(context: FloorplanSelectionToolContext) {
96
+ if (context.phase === 'structure') {
97
+ const openingHit = context.openings.find(({ polygon }) => {
98
+ if (isPointInsidePolygon(context.point, polygon)) {
99
+ return true
100
+ }
101
+
102
+ const centerLine = context.getOpeningCenterLine(polygon)
103
+ if (!centerLine) {
104
+ return false
105
+ }
106
+
107
+ return (
108
+ getDistanceToWallSegment(
109
+ context.point,
110
+ [centerLine.start.x, centerLine.start.y],
111
+ [centerLine.end.x, centerLine.end.y],
112
+ ) <= context.openingHitTolerance
113
+ )
114
+ })
115
+ if (openingHit) {
116
+ return openingHit.opening.id
117
+ }
118
+
119
+ const stairHit = context.stairs.find((stair) =>
120
+ getStairHitPolygons(stair).some((polygon) => isPointInsidePolygon(context.point, polygon)),
121
+ )
122
+ if (stairHit) {
123
+ return stairHit.stair.id
124
+ }
125
+
126
+ const wallHit = context.walls.find(
127
+ ({ wall, polygon }) =>
128
+ isPointInsidePolygon(context.point, polygon) ||
129
+ getDistanceToWallSegment(context.point, wall.start, wall.end) <= context.wallHitTolerance,
130
+ )
131
+ if (wallHit) {
132
+ return wallHit.wall.id
133
+ }
134
+
135
+ const roofHit = context.roofs.find(({ segments }) =>
136
+ segments.some(({ polygon }) => isPointInsidePolygon(context.point, polygon)),
137
+ )
138
+ if (roofHit) {
139
+ return roofHit.roof.id
140
+ }
141
+
142
+ const ceilingHit = context.ceilings.find(({ polygon, holes }) =>
143
+ isPointInsidePolygonWithHoles(context.point, polygon, holes),
144
+ )
145
+ if (ceilingHit) {
146
+ return ceilingHit.ceiling.id
147
+ }
148
+
149
+ const slabHit = context.slabs.find(({ polygon, holes }) =>
150
+ isPointInsidePolygonWithHoles(context.point, polygon, holes),
151
+ )
152
+ if (slabHit) {
153
+ return slabHit.slab.id
154
+ }
155
+ }
156
+
157
+ return getItemHitId(context)
158
+ }
159
+
160
+ type FloorplanSelectionBoundsContext = {
161
+ bounds: FloorplanSelectionBounds
162
+ phase: 'site' | 'structure' | 'furnish'
163
+ isItemContextActive: boolean
164
+ items: ItemEntry[]
165
+ walls: WallEntry[]
166
+ openings: OpeningPolygonEntry[]
167
+ slabs: SlabEntry[]
168
+ ceilings: CeilingEntry[]
169
+ stairs: StairEntry[]
170
+ roofs: RoofEntry[]
171
+ }
172
+
173
+ export function getFloorplanSelectionIdsInBounds({
174
+ bounds,
175
+ phase,
176
+ isItemContextActive,
177
+ items,
178
+ walls,
179
+ openings,
180
+ slabs,
181
+ ceilings,
182
+ stairs,
183
+ roofs,
184
+ }: FloorplanSelectionBoundsContext) {
185
+ const itemIds = isItemContextActive
186
+ ? items
187
+ .filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
188
+ .map(({ item }) => item.id)
189
+ : []
190
+
191
+ if (phase !== 'structure') {
192
+ return itemIds
193
+ }
194
+
195
+ const wallIds = walls
196
+ .filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
197
+ .map(({ wall }) => wall.id)
198
+ const openingIds = openings
199
+ .filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
200
+ .map(({ opening }) => opening.id)
201
+ const slabIds = slabs
202
+ .filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
203
+ .map(({ slab }) => slab.id)
204
+ const ceilingIds = ceilings
205
+ .filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
206
+ .map(({ ceiling }) => ceiling.id)
207
+ const stairIds = stairs
208
+ .filter((stair) =>
209
+ getStairHitPolygons(stair).some((polygon) =>
210
+ doesPolygonIntersectSelectionBounds(polygon, bounds),
211
+ ),
212
+ )
213
+ .map(({ stair }) => stair.id)
214
+ const roofIds = roofs
215
+ .filter(({ segments }) =>
216
+ segments.some(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds)),
217
+ )
218
+ .map(({ roof }) => roof.id)
219
+
220
+ return Array.from(
221
+ new Set([
222
+ ...itemIds,
223
+ ...wallIds,
224
+ ...openingIds,
225
+ ...slabIds,
226
+ ...ceilingIds,
227
+ ...stairIds,
228
+ ...roofIds,
229
+ ]),
230
+ )
231
+ }