@pascal-app/editor 1.0.0-beta.5 → 1.0.1
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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- 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 +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- 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 +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- 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/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- 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/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- 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/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- 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/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
BlockNode,
|
|
5
|
+
BuildingNode,
|
|
6
|
+
getBlockFaceFrame,
|
|
7
|
+
ItemNode,
|
|
8
|
+
LevelNode,
|
|
9
|
+
type NodeEvent,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { renderToString } from 'react-dom/server'
|
|
14
|
+
import { BufferGeometry, Mesh, MeshBasicMaterial, Vector3 } from 'three'
|
|
15
|
+
import { commitFaceHostClick } from './face-host-commit'
|
|
16
|
+
import type { PlacementContext } from './placement-types'
|
|
17
|
+
import { registerTestBlockFaceHost } from './test-face-host'
|
|
18
|
+
import { type DraftNodeHandle, useDraftNode } from './use-draft-node'
|
|
19
|
+
|
|
20
|
+
type RafFn = (callback: (time: number) => void) => number
|
|
21
|
+
;(globalThis as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (callback) => {
|
|
22
|
+
callback(0)
|
|
23
|
+
return 0
|
|
24
|
+
}
|
|
25
|
+
;(globalThis as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??= () => {}
|
|
26
|
+
|
|
27
|
+
const BUILDING_ID = 'building_draft_custom_mesh'
|
|
28
|
+
const LEVEL_ID = 'level_draft_custom_mesh'
|
|
29
|
+
const BLOCK_ID = 'block_draft_host'
|
|
30
|
+
|
|
31
|
+
let draftNode: DraftNodeHandle | null = null
|
|
32
|
+
|
|
33
|
+
function DraftHarness() {
|
|
34
|
+
draftNode = useDraftNode()
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
registerTestBlockFaceHost()
|
|
40
|
+
const block = BlockNode.parse({
|
|
41
|
+
id: BLOCK_ID,
|
|
42
|
+
parentId: LEVEL_ID,
|
|
43
|
+
})
|
|
44
|
+
const level = LevelNode.parse({
|
|
45
|
+
id: LEVEL_ID,
|
|
46
|
+
parentId: BUILDING_ID,
|
|
47
|
+
children: [BLOCK_ID],
|
|
48
|
+
level: 0,
|
|
49
|
+
})
|
|
50
|
+
const building = BuildingNode.parse({
|
|
51
|
+
id: BUILDING_ID,
|
|
52
|
+
children: [LEVEL_ID],
|
|
53
|
+
})
|
|
54
|
+
useScene.setState({
|
|
55
|
+
nodes: {
|
|
56
|
+
[BUILDING_ID]: building,
|
|
57
|
+
[LEVEL_ID]: level,
|
|
58
|
+
[BLOCK_ID]: block,
|
|
59
|
+
},
|
|
60
|
+
rootNodeIds: [BUILDING_ID],
|
|
61
|
+
collections: {},
|
|
62
|
+
dirtyNodes: new Set(),
|
|
63
|
+
} as never)
|
|
64
|
+
useScene.temporal.getState().clear()
|
|
65
|
+
useScene.temporal.getState().resume()
|
|
66
|
+
useViewer.setState({
|
|
67
|
+
selection: {
|
|
68
|
+
buildingId: BUILDING_ID,
|
|
69
|
+
levelId: LEVEL_ID,
|
|
70
|
+
zoneId: null,
|
|
71
|
+
selectedIds: [],
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
draftNode = null
|
|
75
|
+
renderToString(<DraftHarness />)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('useDraftNode block face commit', () => {
|
|
79
|
+
test('persists the face host used by the placement preview', () => {
|
|
80
|
+
const draft = draftNode!
|
|
81
|
+
draft.create(new Vector3(0, 0, 0), {
|
|
82
|
+
id: 'wall-art',
|
|
83
|
+
category: 'decor',
|
|
84
|
+
name: 'Wall art',
|
|
85
|
+
thumbnail: '/wall-art.png',
|
|
86
|
+
src: '/wall-art.glb',
|
|
87
|
+
dimensions: [1, 1, 0.1],
|
|
88
|
+
attachTo: 'wall-side',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const committedId = draft.commit({
|
|
92
|
+
parentId: BLOCK_ID,
|
|
93
|
+
position: [0.5, -0.5, 0],
|
|
94
|
+
rotation: [0, 0, 0],
|
|
95
|
+
blockFaceId: 'face-front',
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const committed = useScene.getState().nodes[committedId as AnyNodeId]
|
|
99
|
+
expect(committed).toMatchObject({
|
|
100
|
+
parentId: BLOCK_ID,
|
|
101
|
+
position: [0.5, -0.5, 0],
|
|
102
|
+
blockFaceId: 'face-front',
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('keeps a block-face placement visible until undo removes the committed item', () => {
|
|
107
|
+
useScene.temporal.getState().pause()
|
|
108
|
+
const draft = draftNode!
|
|
109
|
+
const transient = draft.create(new Vector3(0, 0, 0), {
|
|
110
|
+
id: 'potted-plant',
|
|
111
|
+
category: 'decor',
|
|
112
|
+
name: 'Potted plant',
|
|
113
|
+
thumbnail: '/potted-plant.png',
|
|
114
|
+
src: '/potted-plant.glb',
|
|
115
|
+
dimensions: [0.5, 0.39, 0.5],
|
|
116
|
+
})!
|
|
117
|
+
|
|
118
|
+
const committedId = draft.commit({
|
|
119
|
+
parentId: BLOCK_ID,
|
|
120
|
+
position: [0.5, 0, 0],
|
|
121
|
+
rotation: [Math.PI / 2, 0, 0],
|
|
122
|
+
blockFaceId: 'face-top',
|
|
123
|
+
})!
|
|
124
|
+
|
|
125
|
+
const afterCommit = useScene.getState().nodes
|
|
126
|
+
expect(afterCommit[transient.id as AnyNodeId]).toBeUndefined()
|
|
127
|
+
expect(afterCommit[committedId as AnyNodeId]).toMatchObject({
|
|
128
|
+
parentId: BLOCK_ID,
|
|
129
|
+
blockFaceId: 'face-top',
|
|
130
|
+
})
|
|
131
|
+
expect((afterCommit[BLOCK_ID as AnyNodeId] as BlockNode).children).toContain(
|
|
132
|
+
committedId as ItemNode['id'],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
useScene.temporal.getState().undo()
|
|
136
|
+
|
|
137
|
+
const afterUndo = useScene.getState().nodes
|
|
138
|
+
expect(afterUndo[committedId as AnyNodeId]).toBeUndefined()
|
|
139
|
+
expect(afterUndo[transient.id as AnyNodeId]).toBeUndefined()
|
|
140
|
+
expect((afterUndo[BLOCK_ID as AnyNodeId] as BlockNode).children).not.toContain(
|
|
141
|
+
committedId as ItemNode['id'],
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('moves a block-face item to the floor as one undoable reparent', () => {
|
|
146
|
+
const hosted = ItemNode.parse({
|
|
147
|
+
id: 'item_hosted-potted-plant',
|
|
148
|
+
parentId: BLOCK_ID,
|
|
149
|
+
asset: {
|
|
150
|
+
id: 'potted-plant',
|
|
151
|
+
category: 'decor',
|
|
152
|
+
name: 'Potted plant',
|
|
153
|
+
thumbnail: '/potted-plant.png',
|
|
154
|
+
src: '/potted-plant.glb',
|
|
155
|
+
dimensions: [0.5, 0.39, 0.5],
|
|
156
|
+
},
|
|
157
|
+
position: [0.5, 0, 0],
|
|
158
|
+
rotation: [Math.PI / 2, 0, 0],
|
|
159
|
+
blockFaceId: 'face-top',
|
|
160
|
+
})
|
|
161
|
+
useScene.getState().createNode(hosted, BLOCK_ID as AnyNodeId)
|
|
162
|
+
useScene.temporal.getState().clear()
|
|
163
|
+
useScene.temporal.getState().pause()
|
|
164
|
+
|
|
165
|
+
const draft = draftNode!
|
|
166
|
+
draft.adopt(hosted)
|
|
167
|
+
draft.commit({
|
|
168
|
+
parentId: LEVEL_ID,
|
|
169
|
+
position: [2, 0, 3],
|
|
170
|
+
rotation: [0, Math.PI / 4, 0],
|
|
171
|
+
blockFaceId: undefined,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(useScene.getState().nodes[hosted.id as AnyNodeId]).toMatchObject({
|
|
175
|
+
parentId: LEVEL_ID,
|
|
176
|
+
position: [2, 0, 3],
|
|
177
|
+
rotation: [0, Math.PI / 4, 0],
|
|
178
|
+
})
|
|
179
|
+
expect(
|
|
180
|
+
(useScene.getState().nodes[hosted.id as AnyNodeId] as ItemNode).blockFaceId,
|
|
181
|
+
).toBeUndefined()
|
|
182
|
+
expect((useScene.getState().nodes[BLOCK_ID as AnyNodeId] as BlockNode).children).not.toContain(
|
|
183
|
+
hosted.id,
|
|
184
|
+
)
|
|
185
|
+
expect((useScene.getState().nodes[LEVEL_ID as AnyNodeId] as LevelNode).children).toContain(
|
|
186
|
+
hosted.id,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
useScene.temporal.getState().undo()
|
|
190
|
+
|
|
191
|
+
expect(useScene.getState().nodes[hosted.id as AnyNodeId]).toMatchObject({
|
|
192
|
+
parentId: BLOCK_ID,
|
|
193
|
+
position: [0.5, 0, 0],
|
|
194
|
+
rotation: [Math.PI / 2, 0, 0],
|
|
195
|
+
blockFaceId: 'face-top',
|
|
196
|
+
})
|
|
197
|
+
expect((useScene.getState().nodes[BLOCK_ID as AnyNodeId] as BlockNode).children).toContain(
|
|
198
|
+
hosted.id,
|
|
199
|
+
)
|
|
200
|
+
expect((useScene.getState().nodes[LEVEL_ID as AnyNodeId] as LevelNode).children).not.toContain(
|
|
201
|
+
hosted.id,
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('keeps a hosted item visible through a block topology edit and its undo', () => {
|
|
206
|
+
useScene.temporal.getState().pause()
|
|
207
|
+
const draft = draftNode!
|
|
208
|
+
draft.create(new Vector3(0, 0, 0), {
|
|
209
|
+
id: 'potted-plant',
|
|
210
|
+
category: 'decor',
|
|
211
|
+
name: 'Potted plant',
|
|
212
|
+
thumbnail: '/potted-plant.png',
|
|
213
|
+
src: '/potted-plant.glb',
|
|
214
|
+
dimensions: [0.5, 0.39, 0.5],
|
|
215
|
+
})
|
|
216
|
+
const committedId = draft.commit({
|
|
217
|
+
parentId: BLOCK_ID,
|
|
218
|
+
position: [0, 0, 0],
|
|
219
|
+
rotation: [Math.PI / 2, 0, 0],
|
|
220
|
+
blockFaceId: 'f-top',
|
|
221
|
+
})!
|
|
222
|
+
|
|
223
|
+
const beforeEdit = useScene.getState().nodes[BLOCK_ID as AnyNodeId] as BlockNode
|
|
224
|
+
const topVertexIds = new Set(
|
|
225
|
+
beforeEdit.topology.faces.find((face) => face.id === 'f-top')?.vertexIds ?? [],
|
|
226
|
+
)
|
|
227
|
+
const editedTopology = {
|
|
228
|
+
...beforeEdit.topology,
|
|
229
|
+
vertices: beforeEdit.topology.vertices.map((vertex) =>
|
|
230
|
+
topVertexIds.has(vertex.id)
|
|
231
|
+
? {
|
|
232
|
+
...vertex,
|
|
233
|
+
position: [vertex.position[0], vertex.position[1] + 0.5, vertex.position[2]] as [
|
|
234
|
+
number,
|
|
235
|
+
number,
|
|
236
|
+
number,
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
: vertex,
|
|
240
|
+
),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
useScene.temporal.getState().resume()
|
|
244
|
+
useScene.getState().updateNode(BLOCK_ID as AnyNodeId, { topology: editedTopology })
|
|
245
|
+
useScene.temporal.getState().pause()
|
|
246
|
+
|
|
247
|
+
const afterEdit = useScene.getState().nodes
|
|
248
|
+
const editedHost = afterEdit[BLOCK_ID as AnyNodeId] as BlockNode
|
|
249
|
+
expect(editedHost.children).toContain(committedId as ItemNode['id'])
|
|
250
|
+
expect(afterEdit[committedId as AnyNodeId]).toMatchObject({
|
|
251
|
+
parentId: BLOCK_ID,
|
|
252
|
+
blockFaceId: 'f-top',
|
|
253
|
+
})
|
|
254
|
+
expect(getBlockFaceFrame(editedHost.topology, 'f-top')?.origin[1]).toBe(2.9)
|
|
255
|
+
|
|
256
|
+
useScene.temporal.getState().undo()
|
|
257
|
+
|
|
258
|
+
const afterUndo = useScene.getState().nodes
|
|
259
|
+
const restoredHost = afterUndo[BLOCK_ID as AnyNodeId] as BlockNode
|
|
260
|
+
expect(restoredHost.children).toContain(committedId as ItemNode['id'])
|
|
261
|
+
expect(afterUndo[committedId as AnyNodeId]).toMatchObject({
|
|
262
|
+
parentId: BLOCK_ID,
|
|
263
|
+
blockFaceId: 'f-top',
|
|
264
|
+
})
|
|
265
|
+
expect(getBlockFaceFrame(restoredHost.topology, 'f-top')?.origin[1]).toBe(2.4)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('commits before a stop-propagation leave can destroy the block-face draft', () => {
|
|
269
|
+
useScene.temporal.getState().pause()
|
|
270
|
+
const draft = draftNode!
|
|
271
|
+
const transient = draft.create(new Vector3(), {
|
|
272
|
+
id: 'potted-plant',
|
|
273
|
+
category: 'decor',
|
|
274
|
+
name: 'Potted plant',
|
|
275
|
+
thumbnail: '/potted-plant.png',
|
|
276
|
+
src: '/potted-plant.glb',
|
|
277
|
+
dimensions: [0.5, 0.39, 0.5],
|
|
278
|
+
})!
|
|
279
|
+
Object.assign(transient, {
|
|
280
|
+
parentId: BLOCK_ID,
|
|
281
|
+
position: [0, 0, 0],
|
|
282
|
+
rotation: [Math.PI / 2, 0, 0],
|
|
283
|
+
blockFaceId: 'f-top',
|
|
284
|
+
})
|
|
285
|
+
useScene.getState().updateNode(transient.id, transient)
|
|
286
|
+
|
|
287
|
+
const host = useScene.getState().nodes[BLOCK_ID as AnyNodeId] as BlockNode
|
|
288
|
+
const geometry = new BufferGeometry()
|
|
289
|
+
geometry.userData.blockFaces = [{ faceId: 'f-top', start: 0, count: 6 }]
|
|
290
|
+
const object = new Mesh(geometry, new MeshBasicMaterial())
|
|
291
|
+
object.updateMatrixWorld(true)
|
|
292
|
+
const event: NodeEvent = {
|
|
293
|
+
node: host,
|
|
294
|
+
object,
|
|
295
|
+
faceIndex: 0,
|
|
296
|
+
position: [0, 2.4, 0],
|
|
297
|
+
localPosition: [0, 2.4, 0],
|
|
298
|
+
normal: [0, 1, 0],
|
|
299
|
+
stopPropagation: () => draft.destroy(),
|
|
300
|
+
nativeEvent: {} as NodeEvent['nativeEvent'],
|
|
301
|
+
}
|
|
302
|
+
const getContext = (): PlacementContext => ({
|
|
303
|
+
asset: transient.asset,
|
|
304
|
+
levelId: LEVEL_ID,
|
|
305
|
+
draftItem: draft.current,
|
|
306
|
+
gridPosition: new Vector3(),
|
|
307
|
+
state: {
|
|
308
|
+
surface: 'block-face',
|
|
309
|
+
blockId: BLOCK_ID,
|
|
310
|
+
wallId: null,
|
|
311
|
+
roofSegmentId: null,
|
|
312
|
+
ceilingId: null,
|
|
313
|
+
surfaceItemId: null,
|
|
314
|
+
shelfId: null,
|
|
315
|
+
},
|
|
316
|
+
currentCursorRotationY: 0,
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const outcome = commitFaceHostClick({
|
|
320
|
+
getContext,
|
|
321
|
+
event,
|
|
322
|
+
enterFaceHost: () => false,
|
|
323
|
+
commitDraft: (nodeUpdate) => ({
|
|
324
|
+
committedId: draft.commit(nodeUpdate),
|
|
325
|
+
wasAdopted: draft.isAdopted,
|
|
326
|
+
}),
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
expect(outcome?.committedId).not.toBeNull()
|
|
330
|
+
expect(useScene.getState().nodes[outcome!.committedId as AnyNodeId]).toMatchObject({
|
|
331
|
+
parentId: BLOCK_ID,
|
|
332
|
+
blockFaceId: 'f-top',
|
|
333
|
+
metadata: {},
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
})
|
|
@@ -6,13 +6,20 @@ import {
|
|
|
6
6
|
sceneRegistry,
|
|
7
7
|
useScene,
|
|
8
8
|
} from '@pascal-app/core'
|
|
9
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
9
|
+
import { beginPerfAction, commitPerfAction, useViewer } from '@pascal-app/viewer'
|
|
10
10
|
import { useCallback, useMemo, useRef } from 'react'
|
|
11
11
|
import type { Vector3 } from 'three'
|
|
12
|
+
import {
|
|
13
|
+
surfaceAttachmentId,
|
|
14
|
+
surfaceAttachmentUpdates,
|
|
15
|
+
surfaceFramePose,
|
|
16
|
+
updateSurfaceNode,
|
|
17
|
+
} from '../../../lib/surface-attachment'
|
|
12
18
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
13
19
|
import { stripTransient } from './placement-math'
|
|
14
20
|
|
|
15
21
|
interface OriginalState {
|
|
22
|
+
surfaceId: string | null
|
|
16
23
|
position: [number, number, number]
|
|
17
24
|
rotation: [number, number, number]
|
|
18
25
|
side: ItemNode['side']
|
|
@@ -21,10 +28,12 @@ interface OriginalState {
|
|
|
21
28
|
// mid-move, so reverts must restore it alongside parentId.
|
|
22
29
|
roofSegmentId: ItemNode['roofSegmentId']
|
|
23
30
|
roofFace: ItemNode['roofFace']
|
|
31
|
+
blockFaceId: ItemNode['blockFaceId']
|
|
24
32
|
metadata: ItemNode['metadata']
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export interface DraftNodeHandle {
|
|
36
|
+
updateSurface: (data: Partial<ItemNode>, surfaceId: string | null) => void
|
|
28
37
|
/** Current draft item, or null */
|
|
29
38
|
readonly current: ItemNode | null
|
|
30
39
|
/** Whether the current draft was adopted (move mode) vs created (create mode) */
|
|
@@ -46,7 +55,11 @@ export interface DraftNodeHandle {
|
|
|
46
55
|
* commit lands on the surface the cursor pointed at. */
|
|
47
56
|
commit: (
|
|
48
57
|
finalUpdate: Partial<ItemNode>,
|
|
49
|
-
options?: {
|
|
58
|
+
options?: {
|
|
59
|
+
supportElevationCap?: number | null
|
|
60
|
+
preferredSupportSlabId?: string | null
|
|
61
|
+
pinSupport?: boolean
|
|
62
|
+
},
|
|
50
63
|
) => string | null
|
|
51
64
|
/** Destroy the current draft. Create mode: delete node. Move mode: restore original state. */
|
|
52
65
|
destroy: () => void
|
|
@@ -107,16 +120,21 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
107
120
|
: {}
|
|
108
121
|
|
|
109
122
|
originalStateRef.current = {
|
|
123
|
+
surfaceId: surfaceAttachmentId(node),
|
|
110
124
|
position: [...node.position] as [number, number, number],
|
|
111
125
|
rotation: [...node.rotation] as [number, number, number],
|
|
112
126
|
side: node.side,
|
|
113
127
|
parentId: node.parentId,
|
|
114
128
|
roofSegmentId: node.roofSegmentId,
|
|
115
129
|
roofFace: node.roofFace,
|
|
130
|
+
blockFaceId: node.blockFaceId,
|
|
116
131
|
metadata: node.metadata,
|
|
117
132
|
}
|
|
118
133
|
|
|
119
|
-
draftRef.current =
|
|
134
|
+
draftRef.current = {
|
|
135
|
+
...node,
|
|
136
|
+
...surfaceFramePose(node.parentId, surfaceAttachmentId(node), node, false),
|
|
137
|
+
}
|
|
120
138
|
adoptedRef.current = true
|
|
121
139
|
|
|
122
140
|
// Mark as transient so it renders as a draft
|
|
@@ -134,11 +152,23 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
134
152
|
const commit = useCallback(
|
|
135
153
|
(
|
|
136
154
|
finalUpdate: Partial<ItemNode>,
|
|
137
|
-
options?: {
|
|
155
|
+
options?: {
|
|
156
|
+
supportElevationCap?: number | null
|
|
157
|
+
preferredSupportSlabId?: string | null
|
|
158
|
+
pinSupport?: boolean
|
|
159
|
+
},
|
|
138
160
|
): string | null => {
|
|
139
161
|
const draft = draftRef.current
|
|
140
162
|
if (!draft) return null
|
|
141
163
|
|
|
164
|
+
const surfaceId = surfaceAttachmentId(useScene.getState().nodes[draft.id] ?? draft)
|
|
165
|
+
const stored = surfaceFramePose(
|
|
166
|
+
finalUpdate.parentId ?? draft.parentId,
|
|
167
|
+
surfaceId,
|
|
168
|
+
{ ...draft, ...finalUpdate },
|
|
169
|
+
true,
|
|
170
|
+
)
|
|
171
|
+
finalUpdate = { ...finalUpdate, ...stored }
|
|
142
172
|
if (adoptedRef.current) {
|
|
143
173
|
// Move mode: update in place (single undoable action)
|
|
144
174
|
const { parentId: newParentId, ...updateProps } = finalUpdate
|
|
@@ -149,15 +179,20 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
149
179
|
const original = originalStateRef.current!
|
|
150
180
|
|
|
151
181
|
// Restore original state while paused — so the undo baseline is clean
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
updateSurfaceNode(
|
|
183
|
+
draft.id,
|
|
184
|
+
{
|
|
185
|
+
position: original.position,
|
|
186
|
+
rotation: original.rotation,
|
|
187
|
+
side: original.side,
|
|
188
|
+
parentId: original.parentId,
|
|
189
|
+
roofSegmentId: original.roofSegmentId,
|
|
190
|
+
roofFace: original.roofFace,
|
|
191
|
+
blockFaceId: original.blockFaceId,
|
|
192
|
+
metadata: original.metadata,
|
|
193
|
+
},
|
|
194
|
+
original.surfaceId,
|
|
195
|
+
)
|
|
161
196
|
|
|
162
197
|
// Resume → tracked update (undo reverts to original)
|
|
163
198
|
useScene.temporal.getState().resume()
|
|
@@ -169,25 +204,32 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
169
204
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
170
205
|
})
|
|
171
206
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
207
|
+
updateSurfaceNode(
|
|
208
|
+
draft.id,
|
|
209
|
+
{
|
|
210
|
+
position: updateProps.position ?? draft.position,
|
|
211
|
+
rotation: updateProps.rotation ?? draft.rotation,
|
|
212
|
+
side: updateProps.side ?? draft.side,
|
|
213
|
+
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
214
|
+
parentId: parentId as string,
|
|
215
|
+
// Forward the roof host explicitly: strategies set it on every
|
|
216
|
+
// commit (segment id on a roof face, undefined elsewhere), and
|
|
217
|
+
// dropping it here strands the item in the roof frame without
|
|
218
|
+
// the segment transform.
|
|
219
|
+
roofSegmentId: updateProps.roofSegmentId,
|
|
220
|
+
roofFace: updateProps.roofFace,
|
|
221
|
+
blockFaceId: updateProps.blockFaceId,
|
|
222
|
+
// Only when the strategy decided about wallId (roof commits clear
|
|
223
|
+
// it) — floor/ceiling commits never managed the field.
|
|
224
|
+
...('wallId' in updateProps ? { wallId: updateProps.wallId } : {}),
|
|
225
|
+
...resolveSupportSlabPatch(effectiveNode, useScene.getState().nodes, {
|
|
226
|
+
maxElevation: options?.supportElevationCap,
|
|
227
|
+
preferredSlabId: options?.preferredSupportSlabId,
|
|
228
|
+
pinSupport: options?.pinSupport,
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
surfaceId,
|
|
232
|
+
)
|
|
191
233
|
|
|
192
234
|
useScene.temporal.getState().pause()
|
|
193
235
|
|
|
@@ -206,7 +248,9 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
206
248
|
const parentId = (newParentId ?? useViewer.getState().selection.levelId) as AnyNodeId
|
|
207
249
|
if (!parentId) return null
|
|
208
250
|
|
|
251
|
+
beginPerfAction('place:item', draft.id)
|
|
209
252
|
// Delete draft while paused (invisible to undo)
|
|
253
|
+
updateSurfaceNode(draft.id, {}, null)
|
|
210
254
|
useScene.getState().deleteNode(draft.id)
|
|
211
255
|
draftRef.current = null
|
|
212
256
|
|
|
@@ -226,6 +270,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
226
270
|
// forwarded explicitly.
|
|
227
271
|
roofSegmentId: updateProps.roofSegmentId,
|
|
228
272
|
roofFace: updateProps.roofFace,
|
|
273
|
+
blockFaceId: updateProps.blockFaceId,
|
|
229
274
|
...('wallId' in updateProps ? { wallId: updateProps.wallId } : {}),
|
|
230
275
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
231
276
|
parentId,
|
|
@@ -236,10 +281,17 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
236
281
|
...resolveSupportSlabPatch(
|
|
237
282
|
finalNode,
|
|
238
283
|
{ ...nodes, [finalNode.id]: finalNode },
|
|
239
|
-
{
|
|
284
|
+
{
|
|
285
|
+
maxElevation: options?.supportElevationCap,
|
|
286
|
+
preferredSlabId: options?.preferredSupportSlabId,
|
|
287
|
+
pinSupport: options?.pinSupport,
|
|
288
|
+
},
|
|
240
289
|
),
|
|
241
290
|
})
|
|
242
|
-
useScene.getState().
|
|
291
|
+
useScene.getState().applyNodeChanges({
|
|
292
|
+
create: [{ node: committedNode, parentId }],
|
|
293
|
+
update: surfaceAttachmentUpdates(committedNode.id, parentId, surfaceId),
|
|
294
|
+
})
|
|
243
295
|
if (usePlacementPreview.getState().node?.id === draft.id) {
|
|
244
296
|
usePlacementPreview.getState().clear()
|
|
245
297
|
}
|
|
@@ -249,6 +301,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
249
301
|
|
|
250
302
|
adoptedRef.current = false
|
|
251
303
|
originalStateRef.current = null
|
|
304
|
+
commitPerfAction()
|
|
252
305
|
return committedNode.id
|
|
253
306
|
},
|
|
254
307
|
[],
|
|
@@ -272,6 +325,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
272
325
|
const live = useScene.getState().nodes[id as AnyNodeId] as ItemNode | undefined
|
|
273
326
|
const livePosition = live?.position
|
|
274
327
|
const externallyMoved =
|
|
328
|
+
!live?.metadata?.isTransient &&
|
|
275
329
|
!!livePosition &&
|
|
276
330
|
(livePosition[0] !== original.position[0] ||
|
|
277
331
|
livePosition[1] !== original.position[1] ||
|
|
@@ -286,15 +340,20 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
286
340
|
return
|
|
287
341
|
}
|
|
288
342
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
343
|
+
updateSurfaceNode(
|
|
344
|
+
id,
|
|
345
|
+
{
|
|
346
|
+
position: original.position,
|
|
347
|
+
rotation: original.rotation,
|
|
348
|
+
side: original.side,
|
|
349
|
+
parentId: original.parentId,
|
|
350
|
+
roofSegmentId: original.roofSegmentId,
|
|
351
|
+
roofFace: original.roofFace,
|
|
352
|
+
blockFaceId: original.blockFaceId,
|
|
353
|
+
metadata: original.metadata,
|
|
354
|
+
},
|
|
355
|
+
original.surfaceId,
|
|
356
|
+
)
|
|
298
357
|
|
|
299
358
|
// Also reset the Three.js mesh directly — the store update triggers a React
|
|
300
359
|
// re-render but the mesh position was mutated by useFrame and may not reset
|
|
@@ -307,6 +366,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
307
366
|
}
|
|
308
367
|
} else {
|
|
309
368
|
// Create mode: delete the transient node
|
|
369
|
+
updateSurfaceNode(draftRef.current.id, {}, null)
|
|
310
370
|
useScene.getState().deleteNode(draftRef.current.id)
|
|
311
371
|
}
|
|
312
372
|
|
|
@@ -318,6 +378,17 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
318
378
|
}
|
|
319
379
|
}, [])
|
|
320
380
|
|
|
381
|
+
const updateSurface = useCallback((data: Partial<ItemNode>, surfaceId: string | null) => {
|
|
382
|
+
const draft = draftRef.current
|
|
383
|
+
if (!draft) return
|
|
384
|
+
const pose = { ...draft, ...data }
|
|
385
|
+
updateSurfaceNode(
|
|
386
|
+
draft.id,
|
|
387
|
+
{ ...data, ...surfaceFramePose(pose.parentId, surfaceId, pose, true) },
|
|
388
|
+
surfaceId,
|
|
389
|
+
)
|
|
390
|
+
}, [])
|
|
391
|
+
|
|
321
392
|
return useMemo(
|
|
322
393
|
() => ({
|
|
323
394
|
get current() {
|
|
@@ -326,11 +397,12 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
326
397
|
get isAdopted() {
|
|
327
398
|
return adoptedRef.current
|
|
328
399
|
},
|
|
400
|
+
updateSurface,
|
|
329
401
|
create,
|
|
330
402
|
adopt,
|
|
331
403
|
commit,
|
|
332
404
|
destroy,
|
|
333
405
|
}),
|
|
334
|
-
[create, adopt, commit, destroy],
|
|
406
|
+
[create, adopt, commit, destroy, updateSurface],
|
|
335
407
|
)
|
|
336
408
|
}
|