@pascal-app/editor 0.6.0 → 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 (122) hide show
  1. package/package.json +9 -5
  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 +20 -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 +32 -55
  10. package/src/components/editor/floorplan-background-selection.ts +113 -0
  11. package/src/components/editor/floorplan-panel.tsx +9855 -3298
  12. package/src/components/editor/index.tsx +269 -21
  13. package/src/components/editor/selection-manager.tsx +575 -13
  14. package/src/components/editor/thumbnail-generator.tsx +38 -7
  15. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  16. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  17. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  18. package/src/components/editor/wall-measurement-label.tsx +267 -36
  19. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  20. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  21. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  22. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
  23. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  24. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
  25. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  26. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  27. package/src/components/editor-2d/svg-paths.ts +119 -0
  28. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  29. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  30. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  31. package/src/components/tools/column/column-tool.tsx +97 -0
  32. package/src/components/tools/column/move-column-tool.tsx +105 -0
  33. package/src/components/tools/door/door-tool.tsx +7 -0
  34. package/src/components/tools/door/move-door-tool.tsx +28 -8
  35. package/src/components/tools/fence/fence-drafting.ts +10 -3
  36. package/src/components/tools/fence/fence-tool.tsx +159 -3
  37. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
  38. package/src/components/tools/fence/move-fence-tool.tsx +101 -34
  39. package/src/components/tools/item/move-tool.tsx +10 -1
  40. package/src/components/tools/item/placement-math.ts +30 -1
  41. package/src/components/tools/item/placement-strategies.ts +109 -31
  42. package/src/components/tools/item/placement-types.ts +7 -0
  43. package/src/components/tools/item/use-draft-node.ts +2 -0
  44. package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
  45. package/src/components/tools/roof/move-roof-tool.tsx +22 -15
  46. package/src/components/tools/shared/polygon-editor.tsx +153 -28
  47. package/src/components/tools/shared/segment-angle.ts +156 -0
  48. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  49. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  50. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  51. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  52. package/src/components/tools/tool-manager.tsx +18 -3
  53. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
  54. package/src/components/tools/wall/wall-drafting.ts +18 -9
  55. package/src/components/tools/wall/wall-tool.tsx +134 -2
  56. package/src/components/tools/window/move-window-tool.tsx +18 -0
  57. package/src/components/tools/window/window-tool.tsx +5 -0
  58. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  59. package/src/components/ui/action-menu/control-modes.tsx +28 -1
  60. package/src/components/ui/action-menu/index.tsx +91 -1
  61. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +424 -35
  63. package/src/components/ui/command-palette/editor-commands.tsx +18 -1
  64. package/src/components/ui/controls/material-picker.tsx +152 -165
  65. package/src/components/ui/controls/slider-control.tsx +66 -18
  66. package/src/components/ui/floating-level-selector.tsx +286 -55
  67. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  68. package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
  69. package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
  70. package/src/components/ui/level-duplicate-dialog.tsx +115 -0
  71. package/src/components/ui/panels/ceiling-panel.tsx +1 -25
  72. package/src/components/ui/panels/column-panel.tsx +715 -0
  73. package/src/components/ui/panels/door-panel.tsx +981 -289
  74. package/src/components/ui/panels/fence-panel.tsx +3 -45
  75. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  76. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  77. package/src/components/ui/panels/node-display.ts +39 -0
  78. package/src/components/ui/panels/paint-panel.tsx +138 -0
  79. package/src/components/ui/panels/panel-manager.tsx +210 -1
  80. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  81. package/src/components/ui/panels/reference-panel.tsx +238 -5
  82. package/src/components/ui/panels/roof-panel.tsx +4 -105
  83. package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
  84. package/src/components/ui/panels/slab-panel.tsx +4 -30
  85. package/src/components/ui/panels/spawn-panel.tsx +155 -0
  86. package/src/components/ui/panels/stair-panel.tsx +11 -117
  87. package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
  88. package/src/components/ui/panels/wall-panel.tsx +1 -95
  89. package/src/components/ui/panels/window-panel.tsx +660 -139
  90. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  91. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  92. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
  93. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  94. package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
  95. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
  96. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
  97. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
  98. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
  99. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  100. package/src/components/ui/viewer-toolbar.tsx +42 -1
  101. package/src/hooks/use-auto-frame.ts +45 -0
  102. package/src/hooks/use-keyboard.ts +64 -7
  103. package/src/hooks/use-mobile.ts +12 -12
  104. package/src/lib/door-interaction.ts +88 -0
  105. package/src/lib/floorplan/geometry.ts +263 -0
  106. package/src/lib/floorplan/index.ts +38 -0
  107. package/src/lib/floorplan/items.ts +179 -0
  108. package/src/lib/floorplan/selection-tool.ts +231 -0
  109. package/src/lib/floorplan/stairs.ts +478 -0
  110. package/src/lib/floorplan/types.ts +57 -0
  111. package/src/lib/floorplan/walls.ts +23 -0
  112. package/src/lib/guide-events.ts +10 -0
  113. package/src/lib/level-duplication.test.ts +72 -0
  114. package/src/lib/level-duplication.ts +153 -0
  115. package/src/lib/local-guide-image.ts +42 -0
  116. package/src/lib/material-paint.ts +284 -0
  117. package/src/lib/roof-duplication.ts +214 -0
  118. package/src/lib/scene-bounds.test.ts +183 -0
  119. package/src/lib/scene-bounds.ts +169 -0
  120. package/src/lib/stair-duplication.ts +126 -0
  121. package/src/lib/window-interaction.ts +86 -0
  122. package/src/store/use-editor.tsx +164 -8
@@ -265,18 +265,49 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
265
265
  )) as Uint8Array
266
266
 
267
267
  const actualBytesPerRow = width * 4
268
+ const tightTotal = actualBytesPerRow * height
268
269
  const paddedBytesPerRow = Math.ceil(actualBytesPerRow / 256) * 256
270
+ // Two readback shapes to handle:
271
+ // - WebGPU (`copyTextureToBuffer`): top-down + 256-byte row padding
272
+ // when width*4 isn't already a multiple of 256.
273
+ // - WebGL2 fallback (iOS Chrome, etc.): tightly-packed but bottom-up
274
+ // (OpenGL framebuffer convention).
275
+ // `isWebGPURenderer` lies — it stays true even when the renderer
276
+ // falls back to the WebGL backend. Inspect the actual backend
277
+ // instead (presence of a GPU device, or backend constructor name).
278
+ const backend = (renderer as any).backend
279
+ const isWebGPU =
280
+ !!backend?.device ||
281
+ backend?.isWebGPUBackend === true ||
282
+ backend?.constructor?.name === 'WebGPUBackend'
269
283
  let tightPixels: Uint8ClampedArray
270
- if (paddedBytesPerRow === actualBytesPerRow) {
271
- tightPixels = new Uint8ClampedArray(pixels.buffer, pixels.byteOffset, pixels.byteLength)
284
+ if (isWebGPU) {
285
+ // WebGPU: depad rows if needed; orientation is already top-down.
286
+ if (paddedBytesPerRow === actualBytesPerRow) {
287
+ tightPixels = new Uint8ClampedArray(
288
+ pixels.buffer,
289
+ pixels.byteOffset,
290
+ Math.min(pixels.byteLength, tightTotal),
291
+ )
292
+ } else {
293
+ tightPixels = new Uint8ClampedArray(tightTotal)
294
+ for (let row = 0; row < height; row++) {
295
+ tightPixels.set(
296
+ pixels.subarray(
297
+ row * paddedBytesPerRow,
298
+ row * paddedBytesPerRow + actualBytesPerRow,
299
+ ),
300
+ row * actualBytesPerRow,
301
+ )
302
+ }
303
+ }
272
304
  } else {
273
- tightPixels = new Uint8ClampedArray(width * height * 4)
305
+ // WebGL2: tight buffer in bottom-up order — flip rows.
306
+ tightPixels = new Uint8ClampedArray(tightTotal)
274
307
  for (let row = 0; row < height; row++) {
308
+ const srcStart = (height - 1 - row) * actualBytesPerRow
275
309
  tightPixels.set(
276
- pixels.subarray(
277
- row * paddedBytesPerRow,
278
- row * paddedBytesPerRow + actualBytesPerRow,
279
- ),
310
+ pixels.subarray(srcStart, srcStart + actualBytesPerRow),
280
311
  row * actualBytesPerRow,
281
312
  )
282
313
  }
@@ -0,0 +1,257 @@
1
+ 'use client'
2
+
3
+ import { emitter, type FenceNode, isCurvedWall, type WallNode } from '@pascal-app/core'
4
+ import { type MouseEvent as ReactMouseEvent, useCallback } from 'react'
5
+ import { getPlanPointDistance } from '../../lib/floorplan'
6
+ import { snapFenceDraftPoint } from '../tools/fence/fence-drafting'
7
+ import type { WallPlanPoint } from '../tools/wall/wall-drafting'
8
+
9
+ type UseFloorplanBackgroundPlacementArgs = {
10
+ activePolygonDraftPoints: WallPlanPoint[]
11
+ ceilingDraftPoints: WallPlanPoint[]
12
+ clearFencePlacementDraft: () => void
13
+ clearRoofPlacementDraft: () => void
14
+ emitFloorplanGridEvent: (
15
+ type: 'click' | 'double-click' | 'move',
16
+ planPoint: WallPlanPoint,
17
+ event: ReactMouseEvent<SVGSVGElement>,
18
+ ) => WallPlanPoint
19
+ fenceDraftStart: WallPlanPoint | null
20
+ fences: FenceNode[]
21
+ findClosestWallPoint: (
22
+ point: WallPlanPoint,
23
+ walls: WallNode[],
24
+ options?: { canUseWall?: (wall: WallNode) => boolean },
25
+ ) => {
26
+ normal: [number, number, number]
27
+ point: WallPlanPoint
28
+ t: number
29
+ wall: WallNode
30
+ } | null
31
+ floorplanOpeningLocalY: number
32
+ getSnappedFloorplanPoint: (point: WallPlanPoint) => WallPlanPoint
33
+ handleCeilingPlacementPoint: (point: WallPlanPoint) => void
34
+ handleSlabPlacementPoint: (point: WallPlanPoint) => void
35
+ handleWallPlacementPoint: (point: WallPlanPoint) => void
36
+ handleZonePlacementPoint: (point: WallPlanPoint) => void
37
+ isCeilingBuildActive: boolean
38
+ isFenceBuildActive: boolean
39
+ isFloorplanGridInteractionActive: boolean
40
+ isOpeningPlacementActive: boolean
41
+ isPolygonBuildActive: boolean
42
+ isRoofBuildActive: boolean
43
+ isWallBuildActive: boolean
44
+ isZoneBuildActive: boolean
45
+ roofDraftStart: WallPlanPoint | null
46
+ setCursorPoint: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
47
+ setFenceDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
48
+ setFenceDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
49
+ setRoofDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
50
+ setRoofDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
51
+ shiftPressed: boolean
52
+ snapWallDraftPoint: (args: {
53
+ point: WallPlanPoint
54
+ walls: WallNode[]
55
+ start?: WallPlanPoint
56
+ angleSnap: boolean
57
+ }) => WallPlanPoint
58
+ snapPolygonDraftPoint: (args: {
59
+ point: WallPlanPoint
60
+ start?: WallPlanPoint
61
+ angleSnap: boolean
62
+ }) => WallPlanPoint
63
+ toPoint2D: (point: WallPlanPoint) => { x: number; y: number }
64
+ walls: WallNode[]
65
+ }
66
+
67
+ export function useFloorplanBackgroundPlacement({
68
+ activePolygonDraftPoints,
69
+ ceilingDraftPoints,
70
+ clearFencePlacementDraft,
71
+ clearRoofPlacementDraft,
72
+ emitFloorplanGridEvent,
73
+ fenceDraftStart,
74
+ fences,
75
+ findClosestWallPoint,
76
+ floorplanOpeningLocalY,
77
+ getSnappedFloorplanPoint,
78
+ handleCeilingPlacementPoint,
79
+ handleSlabPlacementPoint,
80
+ handleWallPlacementPoint,
81
+ handleZonePlacementPoint,
82
+ isCeilingBuildActive,
83
+ isFenceBuildActive,
84
+ isFloorplanGridInteractionActive,
85
+ isOpeningPlacementActive,
86
+ isPolygonBuildActive,
87
+ isRoofBuildActive,
88
+ isWallBuildActive,
89
+ isZoneBuildActive,
90
+ roofDraftStart,
91
+ setCursorPoint,
92
+ setFenceDraftEnd,
93
+ setFenceDraftStart,
94
+ setRoofDraftEnd,
95
+ setRoofDraftStart,
96
+ shiftPressed,
97
+ snapWallDraftPoint,
98
+ snapPolygonDraftPoint,
99
+ toPoint2D,
100
+ walls,
101
+ }: UseFloorplanBackgroundPlacementArgs) {
102
+ const handleBackgroundPlacementClick = useCallback(
103
+ (
104
+ planPoint: WallPlanPoint,
105
+ event: ReactMouseEvent<SVGSVGElement>,
106
+ draftStart: WallPlanPoint | null,
107
+ ) => {
108
+ if (isOpeningPlacementActive) {
109
+ const closest = findClosestWallPoint(planPoint, walls, {
110
+ canUseWall: (wall) => !isCurvedWall(wall),
111
+ })
112
+ if (closest) {
113
+ const dx = closest.wall.end[0] - closest.wall.start[0]
114
+ const dz = closest.wall.end[1] - closest.wall.start[1]
115
+ const length = Math.sqrt(dx * dx + dz * dz)
116
+ const distance = closest.t * length
117
+
118
+ emitter.emit('wall:click', {
119
+ node: closest.wall,
120
+ point: { x: closest.point[0], y: 0, z: closest.point[1] },
121
+ localPosition: [distance, floorplanOpeningLocalY, 0],
122
+ normal: closest.normal,
123
+ stopPropagation: () => {},
124
+ } as any)
125
+ }
126
+ return true
127
+ }
128
+
129
+ if (isCeilingBuildActive) {
130
+ emitFloorplanGridEvent('click', planPoint, event)
131
+
132
+ const snappedPoint = snapPolygonDraftPoint({
133
+ point: planPoint,
134
+ start: ceilingDraftPoints[ceilingDraftPoints.length - 1],
135
+ angleSnap: ceilingDraftPoints.length > 0 && !shiftPressed,
136
+ })
137
+
138
+ handleCeilingPlacementPoint(snappedPoint)
139
+ return true
140
+ }
141
+
142
+ if (isRoofBuildActive) {
143
+ const snappedPoint = getSnappedFloorplanPoint(planPoint)
144
+ emitFloorplanGridEvent('click', snappedPoint, event)
145
+ setCursorPoint(snappedPoint)
146
+
147
+ if (!roofDraftStart) {
148
+ setRoofDraftStart(snappedPoint)
149
+ setRoofDraftEnd(snappedPoint)
150
+ } else {
151
+ clearRoofPlacementDraft()
152
+ }
153
+ return true
154
+ }
155
+
156
+ if (isFenceBuildActive) {
157
+ emitFloorplanGridEvent('click', planPoint, event)
158
+
159
+ const snappedPoint = snapFenceDraftPoint({
160
+ point: planPoint,
161
+ walls,
162
+ fences,
163
+ start: fenceDraftStart ?? undefined,
164
+ angleSnap: Boolean(fenceDraftStart) && !shiftPressed,
165
+ })
166
+
167
+ setCursorPoint(snappedPoint)
168
+
169
+ if (!fenceDraftStart) {
170
+ setFenceDraftStart(snappedPoint)
171
+ setFenceDraftEnd(snappedPoint)
172
+ } else if (
173
+ getPlanPointDistance(toPoint2D(fenceDraftStart), toPoint2D(snappedPoint)) >= 0.01
174
+ ) {
175
+ clearFencePlacementDraft()
176
+ } else {
177
+ setFenceDraftEnd(snappedPoint)
178
+ }
179
+ return true
180
+ }
181
+
182
+ if (isFloorplanGridInteractionActive) {
183
+ const snappedPoint = emitFloorplanGridEvent('click', planPoint, event)
184
+ setCursorPoint(snappedPoint)
185
+ return true
186
+ }
187
+
188
+ if (isPolygonBuildActive) {
189
+ const snappedPoint = snapPolygonDraftPoint({
190
+ point: planPoint,
191
+ start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1],
192
+ angleSnap: activePolygonDraftPoints.length > 0 && !shiftPressed,
193
+ })
194
+
195
+ if (isZoneBuildActive) {
196
+ handleZonePlacementPoint(snappedPoint)
197
+ } else {
198
+ handleSlabPlacementPoint(snappedPoint)
199
+ }
200
+ return true
201
+ }
202
+
203
+ if (!isWallBuildActive) {
204
+ return false
205
+ }
206
+
207
+ const snappedPoint = snapWallDraftPoint({
208
+ point: planPoint,
209
+ walls,
210
+ start: draftStart ?? undefined,
211
+ angleSnap: Boolean(draftStart) && !shiftPressed,
212
+ })
213
+
214
+ handleWallPlacementPoint(snappedPoint)
215
+ return true
216
+ },
217
+ [
218
+ activePolygonDraftPoints,
219
+ ceilingDraftPoints,
220
+ clearFencePlacementDraft,
221
+ clearRoofPlacementDraft,
222
+ emitFloorplanGridEvent,
223
+ fenceDraftStart,
224
+ fences,
225
+ findClosestWallPoint,
226
+ floorplanOpeningLocalY,
227
+ getSnappedFloorplanPoint,
228
+ handleCeilingPlacementPoint,
229
+ handleSlabPlacementPoint,
230
+ handleZonePlacementPoint,
231
+ isCeilingBuildActive,
232
+ isFenceBuildActive,
233
+ isFloorplanGridInteractionActive,
234
+ isOpeningPlacementActive,
235
+ isPolygonBuildActive,
236
+ isRoofBuildActive,
237
+ isWallBuildActive,
238
+ isZoneBuildActive,
239
+ roofDraftStart,
240
+ setCursorPoint,
241
+ setFenceDraftEnd,
242
+ setFenceDraftStart,
243
+ setRoofDraftEnd,
244
+ setRoofDraftStart,
245
+ shiftPressed,
246
+ snapWallDraftPoint,
247
+ snapPolygonDraftPoint,
248
+ toPoint2D,
249
+ walls,
250
+ handleWallPlacementPoint,
251
+ ],
252
+ )
253
+
254
+ return {
255
+ handleBackgroundPlacementClick,
256
+ }
257
+ }
@@ -0,0 +1,171 @@
1
+ 'use client'
2
+
3
+ import type {
4
+ AnyNode,
5
+ CeilingNode,
6
+ DoorNode,
7
+ ItemNode,
8
+ Point2D,
9
+ RoofNode,
10
+ RoofSegmentNode,
11
+ SlabNode,
12
+ StairNode,
13
+ StairSegmentNode,
14
+ WallNode,
15
+ WindowNode,
16
+ } from '@pascal-app/core'
17
+ import { useCallback } from 'react'
18
+ import {
19
+ getFloorplanHitNodeId,
20
+ getFloorplanSelectionIdsInBounds,
21
+ } from '../../lib/floorplan/selection-tool'
22
+ import type { FloorplanSelectionBounds } from '../../lib/floorplan/types'
23
+ import type { WallPlanPoint } from '../tools/wall/wall-drafting'
24
+
25
+ type OpeningNode = WindowNode | DoorNode
26
+
27
+ type WallPolygonEntry = {
28
+ wall: WallNode
29
+ polygon: Point2D[]
30
+ }
31
+
32
+ type OpeningPolygonEntry = {
33
+ opening: OpeningNode
34
+ polygon: Point2D[]
35
+ }
36
+
37
+ type SlabPolygonEntry = {
38
+ slab: SlabNode
39
+ polygon: Point2D[]
40
+ holes: Point2D[][]
41
+ }
42
+
43
+ type CeilingPolygonEntry = {
44
+ ceiling: CeilingNode
45
+ polygon: Point2D[]
46
+ holes: Point2D[][]
47
+ }
48
+
49
+ type FloorplanRoofEntry = {
50
+ roof: RoofNode
51
+ segments: Array<{
52
+ polygon: Point2D[]
53
+ segment: RoofSegmentNode
54
+ }>
55
+ }
56
+
57
+ type FloorplanItemEntry = {
58
+ item: ItemNode
59
+ polygon: Point2D[]
60
+ }
61
+
62
+ type FloorplanStairSegmentEntry = {
63
+ polygon: Point2D[]
64
+ segment: StairSegmentNode | AnyNode
65
+ }
66
+
67
+ type FloorplanStairEntry = {
68
+ hitPolygons: Point2D[][]
69
+ stair: StairNode
70
+ segments: FloorplanStairSegmentEntry[]
71
+ }
72
+
73
+ type UseFloorplanHitTestingArgs = {
74
+ ceilingPolygons: CeilingPolygonEntry[]
75
+ displaySlabPolygons: SlabPolygonEntry[]
76
+ displayWallPolygons: WallPolygonEntry[]
77
+ floorplanItemEntries: FloorplanItemEntry[]
78
+ floorplanOpeningHitTolerance: number
79
+ floorplanRoofEntries: FloorplanRoofEntry[]
80
+ floorplanStairEntries: FloorplanStairEntry[]
81
+ floorplanWallHitTolerance: number
82
+ getOpeningCenterLine: (polygon: Point2D[]) => { start: Point2D; end: Point2D } | null
83
+ isFloorplanItemContextActive: boolean
84
+ openingsPolygons: OpeningPolygonEntry[]
85
+ phase: 'site' | 'structure' | 'furnish'
86
+ toPoint2D: (point: WallPlanPoint) => Point2D
87
+ }
88
+
89
+ export function useFloorplanHitTesting({
90
+ ceilingPolygons,
91
+ displaySlabPolygons,
92
+ displayWallPolygons,
93
+ floorplanItemEntries,
94
+ floorplanOpeningHitTolerance,
95
+ floorplanRoofEntries,
96
+ floorplanStairEntries,
97
+ floorplanWallHitTolerance,
98
+ getOpeningCenterLine,
99
+ isFloorplanItemContextActive,
100
+ openingsPolygons,
101
+ phase,
102
+ toPoint2D,
103
+ }: UseFloorplanHitTestingArgs) {
104
+ const getFloorplanHitIdAtPoint = useCallback(
105
+ (planPoint: WallPlanPoint) => {
106
+ const point = toPoint2D(planPoint)
107
+ return getFloorplanHitNodeId({
108
+ point,
109
+ ceilings: ceilingPolygons,
110
+ phase,
111
+ isItemContextActive: isFloorplanItemContextActive,
112
+ items: floorplanItemEntries,
113
+ openings: openingsPolygons,
114
+ roofs: floorplanRoofEntries,
115
+ stairs: floorplanStairEntries,
116
+ walls: displayWallPolygons,
117
+ slabs: displaySlabPolygons,
118
+ openingHitTolerance: floorplanOpeningHitTolerance,
119
+ wallHitTolerance: floorplanWallHitTolerance,
120
+ getOpeningCenterLine,
121
+ })
122
+ },
123
+ [
124
+ ceilingPolygons,
125
+ displaySlabPolygons,
126
+ displayWallPolygons,
127
+ floorplanItemEntries,
128
+ floorplanOpeningHitTolerance,
129
+ floorplanRoofEntries,
130
+ floorplanStairEntries,
131
+ floorplanWallHitTolerance,
132
+ getOpeningCenterLine,
133
+ isFloorplanItemContextActive,
134
+ openingsPolygons,
135
+ phase,
136
+ toPoint2D,
137
+ ],
138
+ )
139
+
140
+ const getFloorplanSelectionIdsInBoundsForArea = useCallback(
141
+ (bounds: FloorplanSelectionBounds) =>
142
+ getFloorplanSelectionIdsInBounds({
143
+ bounds,
144
+ ceilings: ceilingPolygons,
145
+ phase,
146
+ isItemContextActive: isFloorplanItemContextActive,
147
+ items: floorplanItemEntries,
148
+ walls: displayWallPolygons,
149
+ openings: openingsPolygons,
150
+ roofs: floorplanRoofEntries,
151
+ slabs: displaySlabPolygons,
152
+ stairs: floorplanStairEntries,
153
+ }),
154
+ [
155
+ ceilingPolygons,
156
+ displaySlabPolygons,
157
+ displayWallPolygons,
158
+ floorplanItemEntries,
159
+ floorplanRoofEntries,
160
+ floorplanStairEntries,
161
+ isFloorplanItemContextActive,
162
+ openingsPolygons,
163
+ phase,
164
+ ],
165
+ )
166
+
167
+ return {
168
+ getFloorplanHitIdAtPoint,
169
+ getFloorplanSelectionIdsInBounds: getFloorplanSelectionIdsInBoundsForArea,
170
+ }
171
+ }
@@ -0,0 +1,189 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type BuildingNode,
6
+ type CeilingNode,
7
+ type DoorNode,
8
+ type FenceNode,
9
+ type GuideNode,
10
+ type LevelNode,
11
+ type RoofNode,
12
+ type SiteNode,
13
+ type SlabNode,
14
+ type SpawnNode,
15
+ useScene,
16
+ type WallNode,
17
+ type WindowNode,
18
+ type ZoneNode as ZoneNodeType,
19
+ } from '@pascal-app/core'
20
+ import { useShallow } from 'zustand/react/shallow'
21
+ import { collectLevelDescendants } from '../../lib/floorplan'
22
+
23
+ type OpeningNode = WindowNode | DoorNode
24
+
25
+ const DEFAULT_BUILDING_POSITION = [0, 0, 0] as const satisfies [number, number, number]
26
+
27
+ function useLevelChildren<TNode extends AnyNode>(
28
+ levelId: LevelNode['id'] | null,
29
+ typeGuard: (node: AnyNode | undefined) => node is TNode,
30
+ ) {
31
+ return useScene(
32
+ useShallow((state) => {
33
+ if (!levelId) {
34
+ return [] as TNode[]
35
+ }
36
+
37
+ const levelNode = state.nodes[levelId]
38
+ if (!levelNode || levelNode.type !== 'level') {
39
+ return [] as TNode[]
40
+ }
41
+
42
+ return levelNode.children.map((childId) => state.nodes[childId]).filter(typeGuard)
43
+ }),
44
+ )
45
+ }
46
+
47
+ export function useFloorplanSceneData({
48
+ buildingId,
49
+ levelId,
50
+ }: {
51
+ buildingId: BuildingNode['id'] | null
52
+ levelId: LevelNode['id'] | null
53
+ }) {
54
+ const levelNode = useScene((state) =>
55
+ levelId ? (state.nodes[levelId] as LevelNode | undefined) : undefined,
56
+ )
57
+ const currentBuildingId =
58
+ levelNode?.type === 'level' && levelNode.parentId
59
+ ? (levelNode.parentId as BuildingNode['id'])
60
+ : buildingId
61
+
62
+ const buildingRotationY = useScene((state) => {
63
+ if (!currentBuildingId) return 0
64
+ const node = state.nodes[currentBuildingId]
65
+ return node?.type === 'building' ? (node.rotation[1] ?? 0) : 0
66
+ })
67
+
68
+ const buildingPosition = useScene((state) => {
69
+ if (!currentBuildingId) {
70
+ return DEFAULT_BUILDING_POSITION
71
+ }
72
+
73
+ const node = state.nodes[currentBuildingId]
74
+ return node?.type === 'building'
75
+ ? (node.position as [number, number, number])
76
+ : DEFAULT_BUILDING_POSITION
77
+ })
78
+
79
+ const site = useScene((state) => {
80
+ for (const rootNodeId of state.rootNodeIds) {
81
+ const node = state.nodes[rootNodeId]
82
+ if (node?.type === 'site') {
83
+ return node as SiteNode
84
+ }
85
+ }
86
+
87
+ return null
88
+ })
89
+
90
+ const floorplanLevels = useScene(
91
+ useShallow((state) => {
92
+ if (!currentBuildingId) {
93
+ return [] as LevelNode[]
94
+ }
95
+
96
+ const buildingNode = state.nodes[currentBuildingId]
97
+ if (!buildingNode || buildingNode.type !== 'building') {
98
+ return [] as LevelNode[]
99
+ }
100
+
101
+ return buildingNode.children
102
+ .map((childId) => state.nodes[childId])
103
+ .filter((node): node is LevelNode => node?.type === 'level')
104
+ .sort((a, b) => a.level - b.level)
105
+ }),
106
+ )
107
+
108
+ const walls = useLevelChildren(levelId, (node): node is WallNode => node?.type === 'wall')
109
+ const fences = useLevelChildren(levelId, (node): node is FenceNode => node?.type === 'fence')
110
+ const slabs = useLevelChildren(levelId, (node): node is SlabNode => node?.type === 'slab')
111
+ const ceilings = useLevelChildren(
112
+ levelId,
113
+ (node): node is CeilingNode => node?.type === 'ceiling',
114
+ )
115
+ const levelGuides = useLevelChildren(levelId, (node): node is GuideNode => node?.type === 'guide')
116
+ const zones = useLevelChildren(levelId, (node): node is ZoneNodeType => node?.type === 'zone')
117
+ const spawns = useLevelChildren(levelId, (node): node is SpawnNode => node?.type === 'spawn')
118
+ const roofs = useScene(
119
+ useShallow((state) => {
120
+ if (!levelId) {
121
+ return [] as RoofNode[]
122
+ }
123
+
124
+ const nextLevelNode = state.nodes[levelId]
125
+ if (!nextLevelNode || nextLevelNode.type !== 'level') {
126
+ return [] as RoofNode[]
127
+ }
128
+
129
+ return nextLevelNode.children
130
+ .map((childId) => state.nodes[childId])
131
+ .filter((node): node is RoofNode => node?.type === 'roof' && node.visible !== false)
132
+ }),
133
+ )
134
+ const openings = useScene(
135
+ useShallow((state) => {
136
+ if (!levelId) {
137
+ return [] as OpeningNode[]
138
+ }
139
+
140
+ const nextLevelNode = state.nodes[levelId]
141
+ if (!nextLevelNode || nextLevelNode.type !== 'level') {
142
+ return [] as OpeningNode[]
143
+ }
144
+
145
+ const nextWalls = nextLevelNode.children
146
+ .map((childId) => state.nodes[childId])
147
+ .filter((node): node is WallNode => node?.type === 'wall')
148
+
149
+ return nextWalls.flatMap((wall) =>
150
+ wall.children
151
+ .map((childId) => state.nodes[childId])
152
+ .filter((node): node is OpeningNode => node?.type === 'window' || node?.type === 'door'),
153
+ )
154
+ }),
155
+ )
156
+ const levelDescendantNodes = useScene(
157
+ useShallow((state) => {
158
+ if (!levelId) {
159
+ return [] as AnyNode[]
160
+ }
161
+
162
+ const nextLevelNode = state.nodes[levelId]
163
+ if (!nextLevelNode || nextLevelNode.type !== 'level') {
164
+ return [] as AnyNode[]
165
+ }
166
+
167
+ return collectLevelDescendants(nextLevelNode, state.nodes as Record<string, AnyNode>)
168
+ }),
169
+ )
170
+
171
+ return {
172
+ buildingPosition,
173
+ buildingRotationY,
174
+ currentBuildingId,
175
+ ceilings,
176
+ fences,
177
+ floorplanLevels,
178
+ levelDescendantNodes,
179
+ levelGuides,
180
+ levelNode,
181
+ openings,
182
+ roofs,
183
+ site,
184
+ slabs,
185
+ spawns,
186
+ walls,
187
+ zones,
188
+ }
189
+ }