@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.
- package/package.json +9 -5
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +20 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9855 -3298
- package/src/components/editor/index.tsx +269 -21
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/thumbnail-generator.tsx +38 -7
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +267 -36
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
- package/src/components/tools/fence/move-fence-tool.tsx +101 -34
- package/src/components/tools/item/move-tool.tsx +10 -1
- package/src/components/tools/item/placement-math.ts +30 -1
- package/src/components/tools/item/placement-strategies.ts +109 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
- package/src/components/tools/roof/move-roof-tool.tsx +22 -15
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/tool-manager.tsx +18 -3
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +28 -1
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +18 -1
- package/src/components/ui/controls/material-picker.tsx +152 -165
- package/src/components/ui/controls/slider-control.tsx +66 -18
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +1 -25
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +981 -289
- package/src/components/ui/panels/fence-panel.tsx +3 -45
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +210 -1
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +238 -5
- package/src/components/ui/panels/roof-panel.tsx +4 -105
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +11 -117
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +1 -95
- package/src/components/ui/panels/window-panel.tsx +660 -139
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +42 -1
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-keyboard.ts +64 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- 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 (
|
|
271
|
-
|
|
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
|
-
|
|
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
|
+
}
|