@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import { GROUP_ROTATE_DRAG_LABEL, ROTATE_HANDLE_DRAG_LABEL } from '../lib/contextual-help'
|
|
3
|
+
import useEditor, {
|
|
4
|
+
getActiveSnapContext,
|
|
5
|
+
isAngleSnapActive,
|
|
6
|
+
normalizePersistedEditorLayoutState,
|
|
7
|
+
} from './use-editor'
|
|
8
|
+
import useInteractionScope from './use-interaction-scope'
|
|
9
|
+
|
|
10
|
+
const initialEditor = useEditor.getState()
|
|
11
|
+
const initialScope = useInteractionScope.getState()
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
useEditor.setState(initialEditor)
|
|
14
|
+
useInteractionScope.setState(initialScope)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
for (const handle of [ROTATE_HANDLE_DRAG_LABEL, GROUP_ROTATE_DRAG_LABEL]) {
|
|
18
|
+
test(`${handle} reads and cycles the shared mode without changing placement modes`, () => {
|
|
19
|
+
useInteractionScope.getState().begin({ kind: 'handle-drag', nodeId: 'item_test', handle })
|
|
20
|
+
const placementModes = useEditor.getState().snappingModeByContext
|
|
21
|
+
expect(getActiveSnapContext()).toBe('rotation')
|
|
22
|
+
expect(isAngleSnapActive()).toBe(true)
|
|
23
|
+
expect(useEditor.getState().cycleSnappingMode()).toBe('off')
|
|
24
|
+
expect(isAngleSnapActive()).toBe(false)
|
|
25
|
+
expect(useEditor.getState().snappingModeByContext.wall).toBe(placementModes.wall)
|
|
26
|
+
expect(useEditor.getState().snappingModeByContext.item).toBe(placementModes.item)
|
|
27
|
+
expect(useEditor.getState().cycleSnappingMode()).toBe('angles')
|
|
28
|
+
expect(isAngleSnapActive()).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('older persisted preferences receive the rotation default and retain other modes', () => {
|
|
33
|
+
const merged = normalizePersistedEditorLayoutState({
|
|
34
|
+
snappingModeByContext: { wall: 'off', item: 'lines', polygon: 'grid' },
|
|
35
|
+
})
|
|
36
|
+
expect(merged?.snappingModeByContext).toEqual({
|
|
37
|
+
wall: 'off',
|
|
38
|
+
item: 'lines',
|
|
39
|
+
polygon: 'grid',
|
|
40
|
+
rotation: 'angles',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('persisted rotation mode is retained when valid and reset when invalid', () => {
|
|
45
|
+
expect(
|
|
46
|
+
normalizePersistedEditorLayoutState({ snappingModeByContext: { rotation: 'off' } })
|
|
47
|
+
.snappingModeByContext.rotation,
|
|
48
|
+
).toBe('off')
|
|
49
|
+
expect(
|
|
50
|
+
normalizePersistedEditorLayoutState({ snappingModeByContext: { rotation: 'grid' } })
|
|
51
|
+
.snappingModeByContext.rotation,
|
|
52
|
+
).toBe('angles')
|
|
53
|
+
})
|
package/src/store/use-editor.tsx
CHANGED
|
@@ -570,6 +570,7 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
570
570
|
wall: defaultSnappingModeFor('wall'),
|
|
571
571
|
item: defaultSnappingModeFor('item'),
|
|
572
572
|
polygon: defaultSnappingModeFor('polygon'),
|
|
573
|
+
rotation: defaultSnappingModeFor('rotation'),
|
|
573
574
|
},
|
|
574
575
|
continuationByContext: {
|
|
575
576
|
wall: CONTINUATION_PROFILES.wall.default,
|
|
@@ -800,8 +801,14 @@ function normalizeContinuationByContext(
|
|
|
800
801
|
}
|
|
801
802
|
}
|
|
802
803
|
|
|
803
|
-
function normalizePersistedEditorLayoutState(
|
|
804
|
-
state:
|
|
804
|
+
export function normalizePersistedEditorLayoutState(
|
|
805
|
+
state:
|
|
806
|
+
| (Omit<Partial<PersistedEditorLayoutState>, 'snappingModeByContext'> &
|
|
807
|
+
LegacyContinuationState & {
|
|
808
|
+
snappingModeByContext?: Partial<Record<SnapContext, unknown>>
|
|
809
|
+
})
|
|
810
|
+
| null
|
|
811
|
+
| undefined,
|
|
805
812
|
): PersistedEditorLayoutState {
|
|
806
813
|
return {
|
|
807
814
|
activeSidebarPanel:
|
|
@@ -821,6 +828,7 @@ function normalizePersistedEditorLayoutState(
|
|
|
821
828
|
wall: migrateSnappingMode(state?.snappingModeByContext?.wall, 'wall'),
|
|
822
829
|
item: migrateSnappingMode(state?.snappingModeByContext?.item, 'item'),
|
|
823
830
|
polygon: migrateSnappingMode(state?.snappingModeByContext?.polygon, 'polygon'),
|
|
831
|
+
rotation: migrateSnappingMode(state?.snappingModeByContext?.rotation, 'rotation'),
|
|
824
832
|
},
|
|
825
833
|
continuationByContext: normalizeContinuationByContext(state),
|
|
826
834
|
showReferenceFloor: state?.showReferenceFloor === true,
|
|
@@ -40,6 +40,9 @@ type FloorplanDraftPreviewState = {
|
|
|
40
40
|
wallDraftStart: WallPlanPoint | null
|
|
41
41
|
fenceDraftStart: WallPlanPoint | null
|
|
42
42
|
roofDraftStart: WallPlanPoint | null
|
|
43
|
+
/** First corner of an open rectangle-wall draft; the live cursor is the
|
|
44
|
+
* opposite corner, so the rectangle tool publishes only per click. */
|
|
45
|
+
wallRectangleDraftStart: WallPlanPoint | null
|
|
43
46
|
roofDraftQuarterTurn: boolean
|
|
44
47
|
polygonDraftType: FloorplanPolygonDraftType | null
|
|
45
48
|
polygonDraftPoints: WallPlanPoint[]
|
|
@@ -55,6 +58,7 @@ type FloorplanDraftPreviewState = {
|
|
|
55
58
|
setWallDraftStart(point: WallPlanPoint | null): void
|
|
56
59
|
setFenceDraftStart(point: WallPlanPoint | null): void
|
|
57
60
|
setRoofDraftStart(point: WallPlanPoint | null): void
|
|
61
|
+
setWallRectangleDraftStart(point: WallPlanPoint | null): void
|
|
58
62
|
setRoofDraftQuarterTurn(quarterTurn: boolean): void
|
|
59
63
|
setPolygonDraft(type: FloorplanPolygonDraftType | null, points: readonly WallPlanPoint[]): void
|
|
60
64
|
reset(): void
|
|
@@ -74,7 +78,8 @@ function setPlanPointField(
|
|
|
74
78
|
| 'roofDraftEnd'
|
|
75
79
|
| 'roofDraftStart'
|
|
76
80
|
| 'wallDraftEnd'
|
|
77
|
-
| 'wallDraftStart'
|
|
81
|
+
| 'wallDraftStart'
|
|
82
|
+
| 'wallRectangleDraftStart',
|
|
78
83
|
point: WallPlanPoint | null,
|
|
79
84
|
) {
|
|
80
85
|
return (
|
|
@@ -96,6 +101,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
96
101
|
wallDraftStart: null,
|
|
97
102
|
fenceDraftStart: null,
|
|
98
103
|
roofDraftStart: null,
|
|
104
|
+
wallRectangleDraftStart: null,
|
|
99
105
|
roofDraftQuarterTurn: false,
|
|
100
106
|
polygonDraftType: null,
|
|
101
107
|
polygonDraftPoints: [],
|
|
@@ -119,6 +125,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
119
125
|
setWallDraftStart: (point) => set(setPlanPointField('wallDraftStart', point)),
|
|
120
126
|
setFenceDraftStart: (point) => set(setPlanPointField('fenceDraftStart', point)),
|
|
121
127
|
setRoofDraftStart: (point) => set(setPlanPointField('roofDraftStart', point)),
|
|
128
|
+
setWallRectangleDraftStart: (point) => set(setPlanPointField('wallRectangleDraftStart', point)),
|
|
122
129
|
setRoofDraftQuarterTurn: (quarterTurn) =>
|
|
123
130
|
set((state) =>
|
|
124
131
|
state.roofDraftQuarterTurn === quarterTurn ? state : { roofDraftQuarterTurn: quarterTurn },
|
|
@@ -139,6 +146,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
139
146
|
state.wallDraftStart === null &&
|
|
140
147
|
state.fenceDraftStart === null &&
|
|
141
148
|
state.roofDraftStart === null &&
|
|
149
|
+
state.wallRectangleDraftStart === null &&
|
|
142
150
|
state.roofDraftQuarterTurn === false &&
|
|
143
151
|
state.polygonDraftType === null &&
|
|
144
152
|
state.polygonDraftPoints.length === 0
|
|
@@ -152,6 +160,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
152
160
|
wallDraftStart: null,
|
|
153
161
|
fenceDraftStart: null,
|
|
154
162
|
roofDraftStart: null,
|
|
163
|
+
wallRectangleDraftStart: null,
|
|
155
164
|
roofDraftQuarterTurn: false,
|
|
156
165
|
polygonDraftType: null,
|
|
157
166
|
polygonDraftPoints: [],
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
collectSubtree,
|
|
7
|
+
useLiveNodeOverrides,
|
|
8
|
+
useLiveTransforms,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
4
11
|
import {
|
|
5
12
|
beginPerfAction,
|
|
6
13
|
commitPerfAction,
|
|
@@ -10,6 +17,7 @@ import {
|
|
|
10
17
|
import { useRef } from 'react'
|
|
11
18
|
import { create } from 'zustand'
|
|
12
19
|
import { useShallow } from 'zustand/react/shallow'
|
|
20
|
+
import { discardFreshPlacementSubtree } from '../lib/fresh-planar-placement'
|
|
13
21
|
import {
|
|
14
22
|
type ActiveInteractionScope,
|
|
15
23
|
controlPointReshapeInfo,
|
|
@@ -25,6 +33,7 @@ import {
|
|
|
25
33
|
reshapingNodeId,
|
|
26
34
|
tangentReshapeInfo,
|
|
27
35
|
} from '../lib/interaction/scope'
|
|
36
|
+
import usePlacementPreview from './use-placement-preview'
|
|
28
37
|
|
|
29
38
|
// The authoritative interaction state machine. A single owner holds exactly one
|
|
30
39
|
// scope at a time. `begin` enters an interaction (atomically replacing any prior
|
|
@@ -33,8 +42,21 @@ import {
|
|
|
33
42
|
// the end of its interaction. There is no setter that can leave the store in an
|
|
34
43
|
// illegal half-state: the only writable shape is `InteractionScope`.
|
|
35
44
|
|
|
45
|
+
type SubtreeCreation = {
|
|
46
|
+
rootId: AnyNodeId
|
|
47
|
+
node: AnyNode
|
|
48
|
+
hydrationId: object | null
|
|
49
|
+
}
|
|
50
|
+
type OwnedSubtree = { creation: SubtreeCreation; gesture: object }
|
|
51
|
+
|
|
36
52
|
export type InteractionScopeState = {
|
|
37
53
|
scope: InteractionScope
|
|
54
|
+
gesture: object | null
|
|
55
|
+
pendingSubtree: SubtreeCreation | null
|
|
56
|
+
ownedSubtree: OwnedSubtree | null
|
|
57
|
+
noteSubtreeCreation: (node: AnyNode) => void
|
|
58
|
+
adoptSubtree: (rootId: AnyNodeId) => boolean
|
|
59
|
+
finishSubtree: (rootId: AnyNodeId) => void
|
|
38
60
|
// Enter an interaction. If one is already active it is ended first, so the
|
|
39
61
|
// store is always single-owner.
|
|
40
62
|
begin: (scope: ActiveInteractionScope) => void
|
|
@@ -101,9 +123,62 @@ function commitScopePerfAction(): void {
|
|
|
101
123
|
|
|
102
124
|
const useInteractionScope = create<InteractionScopeState>((set, get) => ({
|
|
103
125
|
scope: IDLE_SCOPE,
|
|
126
|
+
gesture: null,
|
|
127
|
+
pendingSubtree: null,
|
|
128
|
+
ownedSubtree: null,
|
|
129
|
+
noteSubtreeCreation: (node) =>
|
|
130
|
+
set({
|
|
131
|
+
pendingSubtree: { rootId: node.id, node, hydrationId: useScene.getState().hydrationId },
|
|
132
|
+
}),
|
|
133
|
+
adoptSubtree: (rootId) => {
|
|
134
|
+
const state = get()
|
|
135
|
+
if (state.ownedSubtree?.creation.rootId === rootId)
|
|
136
|
+
return (
|
|
137
|
+
state.ownedSubtree.gesture === state.gesture &&
|
|
138
|
+
isCurrentCreation(state.ownedSubtree.creation)
|
|
139
|
+
)
|
|
140
|
+
const creation = state.pendingSubtree
|
|
141
|
+
if (
|
|
142
|
+
!creation ||
|
|
143
|
+
creation.rootId !== rootId ||
|
|
144
|
+
!state.gesture ||
|
|
145
|
+
movingNodeOf(state.scope)?.id !== rootId ||
|
|
146
|
+
!isCurrentCreation(creation)
|
|
147
|
+
)
|
|
148
|
+
return false
|
|
149
|
+
set({ ownedSubtree: { creation, gesture: state.gesture }, pendingSubtree: null })
|
|
150
|
+
return true
|
|
151
|
+
},
|
|
152
|
+
finishSubtree: (rootId) =>
|
|
153
|
+
set((state) => {
|
|
154
|
+
if (state.ownedSubtree?.creation.rootId !== rootId && state.pendingSubtree?.rootId !== rootId)
|
|
155
|
+
return state
|
|
156
|
+
return {
|
|
157
|
+
ownedSubtree: state.ownedSubtree?.creation.rootId === rootId ? null : state.ownedSubtree,
|
|
158
|
+
pendingSubtree: state.pendingSubtree?.rootId === rootId ? null : state.pendingSubtree,
|
|
159
|
+
}
|
|
160
|
+
}),
|
|
104
161
|
begin: (scope) => {
|
|
162
|
+
const state = get()
|
|
163
|
+
const nodeId = movingNodeOf(scope)?.id
|
|
164
|
+
const creation = state.ownedSubtree?.creation ?? state.pendingSubtree
|
|
165
|
+
if (
|
|
166
|
+
nodeId &&
|
|
167
|
+
nodeId === movingNodeOf(state.scope)?.id &&
|
|
168
|
+
creation?.rootId === nodeId &&
|
|
169
|
+
isCurrentCreation(creation)
|
|
170
|
+
) {
|
|
171
|
+
set({ scope })
|
|
172
|
+
return
|
|
173
|
+
}
|
|
105
174
|
beginScopePerfAction(scope)
|
|
106
|
-
set({
|
|
175
|
+
set({
|
|
176
|
+
scope,
|
|
177
|
+
gesture: {},
|
|
178
|
+
ownedSubtree: null,
|
|
179
|
+
pendingSubtree:
|
|
180
|
+
get().pendingSubtree?.rootId === movingNodeOf(scope)?.id ? get().pendingSubtree : null,
|
|
181
|
+
})
|
|
107
182
|
},
|
|
108
183
|
update: (patch) =>
|
|
109
184
|
set((state) => {
|
|
@@ -114,18 +189,92 @@ const useInteractionScope = create<InteractionScopeState>((set, get) => ({
|
|
|
114
189
|
end: () => {
|
|
115
190
|
if (get().scope.kind === 'idle') return
|
|
116
191
|
commitScopePerfAction()
|
|
117
|
-
set({ scope: IDLE_SCOPE })
|
|
192
|
+
set({ scope: IDLE_SCOPE, gesture: null, ownedSubtree: null, pendingSubtree: null })
|
|
118
193
|
},
|
|
119
194
|
endIf: (match) => {
|
|
120
195
|
const scope = get().scope
|
|
121
196
|
if (scope.kind === 'idle') return
|
|
122
197
|
if (match(scope)) {
|
|
123
198
|
commitScopePerfAction()
|
|
124
|
-
set({ scope: IDLE_SCOPE })
|
|
199
|
+
set({ scope: IDLE_SCOPE, gesture: null, ownedSubtree: null, pendingSubtree: null })
|
|
125
200
|
}
|
|
126
201
|
},
|
|
127
202
|
}))
|
|
128
203
|
|
|
204
|
+
function isCurrentCreation(creation: SubtreeCreation): boolean {
|
|
205
|
+
const scene = useScene.getState()
|
|
206
|
+
return (
|
|
207
|
+
scene.hydrationId === creation.hydrationId && scene.nodes[creation.rootId] === creation.node
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function isInteractionSubtreeDraft(
|
|
212
|
+
rootId = movingNodeOf(useInteractionScope.getState().scope)?.id,
|
|
213
|
+
): boolean {
|
|
214
|
+
const state = useInteractionScope.getState()
|
|
215
|
+
const creation = state.ownedSubtree?.creation ?? state.pendingSubtree
|
|
216
|
+
return Boolean(
|
|
217
|
+
creation &&
|
|
218
|
+
state.gesture &&
|
|
219
|
+
rootId === movingNodeOf(state.scope)?.id &&
|
|
220
|
+
creation.rootId === rootId &&
|
|
221
|
+
(!state.ownedSubtree || state.ownedSubtree.gesture === state.gesture) &&
|
|
222
|
+
isCurrentCreation(creation),
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Track the identity of the factory-created node through scene patches, never through metadata.
|
|
227
|
+
// Hydration or deletion invalidates the creation even if a later scene reuses the same id.
|
|
228
|
+
useScene.subscribe((scene, previous) => {
|
|
229
|
+
const state = useInteractionScope.getState()
|
|
230
|
+
for (const creation of [state.pendingSubtree, state.ownedSubtree?.creation]) {
|
|
231
|
+
if (!creation) continue
|
|
232
|
+
if (
|
|
233
|
+
scene.hydrationId !== creation.hydrationId ||
|
|
234
|
+
previous.nodes[creation.rootId] !== creation.node ||
|
|
235
|
+
!scene.nodes[creation.rootId]
|
|
236
|
+
) {
|
|
237
|
+
state.finishSubtree(creation.rootId)
|
|
238
|
+
} else creation.node = scene.nodes[creation.rootId]!
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// Factory drafts belong to their gesture even before a mover mounts to adopt them.
|
|
243
|
+
useInteractionScope.subscribe((state, previous) => {
|
|
244
|
+
if (
|
|
245
|
+
state.gesture === previous.gesture &&
|
|
246
|
+
movingNodeOf(state.scope)?.id === movingNodeOf(previous.scope)?.id
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
const owned =
|
|
250
|
+
previous.ownedSubtree ??
|
|
251
|
+
(previous.pendingSubtree && previous.gesture
|
|
252
|
+
? { creation: previous.pendingSubtree, gesture: previous.gesture }
|
|
253
|
+
: null)
|
|
254
|
+
if (
|
|
255
|
+
!owned ||
|
|
256
|
+
owned.gesture !== previous.gesture ||
|
|
257
|
+
movingNodeOf(previous.scope)?.id !== owned.creation.rootId ||
|
|
258
|
+
!isCurrentCreation(owned.creation)
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
const node = owned.creation.node
|
|
262
|
+
const subtree = collectSubtree(useScene.getState().nodes, node.id)
|
|
263
|
+
const temporal = useScene.temporal.getState()
|
|
264
|
+
const wasTracking = temporal.isTracking
|
|
265
|
+
temporal.pause()
|
|
266
|
+
try {
|
|
267
|
+
discardFreshPlacementSubtree(node.id)
|
|
268
|
+
for (const entry of subtree ? [subtree.root, ...subtree.descendants] : []) {
|
|
269
|
+
useLiveTransforms.getState().clear(entry.id)
|
|
270
|
+
useLiveNodeOverrides.getState().clear(entry.id)
|
|
271
|
+
}
|
|
272
|
+
if (usePlacementPreview.getState().node?.id === node.id) usePlacementPreview.getState().clear()
|
|
273
|
+
} finally {
|
|
274
|
+
if (wasTracking) temporal.resume()
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
129
278
|
// Derived, reference-stable views of the active scope, replacing the legacy
|
|
130
279
|
// `useEditor.activeHandleDrag` / `useEditor.editingHole` flags. `useShallow`
|
|
131
280
|
// keeps the result reference-stable across unrelated scope changes, so hot-path
|
|
@@ -190,8 +339,8 @@ export const useReshapingNode = (): AnyNode | null => {
|
|
|
190
339
|
// The node currently being placed or moved. Replaces the legacy
|
|
191
340
|
// `useEditor.movingNode` flag. Unlike `useReshapingNode`, no `useRef` snapshot is
|
|
192
341
|
// needed: the node is carried inline in the scope and set once at `begin`, so it
|
|
193
|
-
//
|
|
194
|
-
//
|
|
342
|
+
// remains stable until a caller explicitly re-arms the same interaction.
|
|
343
|
+
// Returns null whenever no placing/moving interaction is active.
|
|
195
344
|
export const useMovingNode = (): AnyNode | null => useInteractionScope((s) => movingNodeOf(s.scope))
|
|
196
345
|
|
|
197
346
|
// Imperative (non-React) read for event handlers / effects.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
/** Progress of a "Send to Blender" hand-off. Lives outside the settings panel so
|
|
4
|
+
* switching tabs mid-send neither drops the lock nor loses the outcome. */
|
|
5
|
+
export type SendToAppStep = 'probing' | 'exporting' | 'sending' | 'importing'
|
|
6
|
+
|
|
7
|
+
export type SendToAppMessage = { tone: 'info' | 'error'; text: string }
|
|
8
|
+
|
|
9
|
+
type SendToAppState = {
|
|
10
|
+
step: SendToAppStep | null
|
|
11
|
+
message: SendToAppMessage | null
|
|
12
|
+
setStep: (step: SendToAppStep | null) => void
|
|
13
|
+
setMessage: (message: SendToAppMessage | null) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useSendToApp = create<SendToAppState>((set) => ({
|
|
17
|
+
step: null,
|
|
18
|
+
message: null,
|
|
19
|
+
setStep: (step) => set({ step }),
|
|
20
|
+
setMessage: (message) => set({ message }),
|
|
21
|
+
}))
|