@pascal-app/editor 1.0.0 → 1.0.3
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 +12 -6
- package/src/components/editor/bake-exporter.tsx +4 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floating-action-menu.tsx +14 -1
- package/src/components/editor/floorplan-panel.tsx +233 -166
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +5 -9
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +13 -2
- package/src/components/editor/handles/use-handle-drag.ts +80 -8
- package/src/components/editor/index.tsx +36 -13
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +162 -69
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- package/src/components/editor/selection-manager.tsx +49 -0
- package/src/components/editor/thumbnail-generator.tsx +11 -2
- package/src/components/editor/wall-move-side-handles.tsx +71 -21
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
- package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +113 -24
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/zone/zone-system.tsx +8 -4
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +169 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +204 -0
- package/src/components/tools/item/placement-strategies.ts +212 -200
- package/src/components/tools/item/placement-types.ts +4 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/use-draft-node.ts +136 -47
- package/src/components/tools/item/use-placement-coordinator.tsx +391 -157
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +509 -0
- package/src/components/tools/registry/item-surface-move.ts +463 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +237 -78
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +27 -13
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +107 -0
- package/src/components/tools/shared/surface-rejection.tsx +43 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/command-palette/editor-commands.tsx +54 -1
- package/src/components/ui/controls/material-paint-panel.tsx +6 -67
- package/src/components/ui/controls/material-picker.tsx +5 -67
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/slider-control.tsx +87 -35
- package/src/components/ui/floating-level-selector.tsx +81 -5
- package/src/components/ui/helpers/helper-manager.tsx +24 -6
- package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +6 -3
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +9 -17
- package/src/components/ui/panels/reference-panel.tsx +3 -3
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +669 -336
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +30 -11
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +106 -4
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +6 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +1 -12
- package/src/components/viewer/viewer-scene-header.tsx +3 -0
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.integration.test.tsx +197 -0
- package/src/hooks/use-auto-save.ts +21 -6
- package/src/hooks/use-ceiling-events.test.ts +25 -2
- package/src/hooks/use-ceiling-events.ts +7 -1
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +7 -0
- package/src/hooks/use-keyboard.ts +43 -28
- package/src/index.tsx +68 -3
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floorplan/floorplan-export.test.ts +142 -2
- package/src/lib/floorplan/floorplan-export.tsx +9 -3
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/floorplan/floorplan-readonly.ts +4 -0
- package/src/lib/fresh-planar-placement.ts +173 -33
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +265 -16
- package/src/lib/glb-export.ts +197 -37
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/interaction/scope.ts +9 -1
- package/src/lib/keyboard-pan.test.ts +62 -0
- package/src/lib/keyboard-pan.ts +89 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/panel-rows.ts +75 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/portable-export.test.ts +89 -0
- package/src/lib/portable-export.ts +84 -11
- package/src/lib/print-shell-compiler-manifold-core.ts +59 -10
- package/src/lib/print-shell-compiler-manifold-worker.ts +14 -1
- package/src/lib/print-shell-compiler-manifold.worker.ts +1 -1
- package/src/lib/print-shell-compiler-protocol.ts +8 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/snapping-mode.test.ts +13 -2
- package/src/lib/snapping-mode.ts +11 -6
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +43 -16
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/use-editor.tsx +10 -2
- package/src/store/use-floorplan-draft-preview.ts +10 -1
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-send-to-app.ts +21 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type Cursor,
|
|
7
|
+
cascadeDirty,
|
|
7
8
|
createSceneApi,
|
|
8
9
|
type HandleDragModifiers,
|
|
9
10
|
runAsSingleSceneHistoryStep,
|
|
@@ -16,6 +17,8 @@ import { useEffect, useRef } from 'react'
|
|
|
16
17
|
import { type Camera, type Object3D, type Plane, type Ray, Vector2, type Vector3 } from 'three'
|
|
17
18
|
import { isHistoryShortcut } from '../../../lib/history'
|
|
18
19
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
20
|
+
import { intersectSpatialDragPlane } from '../../../lib/spatial-drag-plane'
|
|
21
|
+
import { getSpatialPointerId, spatialPointerInput } from '../../../lib/spatial-pointer-input'
|
|
19
22
|
import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
|
|
20
23
|
import { commitHandleDragPatch } from './handle-drag-history'
|
|
21
24
|
|
|
@@ -57,6 +60,7 @@ type HandleDragSession = {
|
|
|
57
60
|
commit?: (patch: Partial<AnyNode>) => void
|
|
58
61
|
markDirty?: boolean
|
|
59
62
|
onBegin?: () => void
|
|
63
|
+
onCancel?: () => void
|
|
60
64
|
onEnd?: () => void
|
|
61
65
|
overrideId?: AnyNodeId
|
|
62
66
|
}
|
|
@@ -106,6 +110,7 @@ function suppressInputDraggingUntilPointerRelease(pointerId: number) {
|
|
|
106
110
|
window.addEventListener('pointerup', restore)
|
|
107
111
|
window.addEventListener('pointercancel', restore)
|
|
108
112
|
window.addEventListener('blur', onBlur)
|
|
113
|
+
return () => restore()
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
export function useHandleDrag(args: UseHandleDragArgs) {
|
|
@@ -120,9 +125,17 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
120
125
|
if (event.button !== 0) return
|
|
121
126
|
event.stopPropagation()
|
|
122
127
|
suppressBoxSelectForPointer(event)
|
|
128
|
+
const spatialPointerId = getSpatialPointerId(event.nativeEvent)
|
|
123
129
|
|
|
124
130
|
if (args.kind === 'tap') {
|
|
125
|
-
suppressInputDraggingUntilPointerRelease(event.
|
|
131
|
+
const restoreInputDragging = suppressInputDraggingUntilPointerRelease(event.pointerId)
|
|
132
|
+
if (spatialPointerId) {
|
|
133
|
+
spatialPointerInput.capture(spatialPointerId, {
|
|
134
|
+
onMove: () => undefined,
|
|
135
|
+
onRelease: restoreInputDragging,
|
|
136
|
+
onCancel: restoreInputDragging,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
126
139
|
swallowNextClick()
|
|
127
140
|
sfxEmitter.emit('sfx:item-pick')
|
|
128
141
|
document.body.style.cursor = ''
|
|
@@ -132,6 +145,7 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
132
145
|
|
|
133
146
|
const { cursor, dragControls, handleIndex, node, rideObject, setIsDragging } = args
|
|
134
147
|
rideObject.updateMatrixWorld()
|
|
148
|
+
const spatialRay = spatialPointerId ? event.ray.clone() : null
|
|
135
149
|
|
|
136
150
|
const ndc = new Vector2()
|
|
137
151
|
const setPointerRay = (clientX: number, clientY: number) => {
|
|
@@ -143,10 +157,12 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
143
157
|
raycaster.setFromCamera(ndc, camera)
|
|
144
158
|
}
|
|
145
159
|
const getPointerRay: GetPointerRay = (clientX, clientY, target) => {
|
|
160
|
+
if (spatialRay) return target.copy(spatialRay)
|
|
146
161
|
setPointerRay(clientX, clientY)
|
|
147
162
|
return target.copy(raycaster.ray)
|
|
148
163
|
}
|
|
149
164
|
const intersectPlane: IntersectPlane = (clientX, clientY, plane, target) => {
|
|
165
|
+
if (spatialRay) return intersectSpatialDragPlane(spatialRay, plane, target)
|
|
150
166
|
setPointerRay(clientX, clientY)
|
|
151
167
|
return raycaster.ray.intersectPlane(plane, target)
|
|
152
168
|
}
|
|
@@ -179,7 +195,11 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
179
195
|
|
|
180
196
|
let lastPatch: Partial<AnyNode> | null = null
|
|
181
197
|
let historyPaused = true
|
|
198
|
+
let releaseSpatialCapture: (() => void) | null = null
|
|
182
199
|
let altKey = event.nativeEvent.altKey
|
|
200
|
+
let shiftKey = event.nativeEvent.shiftKey
|
|
201
|
+
let pendingMoveEvent: PointerEvent | null = null
|
|
202
|
+
let moveFrame = 0
|
|
183
203
|
|
|
184
204
|
const resumeHistory = () => {
|
|
185
205
|
if (!historyPaused) return
|
|
@@ -187,10 +207,10 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
187
207
|
useScene.temporal.getState().resume()
|
|
188
208
|
}
|
|
189
209
|
|
|
190
|
-
const
|
|
210
|
+
const processMove = (moveEvent: PointerEvent) => {
|
|
191
211
|
const patch = session.move({
|
|
192
212
|
event: moveEvent,
|
|
193
|
-
modifiers: { altKey },
|
|
213
|
+
modifiers: { altKey, shiftKey },
|
|
194
214
|
getPointerRay,
|
|
195
215
|
intersectPlane,
|
|
196
216
|
})
|
|
@@ -198,16 +218,40 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
198
218
|
lastPatch = patch
|
|
199
219
|
useLiveNodeOverrides.getState().set(overrideId, patch as Record<string, unknown>)
|
|
200
220
|
if (markDirty) {
|
|
201
|
-
|
|
221
|
+
for (const id of cascadeDirty(overrideId, { scene: createSceneApi(useScene) }))
|
|
222
|
+
useScene.getState().markDirty(id)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Coalesce high-frequency pointer events so live React/store updates happen
|
|
227
|
+
// at most once per animation frame while retaining the newest position.
|
|
228
|
+
const flushMove = () => {
|
|
229
|
+
moveFrame = 0
|
|
230
|
+
const moveEvent = pendingMoveEvent
|
|
231
|
+
pendingMoveEvent = null
|
|
232
|
+
if (moveEvent) processMove(moveEvent)
|
|
233
|
+
}
|
|
234
|
+
const onMove = (moveEvent: PointerEvent) => {
|
|
235
|
+
pendingMoveEvent = moveEvent
|
|
236
|
+
if (moveFrame !== 0) return
|
|
237
|
+
if (typeof window.requestAnimationFrame === 'function') {
|
|
238
|
+
moveFrame = window.requestAnimationFrame(flushMove)
|
|
239
|
+
} else {
|
|
240
|
+
flushMove()
|
|
202
241
|
}
|
|
203
242
|
}
|
|
204
243
|
|
|
205
244
|
const cleanup = () => {
|
|
245
|
+
if (moveFrame !== 0) window.cancelAnimationFrame?.(moveFrame)
|
|
246
|
+
moveFrame = 0
|
|
247
|
+
pendingMoveEvent = null
|
|
206
248
|
window.removeEventListener('pointermove', onMove)
|
|
207
249
|
window.removeEventListener('pointerup', onUp)
|
|
208
250
|
window.removeEventListener('pointercancel', onCancel)
|
|
209
251
|
window.removeEventListener('keydown', onKeyDown, true)
|
|
210
252
|
window.removeEventListener('keyup', onKeyUp, true)
|
|
253
|
+
releaseSpatialCapture?.()
|
|
254
|
+
releaseSpatialCapture = null
|
|
211
255
|
if (document.body.style.cursor === cursor) {
|
|
212
256
|
document.body.style.cursor = ''
|
|
213
257
|
}
|
|
@@ -222,11 +266,14 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
222
266
|
const clearOverride = () => {
|
|
223
267
|
useLiveNodeOverrides.getState().clear(overrideId)
|
|
224
268
|
if (markDirty) {
|
|
225
|
-
|
|
269
|
+
for (const id of cascadeDirty(overrideId, { scene: createSceneApi(useScene) }))
|
|
270
|
+
useScene.getState().markDirty(id)
|
|
226
271
|
}
|
|
227
272
|
}
|
|
228
273
|
|
|
229
274
|
const onUp = () => {
|
|
275
|
+
if (moveFrame !== 0) window.cancelAnimationFrame?.(moveFrame)
|
|
276
|
+
flushMove()
|
|
230
277
|
swallowNextClick()
|
|
231
278
|
sfxEmitter.emit('sfx:item-place')
|
|
232
279
|
if (lastPatch) {
|
|
@@ -242,6 +289,7 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
242
289
|
}
|
|
243
290
|
|
|
244
291
|
const onCancel = () => {
|
|
292
|
+
session.onCancel?.()
|
|
245
293
|
clearOverride()
|
|
246
294
|
cleanup()
|
|
247
295
|
}
|
|
@@ -253,6 +301,10 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
253
301
|
altKey = true
|
|
254
302
|
return
|
|
255
303
|
}
|
|
304
|
+
if (e.key === 'Shift') {
|
|
305
|
+
shiftKey = true
|
|
306
|
+
return
|
|
307
|
+
}
|
|
256
308
|
if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
|
|
257
309
|
e.preventDefault()
|
|
258
310
|
e.stopPropagation()
|
|
@@ -261,12 +313,32 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
261
313
|
}
|
|
262
314
|
const onKeyUp = (e: KeyboardEvent) => {
|
|
263
315
|
if (e.key === 'Alt') altKey = false
|
|
316
|
+
if (e.key === 'Shift') shiftKey = false
|
|
264
317
|
}
|
|
265
318
|
|
|
266
319
|
dragCleanupRef.current = onCancel
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
320
|
+
if (spatialPointerId && spatialRay) {
|
|
321
|
+
releaseSpatialCapture = spatialPointerInput.capture(spatialPointerId, {
|
|
322
|
+
onMove: (ray) => {
|
|
323
|
+
spatialRay.copy(ray)
|
|
324
|
+
processMove(
|
|
325
|
+
new PointerEvent('pointermove', {
|
|
326
|
+
button: 0,
|
|
327
|
+
buttons: 1,
|
|
328
|
+
altKey,
|
|
329
|
+
pointerId: event.pointerId,
|
|
330
|
+
pointerType: 'xr',
|
|
331
|
+
}),
|
|
332
|
+
)
|
|
333
|
+
},
|
|
334
|
+
onRelease: onUp,
|
|
335
|
+
onCancel,
|
|
336
|
+
})
|
|
337
|
+
} else {
|
|
338
|
+
window.addEventListener('pointermove', onMove)
|
|
339
|
+
window.addEventListener('pointerup', onUp)
|
|
340
|
+
window.addEventListener('pointercancel', onCancel)
|
|
341
|
+
}
|
|
270
342
|
window.addEventListener('keydown', onKeyDown, true)
|
|
271
343
|
window.addEventListener('keyup', onKeyUp, true)
|
|
272
344
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
SceneEnvironment,
|
|
20
20
|
useViewer,
|
|
21
21
|
Viewer,
|
|
22
|
+
type ViewerImmersiveSession,
|
|
22
23
|
ViewerPresentations,
|
|
23
24
|
} from '@pascal-app/viewer'
|
|
24
25
|
import {
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
writePersistedSelection,
|
|
50
51
|
} from '../../lib/scene'
|
|
51
52
|
import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
|
|
53
|
+
import { useUnitFocusRules } from '../../lib/units'
|
|
52
54
|
import { type CameraHintAction, useCameraHintFocus } from '../../store/use-camera-hint-focus'
|
|
53
55
|
import useEditor from '../../store/use-editor'
|
|
54
56
|
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
@@ -201,6 +203,7 @@ export interface EditorProps {
|
|
|
201
203
|
projectId?: string | null
|
|
202
204
|
|
|
203
205
|
// Persistence — defaults to localStorage when omitted
|
|
206
|
+
guardAgainstSceneWipe?: boolean
|
|
204
207
|
onLoad?: () => Promise<SceneGraph | null>
|
|
205
208
|
onSave?: (scene: SceneGraph, options?: { keepalive?: boolean }) => Promise<void>
|
|
206
209
|
/**
|
|
@@ -233,6 +236,9 @@ export interface EditorProps {
|
|
|
233
236
|
*/
|
|
234
237
|
disablePostFx?: boolean
|
|
235
238
|
|
|
239
|
+
/** Host-provided immersive XR runtime for the main 3D canvas. */
|
|
240
|
+
immersive?: ViewerImmersiveSession
|
|
241
|
+
|
|
236
242
|
// Version preview overlays (rendered by host app)
|
|
237
243
|
sidebarOverlay?: ReactNode
|
|
238
244
|
viewerBanner?: ReactNode
|
|
@@ -782,6 +788,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
782
788
|
isVersionPreviewMode,
|
|
783
789
|
isLoading,
|
|
784
790
|
isFirstPersonMode,
|
|
791
|
+
isXRMode,
|
|
785
792
|
isStudioMode,
|
|
786
793
|
onThumbnailCapture,
|
|
787
794
|
viewerSceneSlot,
|
|
@@ -790,6 +797,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
790
797
|
isVersionPreviewMode: boolean
|
|
791
798
|
isLoading: boolean
|
|
792
799
|
isFirstPersonMode: boolean
|
|
800
|
+
isXRMode: boolean
|
|
793
801
|
isStudioMode: boolean
|
|
794
802
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
795
803
|
viewerSceneSlot?: ReactNode
|
|
@@ -806,7 +814,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
806
814
|
<>
|
|
807
815
|
<SceneEnvironment />
|
|
808
816
|
{!(isFirstPersonMode || isStudioMode || isCaptureMode) && <SelectionManager />}
|
|
809
|
-
{!noEditing && <BoxSelectTool />}
|
|
817
|
+
{!(noEditing || isXRMode) && <BoxSelectTool />}
|
|
810
818
|
{!noEditing && <NodeArrowHandles />}
|
|
811
819
|
{!noEditing && <GroupRotateHandle />}
|
|
812
820
|
{!noEditing && <GroupSelectionBox3D />}
|
|
@@ -814,10 +822,10 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
814
822
|
{!noEditing && <SlabHoleHighlights />}
|
|
815
823
|
{!noEditing && <WallMoveSideHandles />}
|
|
816
824
|
{!noEditing && <FenceTangentLines3D />}
|
|
817
|
-
{!noEditing && <FloatingActionMenu />}
|
|
818
|
-
{!noEditing && <GroupFloatingActionMenu />}
|
|
819
|
-
{!noEditing && <FloatingBuildingActionMenu />}
|
|
820
|
-
{!isFirstPersonMode && <WallMeasurementLabel />}
|
|
825
|
+
{!(noEditing || isXRMode) && <FloatingActionMenu />}
|
|
826
|
+
{!(noEditing || isXRMode) && <GroupFloatingActionMenu />}
|
|
827
|
+
{!(noEditing || isXRMode) && <FloatingBuildingActionMenu />}
|
|
828
|
+
{!(isFirstPersonMode || isXRMode) && <WallMeasurementLabel />}
|
|
821
829
|
<ExportManager />
|
|
822
830
|
{isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
|
823
831
|
<CeilingSystem />
|
|
@@ -828,10 +836,10 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
828
836
|
{!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
|
|
829
837
|
{!(isLoading || noEditing) && <ToolManager />}
|
|
830
838
|
{isFirstPersonMode && <FirstPersonControls />}
|
|
831
|
-
{isCaptureMode && <CaptureCameraRig />}
|
|
832
|
-
<CustomCameraControls />
|
|
833
|
-
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
834
|
-
{!isFirstPersonMode && <SiteEdgeLabels />}
|
|
839
|
+
{isCaptureMode && !isXRMode && <CaptureCameraRig />}
|
|
840
|
+
{!isXRMode && <CustomCameraControls />}
|
|
841
|
+
{!isXRMode && <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />}
|
|
842
|
+
{!(isFirstPersonMode || isXRMode) && <SiteEdgeLabels />}
|
|
835
843
|
<InteractiveSystem />
|
|
836
844
|
{presentationsReady ? <ViewerPresentations /> : null}
|
|
837
845
|
{!noEditing && viewerSceneSlot}
|
|
@@ -1021,6 +1029,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1021
1029
|
viewerSceneSlot,
|
|
1022
1030
|
floorplanSceneSlot,
|
|
1023
1031
|
disablePostFx = false,
|
|
1032
|
+
immersive,
|
|
1024
1033
|
}: {
|
|
1025
1034
|
isVersionPreviewMode: boolean
|
|
1026
1035
|
isLoading: boolean
|
|
@@ -1035,12 +1044,17 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1035
1044
|
viewerSceneSlot?: ReactNode
|
|
1036
1045
|
floorplanSceneSlot?: ReactNode
|
|
1037
1046
|
disablePostFx?: boolean
|
|
1047
|
+
immersive?: ViewerImmersiveSession
|
|
1038
1048
|
}) {
|
|
1039
1049
|
const viewMode = useEditor((s) => s.viewMode)
|
|
1040
1050
|
const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
|
|
1041
1051
|
const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
|
|
1042
1052
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
1043
1053
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
1054
|
+
useUnitFocusRules()
|
|
1055
|
+
const presetIsolation = useEditor((s) =>
|
|
1056
|
+
s.captureMode.mode === 'preset' ? s.captureMode.isolated : null,
|
|
1057
|
+
)
|
|
1044
1058
|
|
|
1045
1059
|
const [isCameraControlsHintVisible, setIsCameraControlsHintVisible] = useState<boolean | null>(
|
|
1046
1060
|
null,
|
|
@@ -1151,10 +1165,15 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1151
1165
|
defaultRender={EDITOR_DEFAULT_RENDER}
|
|
1152
1166
|
disablePostFx={disablePostFx}
|
|
1153
1167
|
hoverStyles={EDITOR_HOVER_STYLES}
|
|
1168
|
+
isolate={presetIsolation}
|
|
1169
|
+
// Preset captures isolate one subtree and keep the exterior transparent.
|
|
1170
|
+
// Other modes retain the viewer's configured background policy.
|
|
1171
|
+
transparent={presetIsolation === null ? undefined : true}
|
|
1154
1172
|
onSceneReadyChange={onSceneReadyChange}
|
|
1155
1173
|
renderContext="editor"
|
|
1156
1174
|
renderPaused={!show3d && !showLoader}
|
|
1157
1175
|
sceneReadyKey={sceneReadyKey}
|
|
1176
|
+
immersive={immersive}
|
|
1158
1177
|
// Walk/drone framing during snapshot capture is camera-only: the
|
|
1159
1178
|
// viewer's default selection manager would hover-highlight whatever
|
|
1160
1179
|
// the cursor crosses, which orbit capture never does.
|
|
@@ -1163,6 +1182,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1163
1182
|
<ViewerSceneContent
|
|
1164
1183
|
isFirstPersonMode={isFirstPersonMode}
|
|
1165
1184
|
isLoading={showLoader}
|
|
1185
|
+
isXRMode={immersive != null}
|
|
1166
1186
|
isStudioMode={isStudioMode}
|
|
1167
1187
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
1168
1188
|
onThumbnailCapture={onThumbnailCapture}
|
|
@@ -1232,6 +1252,7 @@ function PreviewStage({
|
|
|
1232
1252
|
}
|
|
1233
1253
|
|
|
1234
1254
|
function EditorContent({
|
|
1255
|
+
guardAgainstSceneWipe,
|
|
1235
1256
|
layoutVersion = 'v1',
|
|
1236
1257
|
appMenuButton,
|
|
1237
1258
|
sidebarTop,
|
|
@@ -1256,6 +1277,7 @@ function EditorContent({
|
|
|
1256
1277
|
onLoaderChange,
|
|
1257
1278
|
onThumbnailCapture,
|
|
1258
1279
|
disablePostFx = false,
|
|
1280
|
+
immersive,
|
|
1259
1281
|
sidebarOverlay,
|
|
1260
1282
|
viewerBanner,
|
|
1261
1283
|
settingsPanelProps,
|
|
@@ -1291,6 +1313,7 @@ function EditorContent({
|
|
|
1291
1313
|
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
|
|
1292
1314
|
|
|
1293
1315
|
const { isLoadingSceneRef, saveNow } = useAutoSave({
|
|
1316
|
+
guardAgainstSceneWipe,
|
|
1294
1317
|
onSave,
|
|
1295
1318
|
onDirty,
|
|
1296
1319
|
onSaveStatusChange,
|
|
@@ -1347,10 +1370,9 @@ function EditorContent({
|
|
|
1347
1370
|
|
|
1348
1371
|
// Load on mount, project switches, and explicit retry attempts.
|
|
1349
1372
|
useEffect(() => {
|
|
1350
|
-
void sceneLoadAttempt
|
|
1351
1373
|
let cancelled = false
|
|
1352
1374
|
|
|
1353
|
-
async function load() {
|
|
1375
|
+
async function load(attempt: number) {
|
|
1354
1376
|
isLoadingSceneRef.current = true
|
|
1355
1377
|
setSceneLoadError(null)
|
|
1356
1378
|
setHasLoadedInitialScene(false)
|
|
@@ -1364,7 +1386,7 @@ function EditorContent({
|
|
|
1364
1386
|
let failed = false
|
|
1365
1387
|
try {
|
|
1366
1388
|
const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
|
|
1367
|
-
if (!cancelled) {
|
|
1389
|
+
if (!cancelled && attempt === sceneLoadAttempt) {
|
|
1368
1390
|
applySceneGraphToEditor(sceneGraph)
|
|
1369
1391
|
setIsViewerSceneReady(false)
|
|
1370
1392
|
setSceneReadyKey((key) => key + 1)
|
|
@@ -1390,7 +1412,7 @@ function EditorContent({
|
|
|
1390
1412
|
}
|
|
1391
1413
|
}
|
|
1392
1414
|
|
|
1393
|
-
load()
|
|
1415
|
+
load(sceneLoadAttempt)
|
|
1394
1416
|
|
|
1395
1417
|
return () => {
|
|
1396
1418
|
cancelled = true
|
|
@@ -1534,6 +1556,7 @@ function EditorContent({
|
|
|
1534
1556
|
showLoader={showLoader}
|
|
1535
1557
|
viewerSceneSlot={viewerSceneSlot}
|
|
1536
1558
|
floorplanSceneSlot={floorplanSceneSlot}
|
|
1559
|
+
immersive={immersive}
|
|
1537
1560
|
/>
|
|
1538
1561
|
)
|
|
1539
1562
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
4
|
import { Copy, Group, Move, PencilRuler, Search, Spline, Trash2, Ungroup } from 'lucide-react'
|
|
5
5
|
import type { MouseEventHandler, PointerEventHandler } from 'react'
|
|
6
|
+
import { RegistryActionContributions } from './registry-action-contributions'
|
|
6
7
|
|
|
7
8
|
type NodeActionMenuProps = {
|
|
8
9
|
onFind?: MouseEventHandler<HTMLButtonElement>
|
|
@@ -111,6 +112,7 @@ export function NodeActionMenu({
|
|
|
111
112
|
<Spline className="h-4 w-4" />
|
|
112
113
|
</button>
|
|
113
114
|
)}
|
|
115
|
+
<RegistryActionContributions />
|
|
114
116
|
{onDuplicate && (
|
|
115
117
|
<button
|
|
116
118
|
aria-label="Duplicate"
|