@pascal-app/editor 1.0.1 → 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 +10 -5
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floorplan-panel.tsx +210 -163
- 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/use-handle-drag.ts +75 -6
- package/src/components/editor/index.tsx +24 -9
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +141 -7
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- 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-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 +67 -21
- 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 +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-strategies.ts +55 -43
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.ts +41 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +166 -73
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +23 -9
- 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 +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- 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/slider-control.tsx +76 -31
- 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/parametric-inspector.tsx +3 -15
- 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 +585 -369
- 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.tsx +1 -1
- 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-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 +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- 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 +42 -29
- package/src/index.tsx +63 -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/floorplan/floorplan-export.test.ts +3 -1
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/fresh-planar-placement.ts +168 -36
- package/src/lib/glb-export.test.ts +59 -16
- package/src/lib/glb-export.ts +26 -13
- 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/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -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/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- 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/terrain-panel-model.ts +117 -0
- 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-interaction-scope.ts +155 -6
|
@@ -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.
|