@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
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
CeilingEvent,
|
|
5
5
|
CeilingNode,
|
|
6
6
|
GridEvent,
|
|
7
|
-
ItemNode,
|
|
8
7
|
NodeEvent,
|
|
9
8
|
RoofEvent,
|
|
10
9
|
RoofNode,
|
|
@@ -17,6 +16,7 @@ import type {
|
|
|
17
16
|
} from '@pascal-app/core'
|
|
18
17
|
import {
|
|
19
18
|
clampRectToRoofWallFace,
|
|
19
|
+
clearFaceHostItemFields,
|
|
20
20
|
createSceneApi,
|
|
21
21
|
getRoofSegmentWallFace,
|
|
22
22
|
getScaledDimensions,
|
|
@@ -24,11 +24,13 @@ import {
|
|
|
24
24
|
resolveSurfacePlacement,
|
|
25
25
|
roofFacePointToSegment,
|
|
26
26
|
sceneRegistry,
|
|
27
|
+
snapLocalXZInWorld,
|
|
27
28
|
useScene,
|
|
29
|
+
wouldCreateHostingCycle,
|
|
28
30
|
} from '@pascal-app/core'
|
|
29
31
|
import { Euler, Quaternion, Vector3 } from 'three'
|
|
30
32
|
import { hasRoofFaceChildOverlap, resolveRoofWallHit } from '../../../lib/roof-wall-hit'
|
|
31
|
-
import {
|
|
33
|
+
import { getActiveBuildingPose } from '../../../lib/world-grid-snap'
|
|
32
34
|
import { itemEventToSurfaceHit, surfaceWorldNormalY } from '../shared/surface-hit'
|
|
33
35
|
import {
|
|
34
36
|
calculateItemRotation,
|
|
@@ -49,20 +51,6 @@ import type {
|
|
|
49
51
|
} from './placement-types'
|
|
50
52
|
|
|
51
53
|
const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
|
|
52
|
-
function isDescendantOfItem(
|
|
53
|
-
candidate: AnyNode,
|
|
54
|
-
ancestorId: string,
|
|
55
|
-
nodes: Record<string, AnyNode>,
|
|
56
|
-
): boolean {
|
|
57
|
-
let parentId = candidate.parentId
|
|
58
|
-
while (parentId) {
|
|
59
|
-
if (parentId === ancestorId) return true
|
|
60
|
-
const parent = nodes[parentId as AnyNodeId]
|
|
61
|
-
parentId = parent?.parentId ?? null
|
|
62
|
-
}
|
|
63
|
-
return false
|
|
64
|
-
}
|
|
65
|
-
|
|
66
54
|
// ============================================================================
|
|
67
55
|
// FLOOR STRATEGY
|
|
68
56
|
// ============================================================================
|
|
@@ -89,11 +77,12 @@ export const floorStrategy = {
|
|
|
89
77
|
// the world grid.
|
|
90
78
|
// Snapping is governed by the active mode (snapToGrid returns raw in Off /
|
|
91
79
|
// non-grid modes); Alt is force-place only and never bypasses snapping here.
|
|
92
|
-
const [
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
const dimensions = [swapDims ? dimZ : dimX, swapDims ? dimX : dimZ] as const
|
|
81
|
+
const [x, z] = snapLocalXZInWorld(
|
|
82
|
+
[event.localPosition[0], event.localPosition[2]],
|
|
83
|
+
getActiveBuildingPose(),
|
|
84
|
+
(value, axis) => snapToGrid(value, dimensions[axis]),
|
|
85
|
+
)
|
|
97
86
|
const y = ctx.gridPosition.y
|
|
98
87
|
|
|
99
88
|
return {
|
|
@@ -212,6 +201,12 @@ export const wallStrategy = {
|
|
|
212
201
|
if (attachTo !== 'wall' && attachTo !== 'wall-side') return null
|
|
213
202
|
if (!isValidWallSideFace(event.normal)) return null
|
|
214
203
|
|
|
204
|
+
if (
|
|
205
|
+
ctx.draftItem &&
|
|
206
|
+
wouldCreateHostingCycle(ctx.draftItem.id, event.node, createSceneApi(useScene))
|
|
207
|
+
)
|
|
208
|
+
return null
|
|
209
|
+
|
|
215
210
|
// Level guard
|
|
216
211
|
const wallLevelId = resolveLevelId(event.node, nodes)
|
|
217
212
|
if (ctx.levelId !== wallLevelId) return null
|
|
@@ -416,6 +411,11 @@ function resolveRoofWallTarget(
|
|
|
416
411
|
|
|
417
412
|
const hit = resolveRoofWallHit(event.node as RoofNode, event.position, event.normal, event.object)
|
|
418
413
|
if (!hit) return null
|
|
414
|
+
if (
|
|
415
|
+
ctx.draftItem &&
|
|
416
|
+
wouldCreateHostingCycle(ctx.draftItem.id, hit.segment, createSceneApi(useScene))
|
|
417
|
+
)
|
|
418
|
+
return null
|
|
419
419
|
|
|
420
420
|
const rawDims = ctx.draftItem
|
|
421
421
|
? getScaledDimensions(ctx.draftItem)
|
|
@@ -605,6 +605,11 @@ export const roofWallStrategy = {
|
|
|
605
605
|
// ============================================================================
|
|
606
606
|
|
|
607
607
|
function resolveFaceHostTarget(ctx: PlacementContext, event: NodeEvent) {
|
|
608
|
+
if (
|
|
609
|
+
ctx.draftItem &&
|
|
610
|
+
wouldCreateHostingCycle(ctx.draftItem.id, event.node, createSceneApi(useScene))
|
|
611
|
+
)
|
|
612
|
+
return null
|
|
608
613
|
const faceHost = nodeRegistry.get(event.node.type)?.capabilities.faceHost
|
|
609
614
|
if (!faceHost) return null
|
|
610
615
|
const rawDimensions = ctx.draftItem
|
|
@@ -624,18 +629,6 @@ function resolveFaceHostTarget(ctx: PlacementContext, event: NodeEvent) {
|
|
|
624
629
|
})
|
|
625
630
|
}
|
|
626
631
|
|
|
627
|
-
function clearFaceHostItemFields(ctx: PlacementContext): Partial<ItemNode> {
|
|
628
|
-
const host = ctx.state.blockId ? useScene.getState().nodes[ctx.state.blockId] : undefined
|
|
629
|
-
const clearFields = host
|
|
630
|
-
? nodeRegistry.get(host.type)?.capabilities.faceHost?.clearItemFields
|
|
631
|
-
: undefined
|
|
632
|
-
const patch: Partial<ItemNode> = {}
|
|
633
|
-
for (const field of clearFields ?? []) {
|
|
634
|
-
;(patch as Record<string, unknown>)[field] = undefined
|
|
635
|
-
}
|
|
636
|
-
return patch
|
|
637
|
-
}
|
|
638
|
-
|
|
639
632
|
export const faceHostStrategy = {
|
|
640
633
|
enter(ctx: PlacementContext, event: NodeEvent): TransitionResult | null {
|
|
641
634
|
const target = resolveFaceHostTarget(ctx, event)
|
|
@@ -695,7 +688,9 @@ export const faceHostStrategy = {
|
|
|
695
688
|
return {
|
|
696
689
|
stateUpdate: { surface: 'floor', blockId: null },
|
|
697
690
|
nodeUpdate: {
|
|
698
|
-
...clearFaceHostItemFields(
|
|
691
|
+
...clearFaceHostItemFields(
|
|
692
|
+
ctx.state.blockId ? useScene.getState().nodes[ctx.state.blockId] : undefined,
|
|
693
|
+
),
|
|
699
694
|
position: floorPosition,
|
|
700
695
|
parentId: ctx.levelId,
|
|
701
696
|
rotation: [0, ctx.currentCursorRotationY, 0],
|
|
@@ -725,6 +720,12 @@ export const ceilingStrategy = {
|
|
|
725
720
|
): TransitionResult | null {
|
|
726
721
|
if (ctx.asset.attachTo !== 'ceiling') return null
|
|
727
722
|
|
|
723
|
+
if (
|
|
724
|
+
ctx.draftItem &&
|
|
725
|
+
wouldCreateHostingCycle(ctx.draftItem.id, event.node, createSceneApi(useScene))
|
|
726
|
+
)
|
|
727
|
+
return null
|
|
728
|
+
|
|
728
729
|
// Level guard
|
|
729
730
|
const ceilingLevelId = resolveLevelId(event.node, nodes)
|
|
730
731
|
if (ctx.levelId !== ceilingLevelId) return null
|
|
@@ -860,6 +861,8 @@ function resolveCatalogItemSurfacePlacement(
|
|
|
860
861
|
worldYaw: number,
|
|
861
862
|
onReject?: PlacementContext['onSurfaceReject'],
|
|
862
863
|
rawEvent = event,
|
|
864
|
+
childId?: string,
|
|
865
|
+
childRotation?: [number, number, number],
|
|
863
866
|
) {
|
|
864
867
|
const mesh = sceneRegistry.nodes.get(host.id)
|
|
865
868
|
if (!mesh) return null
|
|
@@ -870,7 +873,12 @@ function resolveCatalogItemSurfacePlacement(
|
|
|
870
873
|
const placement = resolveSurfacePlacement({
|
|
871
874
|
host,
|
|
872
875
|
childKind: 'item',
|
|
873
|
-
|
|
876
|
+
childId,
|
|
877
|
+
childFootprint: {
|
|
878
|
+
size: dimensions,
|
|
879
|
+
rotationY: worldYaw - hostYaw,
|
|
880
|
+
rotation: [childRotation?.[0] ?? 0, worldYaw - hostYaw, childRotation?.[2] ?? 0],
|
|
881
|
+
},
|
|
874
882
|
hit: host.type === 'procedural-item' ? (itemEventToSurfaceHit(host, rawEvent) ?? hit) : hit,
|
|
875
883
|
origin: host.type === 'procedural-item' ? hit.point : undefined,
|
|
876
884
|
scene: createSceneApi(useScene),
|
|
@@ -890,17 +898,12 @@ function resolveCatalogItemSurfacePlacement(
|
|
|
890
898
|
export function validCatalogCounterPose(ctx: PlacementContext): boolean {
|
|
891
899
|
const hostId = ctx.state.surface === 'shelf-surface' ? ctx.state.shelfId : ctx.state.surfaceItemId
|
|
892
900
|
const host = hostId ? useScene.getState().nodes[hostId as AnyNodeId] : undefined
|
|
893
|
-
if (
|
|
894
|
-
host?.type !== 'cabinet' &&
|
|
895
|
-
host?.type !== 'shelf' &&
|
|
896
|
-
host?.type !== 'item' &&
|
|
897
|
-
host?.type !== 'procedural-item'
|
|
898
|
-
)
|
|
899
|
-
return true
|
|
901
|
+
if (!host) return true
|
|
900
902
|
if (!ctx.draftItem) return false
|
|
901
903
|
const placement = resolveSurfacePlacement({
|
|
902
904
|
host,
|
|
903
905
|
childKind: 'item',
|
|
906
|
+
childId: ctx.draftItem.id,
|
|
904
907
|
childFootprint: {
|
|
905
908
|
size: getScaledDimensions(ctx.draftItem),
|
|
906
909
|
rotationY: ctx.draftItem.rotation[1],
|
|
@@ -936,7 +939,7 @@ export const itemSurfaceStrategy = {
|
|
|
936
939
|
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
937
940
|
if (
|
|
938
941
|
ctx.draftItem &&
|
|
939
|
-
|
|
942
|
+
wouldCreateHostingCycle(ctx.draftItem.id, surfaceItem, createSceneApi(useScene))
|
|
940
943
|
)
|
|
941
944
|
return null
|
|
942
945
|
const pose = resolveCatalogItemSurfacePlacement(
|
|
@@ -945,6 +948,9 @@ export const itemSurfaceStrategy = {
|
|
|
945
948
|
ourDims,
|
|
946
949
|
ctx.currentCursorRotationY,
|
|
947
950
|
ctx.onSurfaceReject,
|
|
951
|
+
event,
|
|
952
|
+
ctx.draftItem?.id,
|
|
953
|
+
ctx.draftItem?.rotation,
|
|
948
954
|
)
|
|
949
955
|
if (!pose) return null
|
|
950
956
|
const draftRotation = ctx.draftItem?.rotation ?? [0, 0, 0]
|
|
@@ -984,6 +990,8 @@ export const itemSurfaceStrategy = {
|
|
|
984
990
|
ctx.currentCursorRotationY,
|
|
985
991
|
ctx.onSurfaceReject,
|
|
986
992
|
rawEvent,
|
|
993
|
+
ctx.draftItem.id,
|
|
994
|
+
ctx.draftItem.rotation,
|
|
987
995
|
)
|
|
988
996
|
if (!pose) return null
|
|
989
997
|
|
|
@@ -1030,6 +1038,7 @@ function resolveShelfSurfacePlacement(
|
|
|
1030
1038
|
worldYaw: number,
|
|
1031
1039
|
entering: boolean,
|
|
1032
1040
|
onReject?: PlacementContext['onSurfaceReject'],
|
|
1041
|
+
childId?: string,
|
|
1033
1042
|
) {
|
|
1034
1043
|
const mesh = sceneRegistry.nodes.get(host.id)
|
|
1035
1044
|
if (!mesh) return null
|
|
@@ -1039,6 +1048,7 @@ function resolveShelfSurfacePlacement(
|
|
|
1039
1048
|
const placement = resolveSurfacePlacement({
|
|
1040
1049
|
host,
|
|
1041
1050
|
childKind: 'item',
|
|
1051
|
+
childId,
|
|
1042
1052
|
childFootprint: { size: dimensions, rotationY: worldYaw - hostYaw },
|
|
1043
1053
|
hit: {
|
|
1044
1054
|
point: local.toArray(),
|
|
@@ -1085,6 +1095,7 @@ export const shelfSurfaceStrategy = {
|
|
|
1085
1095
|
ctx.currentCursorRotationY,
|
|
1086
1096
|
true,
|
|
1087
1097
|
ctx.onSurfaceReject,
|
|
1098
|
+
ctx.draftItem?.id,
|
|
1088
1099
|
)
|
|
1089
1100
|
if (!pose) return null
|
|
1090
1101
|
const draftRotation = ctx.draftItem?.rotation ?? [0, 0, 0]
|
|
@@ -1119,6 +1130,7 @@ export const shelfSurfaceStrategy = {
|
|
|
1119
1130
|
ctx.currentCursorRotationY,
|
|
1120
1131
|
false,
|
|
1121
1132
|
ctx.onSurfaceReject,
|
|
1133
|
+
ctx.draftItem.id,
|
|
1122
1134
|
)
|
|
1123
1135
|
if (!pose) return null
|
|
1124
1136
|
|
|
@@ -3,7 +3,6 @@ import * as core from '@pascal-app/core'
|
|
|
3
3
|
import {
|
|
4
4
|
ItemNode,
|
|
5
5
|
nodeRegistry,
|
|
6
|
-
registerNode,
|
|
7
6
|
type ShelfEvent,
|
|
8
7
|
ShelfNode,
|
|
9
8
|
sceneRegistry,
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
import { Euler, Group, Quaternion, Vector3 } from 'three'
|
|
13
12
|
import useEditor from '../../../store/use-editor'
|
|
14
13
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
14
|
+
import { registerHostingTestNode } from '../__fixtures__/hosting'
|
|
15
15
|
import { shelfSurfaceStrategy } from './placement-strategies'
|
|
16
16
|
import type { PlacementContext } from './placement-types'
|
|
17
17
|
|
|
@@ -39,7 +39,7 @@ beforeEach(() => {
|
|
|
39
39
|
restoreRegistry = nodeRegistry._snapshot()
|
|
40
40
|
nodeRegistry._reset()
|
|
41
41
|
rows = [0.5, 1.5]
|
|
42
|
-
|
|
42
|
+
registerHostingTestNode({
|
|
43
43
|
kind: 'shelf',
|
|
44
44
|
schemaVersion: 1,
|
|
45
45
|
schema: ShelfNode,
|
|
@@ -51,7 +51,7 @@ beforeEach(() => {
|
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
53
|
})
|
|
54
|
-
|
|
54
|
+
registerHostingTestNode({
|
|
55
55
|
kind: 'item',
|
|
56
56
|
schemaVersion: 1,
|
|
57
57
|
schema: ItemNode,
|
|
@@ -3,18 +3,24 @@ import {
|
|
|
3
3
|
type AssetInput,
|
|
4
4
|
ItemNode,
|
|
5
5
|
resolveSupportSlabPatch,
|
|
6
|
+
type SurfaceRejectReason,
|
|
6
7
|
sceneRegistry,
|
|
7
8
|
useScene,
|
|
8
9
|
} from '@pascal-app/core'
|
|
9
10
|
import { beginPerfAction, commitPerfAction, useViewer } from '@pascal-app/viewer'
|
|
10
11
|
import { useCallback, useMemo, useRef } from 'react'
|
|
11
12
|
import type { Vector3 } from 'three'
|
|
13
|
+
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
14
|
+
import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
|
|
12
15
|
import {
|
|
13
16
|
surfaceAttachmentId,
|
|
14
17
|
surfaceAttachmentUpdates,
|
|
15
18
|
surfaceFramePose,
|
|
16
19
|
updateSurfaceNode,
|
|
17
20
|
} from '../../../lib/surface-attachment'
|
|
21
|
+
import useInteractionScope, {
|
|
22
|
+
isInteractionSubtreeDraft,
|
|
23
|
+
} from '../../../store/use-interaction-scope'
|
|
18
24
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
19
25
|
import { stripTransient } from './placement-math'
|
|
20
26
|
|
|
@@ -58,6 +64,7 @@ export interface DraftNodeHandle {
|
|
|
58
64
|
options?: {
|
|
59
65
|
supportElevationCap?: number | null
|
|
60
66
|
preferredSupportSlabId?: string | null
|
|
67
|
+
onReject?: (reason: SurfaceRejectReason) => void
|
|
61
68
|
pinSupport?: boolean
|
|
62
69
|
},
|
|
63
70
|
) => string | null
|
|
@@ -76,6 +83,7 @@ export interface DraftNodeHandle {
|
|
|
76
83
|
export function useDraftNode(): DraftNodeHandle {
|
|
77
84
|
const draftRef = useRef<ItemNode | null>(null)
|
|
78
85
|
const adoptedRef = useRef(false)
|
|
86
|
+
const ownsSubtreeRef = useRef(false)
|
|
79
87
|
const originalStateRef = useRef<OriginalState | null>(null)
|
|
80
88
|
|
|
81
89
|
const create = useCallback(
|
|
@@ -106,6 +114,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
106
114
|
.set(node, useScene.getState().nodes[currentLevelId as AnyNodeId] ?? null)
|
|
107
115
|
draftRef.current = node
|
|
108
116
|
adoptedRef.current = false
|
|
117
|
+
ownsSubtreeRef.current = false
|
|
109
118
|
originalStateRef.current = null
|
|
110
119
|
return node
|
|
111
120
|
},
|
|
@@ -113,6 +122,8 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
113
122
|
)
|
|
114
123
|
|
|
115
124
|
const adopt = useCallback((node: ItemNode): void => {
|
|
125
|
+
ownsSubtreeRef.current =
|
|
126
|
+
useInteractionScope.getState().adoptSubtree(node.id) || isInteractionSubtreeDraft(node.id)
|
|
116
127
|
// Save original state so destroy() can restore it
|
|
117
128
|
const meta =
|
|
118
129
|
typeof node.metadata === 'object' && node.metadata !== null && !Array.isArray(node.metadata)
|
|
@@ -155,6 +166,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
155
166
|
options?: {
|
|
156
167
|
supportElevationCap?: number | null
|
|
157
168
|
preferredSupportSlabId?: string | null
|
|
169
|
+
onReject?: (reason: SurfaceRejectReason) => void
|
|
158
170
|
pinSupport?: boolean
|
|
159
171
|
},
|
|
160
172
|
): string | null => {
|
|
@@ -169,6 +181,29 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
169
181
|
true,
|
|
170
182
|
)
|
|
171
183
|
finalUpdate = { ...finalUpdate, ...stored }
|
|
184
|
+
if (isFreshPlacementMetadata(originalStateRef.current?.metadata)) {
|
|
185
|
+
const effectiveNode = ItemNode.parse({ ...draft, ...finalUpdate })
|
|
186
|
+
const id = commitFreshPlacementSubtree(
|
|
187
|
+
draft.id,
|
|
188
|
+
{
|
|
189
|
+
...finalUpdate,
|
|
190
|
+
...resolveSupportSlabPatch(effectiveNode, useScene.getState().nodes, {
|
|
191
|
+
maxElevation: options?.supportElevationCap,
|
|
192
|
+
preferredSlabId: options?.preferredSupportSlabId,
|
|
193
|
+
pinSupport: options?.pinSupport,
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
options?.onReject,
|
|
197
|
+
)
|
|
198
|
+
if (!id) return null
|
|
199
|
+
if (usePlacementPreview.getState().node?.id === draft.id) {
|
|
200
|
+
usePlacementPreview.getState().clear()
|
|
201
|
+
}
|
|
202
|
+
draftRef.current = null
|
|
203
|
+
adoptedRef.current = false
|
|
204
|
+
originalStateRef.current = null
|
|
205
|
+
return id
|
|
206
|
+
}
|
|
172
207
|
if (adoptedRef.current) {
|
|
173
208
|
// Move mode: update in place (single undoable action)
|
|
174
209
|
const { parentId: newParentId, ...updateProps } = finalUpdate
|
|
@@ -311,8 +346,12 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
311
346
|
if (!draftRef.current) return
|
|
312
347
|
|
|
313
348
|
const draftId = draftRef.current.id
|
|
314
|
-
|
|
315
|
-
|
|
349
|
+
if (ownsSubtreeRef.current) {
|
|
350
|
+
draftRef.current = null
|
|
351
|
+
adoptedRef.current = false
|
|
352
|
+
originalStateRef.current = null
|
|
353
|
+
return
|
|
354
|
+
} else if (adoptedRef.current && originalStateRef.current) {
|
|
316
355
|
// Move mode: restore original state instead of deleting — but only
|
|
317
356
|
// if no other system has already committed a new position for this
|
|
318
357
|
// node. The 2D `FloorplanRegistryMoveOverlay` commits via
|