@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
|
@@ -16,15 +16,19 @@ import {
|
|
|
16
16
|
type WallNode,
|
|
17
17
|
} from '@pascal-app/core'
|
|
18
18
|
import { useViewer } from '@pascal-app/viewer'
|
|
19
|
+
import { Html } from '@react-three/drei'
|
|
19
20
|
import { useFrame } from '@react-three/fiber'
|
|
20
|
-
import { useEffect, useRef } from 'react'
|
|
21
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
21
22
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
Box3,
|
|
24
|
+
BufferGeometry,
|
|
24
25
|
Euler,
|
|
26
|
+
Float32BufferAttribute,
|
|
25
27
|
type Group,
|
|
26
28
|
type LineSegments,
|
|
29
|
+
Matrix4,
|
|
27
30
|
type Mesh,
|
|
31
|
+
type Object3D,
|
|
28
32
|
PlaneGeometry,
|
|
29
33
|
Quaternion,
|
|
30
34
|
Vector3,
|
|
@@ -33,7 +37,8 @@ import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
|
33
37
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
34
38
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
35
39
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
36
|
-
import
|
|
40
|
+
import useEditor from '../../../store/use-editor'
|
|
41
|
+
import { getGridAlignedDimensions, snapToGrid, snapUpToGridStep } from './placement-math'
|
|
37
42
|
import {
|
|
38
43
|
ceilingStrategy,
|
|
39
44
|
checkCanPlace,
|
|
@@ -46,6 +51,267 @@ import type { DraftNodeHandle } from './use-draft-node'
|
|
|
46
51
|
|
|
47
52
|
const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
|
|
48
53
|
|
|
54
|
+
function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
|
|
55
|
+
if (unit === 'imperial') {
|
|
56
|
+
const feet = value * 3.280_84
|
|
57
|
+
const wholeFeet = Math.floor(feet)
|
|
58
|
+
const inches = Math.round((feet - wholeFeet) * 12)
|
|
59
|
+
if (inches === 12) return `${wholeFeet + 1}'0"`
|
|
60
|
+
return `${wholeFeet}'${inches}"`
|
|
61
|
+
}
|
|
62
|
+
return `${Number.parseFloat(value.toFixed(2))}m`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type PreviewBounds = {
|
|
66
|
+
min: [number, number, number]
|
|
67
|
+
max: [number, number, number]
|
|
68
|
+
dimensions: [number, number, number]
|
|
69
|
+
center: [number, number, number]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Expand `bounds` outward so each axis is rounded up to the active grid step.
|
|
74
|
+
* The wireframe stays centered on the original bounds centre on each axis we
|
|
75
|
+
* expand, so an off-centre mesh bbox stays off-centre. Wall-side items keep
|
|
76
|
+
* `max.z = 0` (flush with the wall plane); the bottom (`min.y`) is preserved
|
|
77
|
+
* so the box still sits on the floor / attachment plane.
|
|
78
|
+
*
|
|
79
|
+
* Floor / ceiling / item-surface: X and Z expand; Y stays exact.
|
|
80
|
+
* Wall / wall-side: X and Y expand; Z stays exact.
|
|
81
|
+
*/
|
|
82
|
+
function expandBoundsToGrid(
|
|
83
|
+
bounds: PreviewBounds,
|
|
84
|
+
attachTo: AssetInput['attachTo'] | null | undefined,
|
|
85
|
+
step: number,
|
|
86
|
+
): PreviewBounds {
|
|
87
|
+
const [w, h, d] = bounds.dimensions
|
|
88
|
+
const [cx, , cz] = bounds.center
|
|
89
|
+
const onWall = attachTo === 'wall' || attachTo === 'wall-side'
|
|
90
|
+
const expandedW = snapUpToGridStep(w, step)
|
|
91
|
+
const expandedH = onWall ? snapUpToGridStep(h, step) : h
|
|
92
|
+
const expandedD = onWall ? d : snapUpToGridStep(d, step)
|
|
93
|
+
|
|
94
|
+
const minX = cx - expandedW / 2
|
|
95
|
+
const maxX = cx + expandedW / 2
|
|
96
|
+
const minY = bounds.min[1]
|
|
97
|
+
const maxY = minY + expandedH
|
|
98
|
+
|
|
99
|
+
let minZ: number
|
|
100
|
+
let maxZ: number
|
|
101
|
+
let newCz: number
|
|
102
|
+
if (attachTo === 'wall-side') {
|
|
103
|
+
maxZ = 0
|
|
104
|
+
minZ = -expandedD
|
|
105
|
+
newCz = -expandedD / 2
|
|
106
|
+
} else {
|
|
107
|
+
minZ = cz - expandedD / 2
|
|
108
|
+
maxZ = cz + expandedD / 2
|
|
109
|
+
newCz = cz
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
min: [minX, minY, minZ],
|
|
114
|
+
max: [maxX, maxY, maxZ],
|
|
115
|
+
dimensions: [expandedW, expandedH, expandedD],
|
|
116
|
+
center: [cx, (minY + maxY) / 2, newCz],
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getPreviewBoundsFromObject(object: Object3D | null): PreviewBounds | null {
|
|
121
|
+
if (!object) return null
|
|
122
|
+
|
|
123
|
+
object.updateWorldMatrix(true, true)
|
|
124
|
+
|
|
125
|
+
const inverseRootMatrix = new Matrix4().copy(object.matrixWorld).invert()
|
|
126
|
+
const localMatrix = new Matrix4()
|
|
127
|
+
const localBounds = new Box3()
|
|
128
|
+
const scratchBounds = new Box3()
|
|
129
|
+
const hasBounds = { current: false }
|
|
130
|
+
const registeredNodeObjects = new Set(sceneRegistry.nodes.values())
|
|
131
|
+
|
|
132
|
+
const expandBounds = (child: Object3D) => {
|
|
133
|
+
if (child !== object && registeredNodeObjects.has(child)) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mesh = child as Object3D & {
|
|
138
|
+
isMesh?: boolean
|
|
139
|
+
name?: string
|
|
140
|
+
geometry?: {
|
|
141
|
+
boundingBox: Box3 | null
|
|
142
|
+
computeBoundingBox?: () => void
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (mesh.isMesh && mesh.name !== 'cutout' && mesh.geometry) {
|
|
147
|
+
if (!mesh.geometry.boundingBox && mesh.geometry.computeBoundingBox) {
|
|
148
|
+
mesh.geometry.computeBoundingBox()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (mesh.geometry.boundingBox) {
|
|
152
|
+
localMatrix.copy(inverseRootMatrix).multiply(mesh.matrixWorld)
|
|
153
|
+
scratchBounds.copy(mesh.geometry.boundingBox).applyMatrix4(localMatrix)
|
|
154
|
+
if (Number.isFinite(scratchBounds.min.x) && Number.isFinite(scratchBounds.max.x)) {
|
|
155
|
+
if (!hasBounds.current) {
|
|
156
|
+
localBounds.copy(scratchBounds)
|
|
157
|
+
hasBounds.current = true
|
|
158
|
+
} else {
|
|
159
|
+
localBounds.union(scratchBounds)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const grandchild of child.children) {
|
|
166
|
+
expandBounds(grandchild)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const child of object.children) {
|
|
171
|
+
expandBounds(child)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!hasBounds.current) return null
|
|
175
|
+
|
|
176
|
+
const size = new Vector3()
|
|
177
|
+
const center = new Vector3()
|
|
178
|
+
localBounds.getSize(size)
|
|
179
|
+
localBounds.getCenter(center)
|
|
180
|
+
|
|
181
|
+
if (size.x <= 0 || size.y <= 0 || size.z <= 0) {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
min: [localBounds.min.x, localBounds.min.y, localBounds.min.z],
|
|
187
|
+
max: [localBounds.max.x, localBounds.max.y, localBounds.max.z],
|
|
188
|
+
dimensions: [size.x, size.y, size.z],
|
|
189
|
+
center: [center.x, center.y, center.z],
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getFallbackPreviewBounds(
|
|
194
|
+
item: import('@pascal-app/core').ItemNode | null,
|
|
195
|
+
asset: AssetInput,
|
|
196
|
+
attachTo: AssetInput['attachTo'],
|
|
197
|
+
): PreviewBounds {
|
|
198
|
+
const dims = item ? getScaledDimensions(item) : (asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
199
|
+
return {
|
|
200
|
+
min: [-dims[0] / 2, 0, attachTo === 'wall-side' ? -dims[2] : -dims[2] / 2],
|
|
201
|
+
max: [dims[0] / 2, dims[1], attachTo === 'wall-side' ? 0 : dims[2] / 2],
|
|
202
|
+
dimensions: dims,
|
|
203
|
+
center: [0, dims[1] / 2, attachTo === 'wall-side' ? -dims[2] / 2 : 0],
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createLineGeometry(points: number[] = [0, 0, 0, 0, 0, 0]): BufferGeometry {
|
|
208
|
+
const geometry = new BufferGeometry()
|
|
209
|
+
geometry.setAttribute('position', new Float32BufferAttribute(points, 3))
|
|
210
|
+
return geometry
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getBoxEdgePoints(bounds: PreviewBounds): number[] {
|
|
214
|
+
const [width, height, depth] = bounds.dimensions
|
|
215
|
+
const [centerX, centerY, centerZ] = bounds.center
|
|
216
|
+
const minX = centerX - width / 2
|
|
217
|
+
const maxX = centerX + width / 2
|
|
218
|
+
const minY = centerY - height / 2
|
|
219
|
+
const maxY = centerY + height / 2
|
|
220
|
+
const minZ = centerZ - depth / 2
|
|
221
|
+
const maxZ = centerZ + depth / 2
|
|
222
|
+
|
|
223
|
+
return [
|
|
224
|
+
minX,
|
|
225
|
+
minY,
|
|
226
|
+
minZ,
|
|
227
|
+
maxX,
|
|
228
|
+
minY,
|
|
229
|
+
minZ,
|
|
230
|
+
maxX,
|
|
231
|
+
minY,
|
|
232
|
+
minZ,
|
|
233
|
+
maxX,
|
|
234
|
+
minY,
|
|
235
|
+
maxZ,
|
|
236
|
+
maxX,
|
|
237
|
+
minY,
|
|
238
|
+
maxZ,
|
|
239
|
+
minX,
|
|
240
|
+
minY,
|
|
241
|
+
maxZ,
|
|
242
|
+
minX,
|
|
243
|
+
minY,
|
|
244
|
+
maxZ,
|
|
245
|
+
minX,
|
|
246
|
+
minY,
|
|
247
|
+
minZ,
|
|
248
|
+
|
|
249
|
+
minX,
|
|
250
|
+
maxY,
|
|
251
|
+
minZ,
|
|
252
|
+
maxX,
|
|
253
|
+
maxY,
|
|
254
|
+
minZ,
|
|
255
|
+
maxX,
|
|
256
|
+
maxY,
|
|
257
|
+
minZ,
|
|
258
|
+
maxX,
|
|
259
|
+
maxY,
|
|
260
|
+
maxZ,
|
|
261
|
+
maxX,
|
|
262
|
+
maxY,
|
|
263
|
+
maxZ,
|
|
264
|
+
minX,
|
|
265
|
+
maxY,
|
|
266
|
+
maxZ,
|
|
267
|
+
minX,
|
|
268
|
+
maxY,
|
|
269
|
+
maxZ,
|
|
270
|
+
minX,
|
|
271
|
+
maxY,
|
|
272
|
+
minZ,
|
|
273
|
+
|
|
274
|
+
minX,
|
|
275
|
+
minY,
|
|
276
|
+
minZ,
|
|
277
|
+
minX,
|
|
278
|
+
maxY,
|
|
279
|
+
minZ,
|
|
280
|
+
maxX,
|
|
281
|
+
minY,
|
|
282
|
+
minZ,
|
|
283
|
+
maxX,
|
|
284
|
+
maxY,
|
|
285
|
+
minZ,
|
|
286
|
+
maxX,
|
|
287
|
+
minY,
|
|
288
|
+
maxZ,
|
|
289
|
+
maxX,
|
|
290
|
+
maxY,
|
|
291
|
+
maxZ,
|
|
292
|
+
minX,
|
|
293
|
+
minY,
|
|
294
|
+
maxZ,
|
|
295
|
+
minX,
|
|
296
|
+
maxY,
|
|
297
|
+
maxZ,
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function updateLineGeometry(ref: React.RefObject<LineSegments>, points: number[]) {
|
|
302
|
+
const geometry = ref.current?.geometry
|
|
303
|
+
if (!geometry) return
|
|
304
|
+
|
|
305
|
+
const attribute = geometry.getAttribute('position') as Float32BufferAttribute | undefined
|
|
306
|
+
if (!attribute || attribute.array.length !== points.length) {
|
|
307
|
+
geometry.setAttribute('position', new Float32BufferAttribute(points, 3))
|
|
308
|
+
} else {
|
|
309
|
+
attribute.set(points)
|
|
310
|
+
attribute.needsUpdate = true
|
|
311
|
+
}
|
|
312
|
+
geometry.computeBoundingSphere()
|
|
313
|
+
}
|
|
314
|
+
|
|
49
315
|
// Shared materials for placement cursor - we just change colors, not swap materials
|
|
50
316
|
// Note: EdgesGeometry doesn't work with dashed lines, so using solid lines
|
|
51
317
|
const edgeMaterial = new LineBasicNodeMaterial({
|
|
@@ -55,6 +321,13 @@ const edgeMaterial = new LineBasicNodeMaterial({
|
|
|
55
321
|
depthWrite: false,
|
|
56
322
|
})
|
|
57
323
|
|
|
324
|
+
const measurementMaterial = new LineBasicNodeMaterial({
|
|
325
|
+
color: 0x0f_17_2a,
|
|
326
|
+
linewidth: 2,
|
|
327
|
+
depthTest: false,
|
|
328
|
+
depthWrite: false,
|
|
329
|
+
})
|
|
330
|
+
|
|
58
331
|
const basePlaneMaterial = new MeshBasicNodeMaterial({
|
|
59
332
|
color: 0xef_44_44, // red-500 (invalid)
|
|
60
333
|
transparent: true,
|
|
@@ -82,6 +355,9 @@ export interface PlacementCoordinatorConfig {
|
|
|
82
355
|
export function usePlacementCoordinator(config: PlacementCoordinatorConfig): React.ReactNode {
|
|
83
356
|
const cursorGroupRef = useRef<Group>(null!)
|
|
84
357
|
const edgesRef = useRef<LineSegments>(null!)
|
|
358
|
+
const measurementWidthRef = useRef<LineSegments>(null!)
|
|
359
|
+
const measurementDepthRef = useRef<LineSegments>(null!)
|
|
360
|
+
const measurementHeightRef = useRef<LineSegments>(null!)
|
|
85
361
|
const basePlaneRef = useRef<Mesh>(null!)
|
|
86
362
|
const gridPosition = useRef(new Vector3(0, 0, 0))
|
|
87
363
|
const lastRawPos = useRef(new Vector3(0, 0, 0))
|
|
@@ -89,6 +365,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
89
365
|
config.initialState ?? { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null },
|
|
90
366
|
)
|
|
91
367
|
const shiftFreeRef = useRef(false)
|
|
368
|
+
const previewBoundsSignatureRef = useRef<string | null>(null)
|
|
369
|
+
const meshPreviewAppliedRef = useRef(false)
|
|
370
|
+
const [dimensionBounds, setDimensionBounds] = useState<PreviewBounds | null>(null)
|
|
92
371
|
|
|
93
372
|
// Store config callbacks in refs to avoid re-running effect when they change
|
|
94
373
|
const configRef = useRef(config)
|
|
@@ -96,10 +375,135 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
96
375
|
|
|
97
376
|
const { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling } = useSpatialQuery()
|
|
98
377
|
const { asset, draftNode } = config
|
|
378
|
+
const unit = useViewer((state) => state.unit)
|
|
379
|
+
const gridSnapStep = useEditor((s) => s.gridSnapStep)
|
|
380
|
+
const updatePreviewGeometry = (bounds: PreviewBounds) => {
|
|
381
|
+
const [width, height, depth] = bounds.dimensions
|
|
382
|
+
const [centerX, centerY, centerZ] = bounds.center
|
|
383
|
+
const signature = `${width.toFixed(4)}:${height.toFixed(4)}:${depth.toFixed(4)}:${centerX.toFixed(4)}:${centerY.toFixed(4)}:${centerZ.toFixed(4)}`
|
|
384
|
+
|
|
385
|
+
if (previewBoundsSignatureRef.current === signature) return
|
|
386
|
+
previewBoundsSignatureRef.current = signature
|
|
387
|
+
|
|
388
|
+
const nextBasePlaneGeometry = new PlaneGeometry(width, depth)
|
|
389
|
+
nextBasePlaneGeometry.rotateX(-Math.PI / 2)
|
|
390
|
+
nextBasePlaneGeometry.translate(centerX, 0.01, centerZ)
|
|
391
|
+
|
|
392
|
+
updateLineGeometry(edgesRef, getBoxEdgePoints(bounds))
|
|
393
|
+
|
|
394
|
+
const oldBasePlaneGeometry = basePlaneRef.current.geometry
|
|
395
|
+
basePlaneRef.current.geometry = nextBasePlaneGeometry
|
|
396
|
+
oldBasePlaneGeometry.dispose()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const updateDimensionGuides = (bounds: PreviewBounds) => {
|
|
400
|
+
setDimensionBounds((current) => {
|
|
401
|
+
if (
|
|
402
|
+
current &&
|
|
403
|
+
current.dimensions[0] === bounds.dimensions[0] &&
|
|
404
|
+
current.dimensions[1] === bounds.dimensions[1] &&
|
|
405
|
+
current.dimensions[2] === bounds.dimensions[2] &&
|
|
406
|
+
current.center[0] === bounds.center[0] &&
|
|
407
|
+
current.center[1] === bounds.center[1] &&
|
|
408
|
+
current.center[2] === bounds.center[2]
|
|
409
|
+
) {
|
|
410
|
+
return current
|
|
411
|
+
}
|
|
412
|
+
return bounds
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const [width, , depth] = bounds.dimensions
|
|
416
|
+
const [centerX, , centerZ] = bounds.center
|
|
417
|
+
const minX = centerX - width / 2
|
|
418
|
+
const maxX = centerX + width / 2
|
|
419
|
+
const minZ = centerZ - depth / 2
|
|
420
|
+
const maxZ = centerZ + depth / 2
|
|
421
|
+
const guideOffset = 0.18
|
|
422
|
+
const tick = 0.08
|
|
423
|
+
const y = 0.02
|
|
424
|
+
|
|
425
|
+
const widthPoints = [
|
|
426
|
+
minX,
|
|
427
|
+
y,
|
|
428
|
+
maxZ + guideOffset,
|
|
429
|
+
maxX,
|
|
430
|
+
y,
|
|
431
|
+
maxZ + guideOffset,
|
|
432
|
+
|
|
433
|
+
minX,
|
|
434
|
+
y,
|
|
435
|
+
maxZ + guideOffset - tick,
|
|
436
|
+
minX,
|
|
437
|
+
y,
|
|
438
|
+
maxZ + guideOffset + tick,
|
|
439
|
+
|
|
440
|
+
maxX,
|
|
441
|
+
y,
|
|
442
|
+
maxZ + guideOffset - tick,
|
|
443
|
+
maxX,
|
|
444
|
+
y,
|
|
445
|
+
maxZ + guideOffset + tick,
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
const depthPoints = [
|
|
449
|
+
maxX + guideOffset,
|
|
450
|
+
y,
|
|
451
|
+
minZ,
|
|
452
|
+
maxX + guideOffset,
|
|
453
|
+
y,
|
|
454
|
+
maxZ,
|
|
455
|
+
|
|
456
|
+
maxX + guideOffset - tick,
|
|
457
|
+
y,
|
|
458
|
+
minZ,
|
|
459
|
+
maxX + guideOffset + tick,
|
|
460
|
+
y,
|
|
461
|
+
minZ,
|
|
462
|
+
|
|
463
|
+
maxX + guideOffset - tick,
|
|
464
|
+
y,
|
|
465
|
+
maxZ,
|
|
466
|
+
maxX + guideOffset + tick,
|
|
467
|
+
y,
|
|
468
|
+
maxZ,
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
const heightPoints = [
|
|
472
|
+
minX - guideOffset,
|
|
473
|
+
0,
|
|
474
|
+
minZ,
|
|
475
|
+
minX - guideOffset,
|
|
476
|
+
bounds.dimensions[1],
|
|
477
|
+
minZ,
|
|
478
|
+
|
|
479
|
+
minX - guideOffset - tick,
|
|
480
|
+
0,
|
|
481
|
+
minZ,
|
|
482
|
+
minX - guideOffset + tick,
|
|
483
|
+
0,
|
|
484
|
+
minZ,
|
|
485
|
+
|
|
486
|
+
minX - guideOffset - tick,
|
|
487
|
+
bounds.dimensions[1],
|
|
488
|
+
minZ,
|
|
489
|
+
minX - guideOffset + tick,
|
|
490
|
+
bounds.dimensions[1],
|
|
491
|
+
minZ,
|
|
492
|
+
]
|
|
493
|
+
|
|
494
|
+
const applyPoints = (ref: React.RefObject<LineSegments>, points: number[]) => {
|
|
495
|
+
updateLineGeometry(ref, points)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
applyPoints(measurementWidthRef, widthPoints)
|
|
499
|
+
applyPoints(measurementDepthRef, depthPoints)
|
|
500
|
+
applyPoints(measurementHeightRef, heightPoints)
|
|
501
|
+
}
|
|
99
502
|
|
|
100
503
|
useEffect(() => {
|
|
101
504
|
if (!asset) return
|
|
102
505
|
useScene.temporal.getState().pause()
|
|
506
|
+
meshPreviewAppliedRef.current = false
|
|
103
507
|
|
|
104
508
|
const validators = { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
|
|
105
509
|
|
|
@@ -110,6 +514,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
110
514
|
ceilingId: null,
|
|
111
515
|
surfaceItemId: null,
|
|
112
516
|
}
|
|
517
|
+
if (!asset.attachTo && placementState.current.surface === 'floor') {
|
|
518
|
+
gridPosition.current.y = 0
|
|
519
|
+
cursorGroupRef.current.position.y = 0
|
|
520
|
+
}
|
|
113
521
|
|
|
114
522
|
// ---- Helpers ----
|
|
115
523
|
|
|
@@ -119,6 +527,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
119
527
|
draftItem: draftNode.current,
|
|
120
528
|
gridPosition: gridPosition.current,
|
|
121
529
|
state: { ...placementState.current },
|
|
530
|
+
currentCursorRotationY: cursorGroupRef.current.rotation.y,
|
|
122
531
|
})
|
|
123
532
|
|
|
124
533
|
const getActiveValidators = () =>
|
|
@@ -236,9 +645,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
236
645
|
|
|
237
646
|
previousGridPos = [...result.gridPosition]
|
|
238
647
|
gridPosition.current.set(...result.gridPosition)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
648
|
+
cursorGroupRef.current.position.set(
|
|
649
|
+
result.cursorPosition[0],
|
|
650
|
+
result.cursorPosition[1],
|
|
651
|
+
result.cursorPosition[2],
|
|
652
|
+
)
|
|
242
653
|
|
|
243
654
|
const draft = draftNode.current
|
|
244
655
|
if (draft) draft.position = result.gridPosition
|
|
@@ -463,6 +874,32 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
463
874
|
|
|
464
875
|
// ---- Item Surface Handlers ----
|
|
465
876
|
|
|
877
|
+
const detachItemSurfaceToFloor = (event: ItemEvent) => {
|
|
878
|
+
const buildingLocalPoint = worldToBuildingLocal(
|
|
879
|
+
event.position[0],
|
|
880
|
+
event.position[1],
|
|
881
|
+
event.position[2],
|
|
882
|
+
)
|
|
883
|
+
const wx = Math.round(buildingLocalPoint.x * 2) / 2
|
|
884
|
+
const wz = Math.round(buildingLocalPoint.z * 2) / 2
|
|
885
|
+
const floorPos: [number, number, number] = [wx, 0, wz]
|
|
886
|
+
|
|
887
|
+
Object.assign(placementState.current, { surface: 'floor', surfaceItemId: null })
|
|
888
|
+
gridPosition.current.set(wx, 0, wz)
|
|
889
|
+
cursorGroupRef.current.position.set(wx, 0, wz)
|
|
890
|
+
|
|
891
|
+
const draft = draftNode.current
|
|
892
|
+
if (draft) {
|
|
893
|
+
draft.position = floorPos
|
|
894
|
+
useScene.getState().updateNode(draft.id, {
|
|
895
|
+
parentId: useViewer.getState().selection.levelId as string,
|
|
896
|
+
position: floorPos,
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
revalidate()
|
|
901
|
+
}
|
|
902
|
+
|
|
466
903
|
const onItemEnter = (event: ItemEvent) => {
|
|
467
904
|
if (event.node.id === draftNode.current?.id) return
|
|
468
905
|
const result = itemSurfaceStrategy.enter(getContext(), event)
|
|
@@ -496,6 +933,24 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
496
933
|
return
|
|
497
934
|
}
|
|
498
935
|
|
|
936
|
+
if (ctx.state.surface === 'item-surface' && event.node.id !== ctx.state.surfaceItemId) {
|
|
937
|
+
const enterResult = itemSurfaceStrategy.enter(
|
|
938
|
+
{ ...ctx, state: { ...ctx.state, surface: 'floor', surfaceItemId: null } },
|
|
939
|
+
event,
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
event.stopPropagation()
|
|
943
|
+
if (enterResult) {
|
|
944
|
+
applyTransition(enterResult)
|
|
945
|
+
if (draftNode.current && enterResult.nodeUpdate.parentId) {
|
|
946
|
+
useScene.getState().updateNode(draftNode.current.id, enterResult.nodeUpdate)
|
|
947
|
+
}
|
|
948
|
+
} else {
|
|
949
|
+
detachItemSurfaceToFloor(event)
|
|
950
|
+
}
|
|
951
|
+
return
|
|
952
|
+
}
|
|
953
|
+
|
|
499
954
|
if (!draftNode.current) {
|
|
500
955
|
const enterResult = itemSurfaceStrategy.enter(getContext(), event)
|
|
501
956
|
if (!enterResult) return
|
|
@@ -537,26 +992,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
537
992
|
|
|
538
993
|
event.stopPropagation()
|
|
539
994
|
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
gridPosition.current.set(wx, 0, wz)
|
|
547
|
-
cursorGroupRef.current.position.x = wx
|
|
548
|
-
cursorGroupRef.current.position.z = wz
|
|
549
|
-
|
|
550
|
-
const draft = draftNode.current
|
|
551
|
-
if (draft) {
|
|
552
|
-
draft.position = floorPos
|
|
553
|
-
useScene.getState().updateNode(draft.id, {
|
|
554
|
-
parentId: useViewer.getState().selection.levelId as string,
|
|
555
|
-
position: floorPos,
|
|
556
|
-
})
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
revalidate()
|
|
995
|
+
// `event.localPosition` from useNodeEvents is in the LEAVING item's
|
|
996
|
+
// local space (the sofa/table the draft is detaching from), not
|
|
997
|
+
// building-local. Convert from world via worldToBuildingLocal instead,
|
|
998
|
+
// otherwise the wireframe jumps to a surface-local-coordinate ghost
|
|
999
|
+
// position until the next mouse move.
|
|
1000
|
+
detachItemSurfaceToFloor(event)
|
|
560
1001
|
}
|
|
561
1002
|
|
|
562
1003
|
const onItemClick = (event: ItemEvent) => {
|
|
@@ -614,7 +1055,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
614
1055
|
return
|
|
615
1056
|
}
|
|
616
1057
|
|
|
617
|
-
lastRawPos.current.set(event.
|
|
1058
|
+
lastRawPos.current.set(event.localPosition[0], event.localPosition[1], event.localPosition[2])
|
|
618
1059
|
const result = ceilingStrategy.move(getContext(), event)
|
|
619
1060
|
if (!result) return
|
|
620
1061
|
|
|
@@ -825,12 +1266,21 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
825
1266
|
// ---- Bounding box geometry ----
|
|
826
1267
|
|
|
827
1268
|
const draft = draftNode.current
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1269
|
+
const fallbackBounds = expandBoundsToGrid(
|
|
1270
|
+
getFallbackPreviewBounds(draft, asset, asset.attachTo),
|
|
1271
|
+
asset.attachTo,
|
|
1272
|
+
gridSnapStep,
|
|
1273
|
+
)
|
|
1274
|
+
const previewBounds = draft
|
|
1275
|
+
? expandBoundsToGrid(
|
|
1276
|
+
getPreviewBoundsFromObject(sceneRegistry.nodes.get(draft.id) ?? null) ??
|
|
1277
|
+
getFallbackPreviewBounds(draft, asset, asset.attachTo),
|
|
1278
|
+
asset.attachTo,
|
|
1279
|
+
gridSnapStep,
|
|
1280
|
+
)
|
|
1281
|
+
: fallbackBounds
|
|
1282
|
+
updatePreviewGeometry(previewBounds)
|
|
1283
|
+
updateDimensionGuides(previewBounds)
|
|
834
1284
|
|
|
835
1285
|
// ---- Undo protection ----
|
|
836
1286
|
// Undo replaces the entire `nodes` object with a previous snapshot, which doesn't
|
|
@@ -874,6 +1324,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
874
1324
|
|
|
875
1325
|
return () => {
|
|
876
1326
|
tearingDown = true
|
|
1327
|
+
meshPreviewAppliedRef.current = false
|
|
877
1328
|
unsubDraftWatch()
|
|
878
1329
|
// Clear live transform for any remaining draft
|
|
879
1330
|
if (draftNode.current) {
|
|
@@ -902,7 +1353,25 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
902
1353
|
}
|
|
903
1354
|
}, [asset, canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling, draftNode])
|
|
904
1355
|
|
|
905
|
-
//
|
|
1356
|
+
// Refresh wireframe when the grid step changes mid-placement so the green/red
|
|
1357
|
+
// box snaps to the new cell size right away.
|
|
1358
|
+
useEffect(() => {
|
|
1359
|
+
if (!asset) return
|
|
1360
|
+
const draft = draftNode.current
|
|
1361
|
+
const fallbackBounds = expandBoundsToGrid(
|
|
1362
|
+
getFallbackPreviewBounds(draft, asset, asset.attachTo),
|
|
1363
|
+
asset.attachTo,
|
|
1364
|
+
gridSnapStep,
|
|
1365
|
+
)
|
|
1366
|
+
const meshBounds = draft
|
|
1367
|
+
? getPreviewBoundsFromObject(sceneRegistry.nodes.get(draft.id) ?? null)
|
|
1368
|
+
: null
|
|
1369
|
+
const previewBounds = meshBounds
|
|
1370
|
+
? expandBoundsToGrid(meshBounds, asset.attachTo, gridSnapStep)
|
|
1371
|
+
: fallbackBounds
|
|
1372
|
+
updatePreviewGeometry(previewBounds)
|
|
1373
|
+
updateDimensionGuides(previewBounds)
|
|
1374
|
+
}, [gridSnapStep, asset, draftNode])
|
|
906
1375
|
// Wall/ceiling items are managed by their own surface entry events (ensureDraft / reparent).
|
|
907
1376
|
const viewerLevelId = useViewer((s) => s.selection.levelId)
|
|
908
1377
|
useEffect(() => {
|
|
@@ -919,6 +1388,19 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
919
1388
|
if (!draftNode.current) return
|
|
920
1389
|
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
|
921
1390
|
if (!mesh) return
|
|
1391
|
+
if (!meshPreviewAppliedRef.current) {
|
|
1392
|
+
const previewBounds = getPreviewBoundsFromObject(mesh)
|
|
1393
|
+
if (previewBounds) {
|
|
1394
|
+
const expandedBounds = expandBoundsToGrid(
|
|
1395
|
+
previewBounds,
|
|
1396
|
+
asset.attachTo,
|
|
1397
|
+
useEditor.getState().gridSnapStep,
|
|
1398
|
+
)
|
|
1399
|
+
updatePreviewGeometry(expandedBounds)
|
|
1400
|
+
updateDimensionGuides(expandedBounds)
|
|
1401
|
+
meshPreviewAppliedRef.current = true
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
922
1404
|
|
|
923
1405
|
// Hide wall/ceiling-attached items when between surfaces (only cursor visible)
|
|
924
1406
|
if (asset.attachTo && placementState.current.surface === 'floor') {
|
|
@@ -942,36 +1424,162 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
942
1424
|
const slabElevation = spatialGridManager.getSlabElevationForItem(
|
|
943
1425
|
levelId,
|
|
944
1426
|
[gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
|
945
|
-
|
|
1427
|
+
getGridAlignedDimensions(
|
|
1428
|
+
getScaledDimensions(draftNode.current),
|
|
1429
|
+
draftNode.current.asset.attachTo,
|
|
1430
|
+
),
|
|
946
1431
|
draftNode.current.rotation,
|
|
947
1432
|
)
|
|
948
1433
|
mesh.position.y = slabElevation
|
|
949
|
-
// Cursor group is at the world root (not inside a level group), so add the
|
|
950
|
-
// level group's current world Y to convert from level-local to world space.
|
|
951
|
-
const levelGroup = sceneRegistry.nodes.get(levelId as AnyNodeId)
|
|
952
|
-
cursorGroupRef.current.position.y = slabElevation + (levelGroup?.position.y ?? 0)
|
|
953
1434
|
}
|
|
954
1435
|
}
|
|
955
1436
|
})
|
|
956
1437
|
|
|
957
1438
|
const initialDraft = draftNode.current
|
|
958
|
-
const
|
|
1439
|
+
const initialAttachTo = config.asset?.attachTo
|
|
1440
|
+
const rawDims = initialDraft
|
|
959
1441
|
? getScaledDimensions(initialDraft)
|
|
960
1442
|
: (config.asset?.dimensions ?? DEFAULT_DIMENSIONS)
|
|
961
|
-
const
|
|
962
|
-
const wallSideZOffset =
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1443
|
+
const dims = getGridAlignedDimensions(rawDims, initialAttachTo, gridSnapStep)
|
|
1444
|
+
const wallSideZOffset = initialAttachTo === 'wall-side' ? -dims[2] / 2 : 0
|
|
1445
|
+
const initialDimensionBounds = expandBoundsToGrid(
|
|
1446
|
+
getFallbackPreviewBounds(initialDraft, config.asset!, initialAttachTo),
|
|
1447
|
+
initialAttachTo,
|
|
1448
|
+
gridSnapStep,
|
|
1449
|
+
)
|
|
1450
|
+
const initialEdgeGeometry = useMemo(
|
|
1451
|
+
() => createLineGeometry(getBoxEdgePoints(initialDimensionBounds)),
|
|
1452
|
+
[
|
|
1453
|
+
initialDimensionBounds.center[0],
|
|
1454
|
+
initialDimensionBounds.center[1],
|
|
1455
|
+
initialDimensionBounds.center[2],
|
|
1456
|
+
initialDimensionBounds.dimensions[0],
|
|
1457
|
+
initialDimensionBounds.dimensions[1],
|
|
1458
|
+
initialDimensionBounds.dimensions[2],
|
|
1459
|
+
],
|
|
1460
|
+
)
|
|
1461
|
+
const basePlaneGeometry = useMemo(() => {
|
|
1462
|
+
const geometry = new PlaneGeometry(dims[0], dims[2])
|
|
1463
|
+
geometry.rotateX(-Math.PI / 2)
|
|
1464
|
+
geometry.translate(0, 0.01, wallSideZOffset)
|
|
1465
|
+
return geometry
|
|
1466
|
+
}, [dims[0], dims[2], wallSideZOffset])
|
|
1467
|
+
const initialWidthGuideGeometry = useMemo(() => createLineGeometry(), [])
|
|
1468
|
+
const initialDepthGuideGeometry = useMemo(() => createLineGeometry(), [])
|
|
1469
|
+
const initialHeightGuideGeometry = useMemo(() => createLineGeometry(), [])
|
|
1470
|
+
const currentDimensionBounds = dimensionBounds ?? initialDimensionBounds
|
|
1471
|
+
const widthLabel = formatMeasurement(currentDimensionBounds.dimensions[0], unit)
|
|
1472
|
+
const depthLabel = formatMeasurement(currentDimensionBounds.dimensions[2], unit)
|
|
1473
|
+
const heightLabel = formatMeasurement(currentDimensionBounds.dimensions[1], unit)
|
|
1474
|
+
const widthLabelPosition: [number, number, number] = [
|
|
1475
|
+
currentDimensionBounds.center[0],
|
|
1476
|
+
0.04,
|
|
1477
|
+
currentDimensionBounds.center[2] + currentDimensionBounds.dimensions[2] / 2 + 0.24,
|
|
1478
|
+
]
|
|
1479
|
+
const depthLabelPosition: [number, number, number] = [
|
|
1480
|
+
currentDimensionBounds.center[0] + currentDimensionBounds.dimensions[0] / 2 + 0.24,
|
|
1481
|
+
0.04,
|
|
1482
|
+
currentDimensionBounds.center[2],
|
|
1483
|
+
]
|
|
1484
|
+
const heightLabelPosition: [number, number, number] = [
|
|
1485
|
+
currentDimensionBounds.center[0] - currentDimensionBounds.dimensions[0] / 2 - 0.24,
|
|
1486
|
+
currentDimensionBounds.dimensions[1] / 2,
|
|
1487
|
+
currentDimensionBounds.center[2] - currentDimensionBounds.dimensions[2] / 2,
|
|
1488
|
+
]
|
|
1489
|
+
|
|
1490
|
+
const measurementContent = (
|
|
1491
|
+
<>
|
|
1492
|
+
<lineSegments
|
|
1493
|
+
layers={EDITOR_LAYER}
|
|
1494
|
+
geometry={initialWidthGuideGeometry}
|
|
1495
|
+
material={measurementMaterial}
|
|
1496
|
+
ref={measurementWidthRef}
|
|
1497
|
+
renderOrder={998}
|
|
1498
|
+
/>
|
|
1499
|
+
<lineSegments
|
|
1500
|
+
layers={EDITOR_LAYER}
|
|
1501
|
+
geometry={initialDepthGuideGeometry}
|
|
1502
|
+
material={measurementMaterial}
|
|
1503
|
+
ref={measurementDepthRef}
|
|
1504
|
+
renderOrder={998}
|
|
1505
|
+
/>
|
|
1506
|
+
<lineSegments
|
|
1507
|
+
layers={EDITOR_LAYER}
|
|
1508
|
+
geometry={initialHeightGuideGeometry}
|
|
1509
|
+
material={measurementMaterial}
|
|
1510
|
+
ref={measurementHeightRef}
|
|
1511
|
+
renderOrder={998}
|
|
1512
|
+
/>
|
|
1513
|
+
<Html center position={widthLabelPosition} style={{ pointerEvents: 'none' }}>
|
|
1514
|
+
<div
|
|
1515
|
+
style={{
|
|
1516
|
+
background: 'rgba(15, 23, 42, 0.86)',
|
|
1517
|
+
border: '1px solid rgba(15, 23, 42, 0.65)',
|
|
1518
|
+
borderRadius: '999px',
|
|
1519
|
+
color: '#f8fafc',
|
|
1520
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
1521
|
+
fontSize: '11px',
|
|
1522
|
+
fontWeight: 600,
|
|
1523
|
+
lineHeight: 1,
|
|
1524
|
+
padding: '4px 8px',
|
|
1525
|
+
pointerEvents: 'none',
|
|
1526
|
+
whiteSpace: 'nowrap',
|
|
1527
|
+
}}
|
|
1528
|
+
>
|
|
1529
|
+
{widthLabel}
|
|
1530
|
+
</div>
|
|
1531
|
+
</Html>
|
|
1532
|
+
<Html center position={depthLabelPosition} style={{ pointerEvents: 'none' }}>
|
|
1533
|
+
<div
|
|
1534
|
+
style={{
|
|
1535
|
+
background: 'rgba(15, 23, 42, 0.86)',
|
|
1536
|
+
border: '1px solid rgba(15, 23, 42, 0.65)',
|
|
1537
|
+
borderRadius: '999px',
|
|
1538
|
+
color: '#f8fafc',
|
|
1539
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
1540
|
+
fontSize: '11px',
|
|
1541
|
+
fontWeight: 600,
|
|
1542
|
+
lineHeight: 1,
|
|
1543
|
+
padding: '4px 8px',
|
|
1544
|
+
pointerEvents: 'none',
|
|
1545
|
+
whiteSpace: 'nowrap',
|
|
1546
|
+
}}
|
|
1547
|
+
>
|
|
1548
|
+
{depthLabel}
|
|
1549
|
+
</div>
|
|
1550
|
+
</Html>
|
|
1551
|
+
<Html center position={heightLabelPosition} style={{ pointerEvents: 'none' }}>
|
|
1552
|
+
<div
|
|
1553
|
+
style={{
|
|
1554
|
+
background: 'rgba(15, 23, 42, 0.86)',
|
|
1555
|
+
border: '1px solid rgba(15, 23, 42, 0.65)',
|
|
1556
|
+
borderRadius: '999px',
|
|
1557
|
+
color: '#f8fafc',
|
|
1558
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
1559
|
+
fontSize: '11px',
|
|
1560
|
+
fontWeight: 600,
|
|
1561
|
+
lineHeight: 1,
|
|
1562
|
+
padding: '4px 8px',
|
|
1563
|
+
pointerEvents: 'none',
|
|
1564
|
+
whiteSpace: 'nowrap',
|
|
1565
|
+
}}
|
|
1566
|
+
>
|
|
1567
|
+
{heightLabel}
|
|
1568
|
+
</div>
|
|
1569
|
+
</Html>
|
|
1570
|
+
</>
|
|
1571
|
+
)
|
|
969
1572
|
|
|
970
1573
|
return (
|
|
971
1574
|
<group ref={cursorGroupRef}>
|
|
972
|
-
<lineSegments
|
|
973
|
-
|
|
974
|
-
|
|
1575
|
+
<lineSegments
|
|
1576
|
+
geometry={initialEdgeGeometry}
|
|
1577
|
+
layers={EDITOR_LAYER}
|
|
1578
|
+
material={edgeMaterial}
|
|
1579
|
+
ref={edgesRef}
|
|
1580
|
+
renderOrder={999}
|
|
1581
|
+
/>
|
|
1582
|
+
{measurementContent}
|
|
975
1583
|
<mesh
|
|
976
1584
|
geometry={basePlaneGeometry}
|
|
977
1585
|
layers={EDITOR_LAYER}
|