@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
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
import { isHistoryShortcut } from '../../lib/history'
|
|
50
50
|
import { endpointReshapeScope } from '../../lib/interaction/scope'
|
|
51
51
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
52
|
+
import { intersectSpatialDragPlane, spatialDragLocalY } from '../../lib/spatial-drag-plane'
|
|
53
|
+
import { getSpatialPointerId, spatialPointerInput } from '../../lib/spatial-pointer-input'
|
|
52
54
|
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
53
55
|
import useInteractionScope, {
|
|
54
56
|
useEndpointReshape,
|
|
@@ -769,7 +771,9 @@ function WallBaseElevationHandle({
|
|
|
769
771
|
const midpointWorld = new Vector3(midpoint[0], initialBase, midpoint[1]).applyMatrix4(
|
|
770
772
|
levelObject.matrixWorld,
|
|
771
773
|
)
|
|
772
|
-
const planeNormal = new Vector3()
|
|
774
|
+
const planeNormal = new Vector3()
|
|
775
|
+
.subVectors(camera.getWorldPosition(new Vector3()), midpointWorld)
|
|
776
|
+
.setY(0)
|
|
773
777
|
if (planeNormal.lengthSq() === 0) return null
|
|
774
778
|
planeNormal.normalize()
|
|
775
779
|
const plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal, midpointWorld)
|
|
@@ -908,16 +912,12 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
908
912
|
const levelObject = wall.parentId ? sceneRegistry.nodes.get(wall.parentId) : null
|
|
909
913
|
if (!levelObject) return
|
|
910
914
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
// pointer movement into a world-space Y value.
|
|
915
|
+
levelObject.updateWorldMatrix(true, false)
|
|
916
|
+
const worldToLocal = levelObject.matrixWorld.clone().invert()
|
|
914
917
|
const midpointWorld = new Vector3(midX, 0, midZ).applyMatrix4(levelObject.matrixWorld)
|
|
915
|
-
const planeNormal = new Vector3().subVectors(camera.position, midpointWorld).setY(0)
|
|
916
|
-
if (planeNormal.lengthSq() === 0) return
|
|
917
|
-
planeNormal.normalize()
|
|
918
|
-
const plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal, midpointWorld)
|
|
919
|
-
|
|
920
918
|
const ndc = new Vector2()
|
|
919
|
+
const spatialPointerId = getSpatialPointerId(event.nativeEvent)
|
|
920
|
+
const spatialRay = spatialPointerId ? event.ray.clone() : null
|
|
921
921
|
const setNDC = (clientX: number, clientY: number) => {
|
|
922
922
|
const rect = gl.domElement.getBoundingClientRect()
|
|
923
923
|
ndc.set(
|
|
@@ -926,17 +926,39 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
926
926
|
)
|
|
927
927
|
}
|
|
928
928
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
929
|
+
let plane: Plane | null = null
|
|
930
|
+
let initialY: number
|
|
931
|
+
if (spatialRay) {
|
|
932
|
+
const worldUp = new Vector3(0, 1, 0).transformDirection(levelObject.matrixWorld)
|
|
933
|
+
const axisPoint = new Vector3()
|
|
934
|
+
const spatialRayLocalY = (ray: Ray) => {
|
|
935
|
+
const axisParameter = closestAxisParameterToRay(midpointWorld, worldUp, ray)
|
|
936
|
+
axisPoint.copy(midpointWorld).addScaledVector(worldUp, axisParameter)
|
|
937
|
+
return spatialDragLocalY(axisPoint, worldToLocal)
|
|
938
|
+
}
|
|
939
|
+
initialY = spatialRayLocalY(spatialRay)
|
|
940
|
+
} else {
|
|
941
|
+
// A camera-facing vertical plane gives desktop pointer movement a stable
|
|
942
|
+
// world-space height. Spatial pointers use the closest point on the
|
|
943
|
+
// wall's vertical axis instead, which remains defined for vertical rays.
|
|
944
|
+
const planeNormal = new Vector3()
|
|
945
|
+
.subVectors(camera.getWorldPosition(new Vector3()), midpointWorld)
|
|
946
|
+
.setY(0)
|
|
947
|
+
if (planeNormal.lengthSq() === 0) return
|
|
948
|
+
plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal.normalize(), midpointWorld)
|
|
949
|
+
setNDC(event.nativeEvent.clientX, event.nativeEvent.clientY)
|
|
950
|
+
raycaster.setFromCamera(ndc, camera)
|
|
951
|
+
const hit = new Vector3()
|
|
952
|
+
if (!intersectSpatialDragPlane(raycaster.ray, plane, hit)) return
|
|
953
|
+
initialY = spatialDragLocalY(hit, worldToLocal)
|
|
954
|
+
}
|
|
933
955
|
|
|
934
956
|
// Dragging the top makes the wall custom-height; seed from the resolved
|
|
935
957
|
// effective height so a plane-bound wall's drag starts at its real top.
|
|
936
958
|
const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
937
|
-
const initialY = hit.y
|
|
938
959
|
const wallId = wall.id as AnyNodeId
|
|
939
960
|
let pendingHeight = initialHeight
|
|
961
|
+
let releaseSpatialCapture: (() => void) | null = null
|
|
940
962
|
|
|
941
963
|
document.body.style.cursor = 'ns-resize'
|
|
942
964
|
sfxEmitter.emit('sfx:item-pick')
|
|
@@ -954,9 +976,23 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
954
976
|
const onMove = (e: PointerEvent) => {
|
|
955
977
|
setNDC(e.clientX, e.clientY)
|
|
956
978
|
raycaster.setFromCamera(ndc, camera)
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
979
|
+
applyRay(raycaster.ray)
|
|
980
|
+
}
|
|
981
|
+
const applyRay = (ray: Ray) => {
|
|
982
|
+
let currentY: number
|
|
983
|
+
if (spatialRay) {
|
|
984
|
+
const worldUp = new Vector3(0, 1, 0).transformDirection(levelObject.matrixWorld)
|
|
985
|
+
const axisParameter = closestAxisParameterToRay(midpointWorld, worldUp, ray)
|
|
986
|
+
const axisPoint = midpointWorld.clone().addScaledVector(worldUp, axisParameter)
|
|
987
|
+
currentY = spatialDragLocalY(axisPoint, worldToLocal)
|
|
988
|
+
} else {
|
|
989
|
+
if (!plane) return
|
|
990
|
+
const intersection = new Vector3()
|
|
991
|
+
if (!intersectSpatialDragPlane(ray, plane, intersection)) return
|
|
992
|
+
currentY = spatialDragLocalY(intersection, worldToLocal)
|
|
993
|
+
}
|
|
994
|
+
const newHeight = Math.max(MIN_WALL_HEIGHT, initialHeight + (currentY - initialY))
|
|
995
|
+
if (Math.abs(newHeight - pendingHeight) < 1e-6) return
|
|
960
996
|
pendingHeight = newHeight
|
|
961
997
|
useLiveNodeOverrides.getState().set(wallId, { height: newHeight })
|
|
962
998
|
useScene.getState().markDirty(wallId)
|
|
@@ -967,6 +1003,8 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
967
1003
|
window.removeEventListener('pointerup', onUp)
|
|
968
1004
|
window.removeEventListener('pointercancel', onCancel)
|
|
969
1005
|
window.removeEventListener('keydown', onKeyDown, true)
|
|
1006
|
+
releaseSpatialCapture?.()
|
|
1007
|
+
releaseSpatialCapture = null
|
|
970
1008
|
if (document.body.style.cursor === 'ns-resize') {
|
|
971
1009
|
document.body.style.cursor = ''
|
|
972
1010
|
}
|
|
@@ -981,6 +1019,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
981
1019
|
// Commit: write the final override-merged value to zustand once
|
|
982
1020
|
// (tracked, undoable), then drop the override so the renderer
|
|
983
1021
|
// falls back to the scene store.
|
|
1022
|
+
useScene.temporal.getState().resume()
|
|
984
1023
|
if (pendingHeight !== initialHeight) {
|
|
985
1024
|
useScene.getState().updateNode(wallId, { height: pendingHeight })
|
|
986
1025
|
}
|
|
@@ -1006,10 +1045,21 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
1006
1045
|
onCancel()
|
|
1007
1046
|
}
|
|
1008
1047
|
|
|
1009
|
-
dragCleanupRef.current =
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1048
|
+
dragCleanupRef.current = onCancel
|
|
1049
|
+
if (spatialPointerId && spatialRay) {
|
|
1050
|
+
releaseSpatialCapture = spatialPointerInput.capture(spatialPointerId, {
|
|
1051
|
+
onMove: (ray) => {
|
|
1052
|
+
spatialRay.copy(ray)
|
|
1053
|
+
applyRay(spatialRay)
|
|
1054
|
+
},
|
|
1055
|
+
onRelease: onUp,
|
|
1056
|
+
onCancel,
|
|
1057
|
+
})
|
|
1058
|
+
} else {
|
|
1059
|
+
window.addEventListener('pointermove', onMove)
|
|
1060
|
+
window.addEventListener('pointerup', onUp)
|
|
1061
|
+
window.addEventListener('pointercancel', onCancel)
|
|
1062
|
+
}
|
|
1013
1063
|
window.addEventListener('keydown', onKeyDown, true)
|
|
1014
1064
|
}
|
|
1015
1065
|
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
type Group,
|
|
12
12
|
type Mesh,
|
|
13
13
|
MeshBasicMaterial,
|
|
14
|
-
Vector3,
|
|
15
14
|
} from 'three'
|
|
16
15
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
17
16
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
17
|
+
import { copyOverlayWorldTransform } from '../../lib/overlay-transform'
|
|
18
18
|
|
|
19
19
|
// How far the outline sits outside the opening's own extents, so it reads as
|
|
20
20
|
// a frame *around* the opening rather than coinciding with its edges.
|
|
@@ -23,7 +23,6 @@ const PAD = 0.05
|
|
|
23
23
|
const ACCENT = 0x83_81_ed
|
|
24
24
|
|
|
25
25
|
const NO_RAYCAST = () => null
|
|
26
|
-
const scratchScale = new Vector3()
|
|
27
26
|
|
|
28
27
|
// Indigo accent matching the resize-arrow handles — deliberately distinct
|
|
29
28
|
// from the white selection outline so the highlight reads as "editable child
|
|
@@ -182,7 +181,7 @@ function OpeningHighlight({ openingId, depth }: { openingId: string; depth: numb
|
|
|
182
181
|
return
|
|
183
182
|
}
|
|
184
183
|
group.visible = true
|
|
185
|
-
obj
|
|
184
|
+
copyOverlayWorldTransform(obj, group)
|
|
186
185
|
})
|
|
187
186
|
|
|
188
187
|
if (!isOpening || !outlineGeometry || !fillGeometry) return null
|
|
@@ -37,8 +37,7 @@ import useInteractionScope, { useMovingNode } from '../../store/use-interaction-
|
|
|
37
37
|
import {
|
|
38
38
|
classifyParticipant,
|
|
39
39
|
collectParticipants,
|
|
40
|
-
|
|
41
|
-
expandToComponent,
|
|
40
|
+
computeGroupPlanBox,
|
|
42
41
|
type GroupPlanBounds,
|
|
43
42
|
groupPlanBounds,
|
|
44
43
|
levelFrame,
|
|
@@ -128,9 +127,8 @@ export function startFloorplanGroupMove(
|
|
|
128
127
|
const participantIds = selectedIds.filter(
|
|
129
128
|
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
130
129
|
)
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
130
|
+
// Only the selection moves; connected walls stretch through `links`, as in 3D.
|
|
131
|
+
const { starts, links } = collectParticipants(participantIds, nodes, levelId)
|
|
134
132
|
if (starts.length === 0) return null
|
|
135
133
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
136
134
|
|
|
@@ -144,10 +142,9 @@ export function startFloorplanGroupMove(
|
|
|
144
142
|
const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
|
|
145
143
|
|
|
146
144
|
// The group aligns as one rigid footprint: its bbox corners + center are
|
|
147
|
-
// the moving anchors
|
|
148
|
-
// mounted under every view mode); plan coords are level-frame, so convert.
|
|
145
|
+
// the moving anchors, measured in the level frame like the plan itself.
|
|
149
146
|
const { inverse: frameInv } = levelFrame(levelId)
|
|
150
|
-
const restBounds = groupPlanBounds(
|
|
147
|
+
const restBounds = groupPlanBounds(starts, frameInv)
|
|
151
148
|
if (!restBounds) return null
|
|
152
149
|
const restAnchors = bboxCornerAnchors(
|
|
153
150
|
'group-move',
|
|
@@ -441,12 +438,11 @@ export function startFloorplanGroupRotate(event: {
|
|
|
441
438
|
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
442
439
|
)
|
|
443
440
|
if (participantIds.length === 0) return false
|
|
444
|
-
const
|
|
445
|
-
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
441
|
+
const { starts, links } = collectParticipants(participantIds, nodes, levelId)
|
|
446
442
|
if (starts.length === 0) return false
|
|
447
443
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
448
444
|
const { inverse: frameInv } = levelFrame(levelId)
|
|
449
|
-
const bounds = groupPlanBounds(
|
|
445
|
+
const bounds = groupPlanBounds(starts, frameInv)
|
|
450
446
|
if (!bounds) return false
|
|
451
447
|
// Same pivot as the dashed box the handles hang off (and as the 3D rotate
|
|
452
448
|
// gizmo): its centre, not the anchor points' centre.
|
|
@@ -605,8 +601,9 @@ const GROUP_BOX_ROTATE_CURSOR_STYLE = {
|
|
|
605
601
|
|
|
606
602
|
/**
|
|
607
603
|
* Dashed bounding box around the current multi-selection's transformable
|
|
608
|
-
* participants
|
|
609
|
-
*
|
|
604
|
+
* participants — what a group drag carries (connected walls outside the
|
|
605
|
+
* selection stretch at their shared ends to stay joined) — and IS the group's
|
|
606
|
+
* drag handle: press anywhere
|
|
610
607
|
* inside it to slide the group, click to pick it up. Holding a selection
|
|
611
608
|
* modifier (Cmd/Ctrl/Shift) lets pointer events pass through so members under
|
|
612
609
|
* the box can still be toggled in and out. Rides the live drag delta so it
|
|
@@ -653,7 +650,7 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
653
650
|
}, [])
|
|
654
651
|
|
|
655
652
|
// `meshEpoch` re-runs the measurement once the meshes settle after a scene
|
|
656
|
-
// change (undo/redo included) — `
|
|
653
|
+
// change (undo/redo included) — `computeGroupPlanBox` reads mesh bounds,
|
|
657
654
|
// which lag the `nodes` commit by a frame or two.
|
|
658
655
|
const meshEpoch = useMeshSettleEpoch(nodes)
|
|
659
656
|
const box = useMemo(() => {
|
|
@@ -663,17 +660,13 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
663
660
|
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
664
661
|
)
|
|
665
662
|
if (participantIds.length === 0) return null
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
if (!world) return null
|
|
669
|
-
const { inverse } = levelFrame(levelId)
|
|
670
|
-
const min = world.min.clone().applyMatrix4(inverse)
|
|
671
|
-
const max = world.max.clone().applyMatrix4(inverse)
|
|
663
|
+
const plan = computeGroupPlanBox(participantIds, levelId)
|
|
664
|
+
if (!plan) return null
|
|
672
665
|
return {
|
|
673
|
-
x:
|
|
674
|
-
z:
|
|
675
|
-
width:
|
|
676
|
-
depth:
|
|
666
|
+
x: plan.minX,
|
|
667
|
+
z: plan.minZ,
|
|
668
|
+
width: plan.maxX - plan.minX,
|
|
669
|
+
depth: plan.maxZ - plan.minZ,
|
|
677
670
|
}
|
|
678
671
|
}, [selectedIds, levelId, nodes, meshEpoch])
|
|
679
672
|
|
|
@@ -14,8 +14,19 @@ import {
|
|
|
14
14
|
} from '../../lib/floorplan/floorplan-mode'
|
|
15
15
|
import useEditor from '../../store/use-editor'
|
|
16
16
|
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
17
|
+
import useInteractionScope, { useReshapingNode } from '../../store/use-interaction-scope'
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
type Loader = () => Promise<{ default: ComponentType<FloorplanToolContext> }>
|
|
20
|
+
const lazyToolCache = new WeakMap<Loader, ComponentType<FloorplanToolContext>>()
|
|
21
|
+
|
|
22
|
+
function lazyTool(loader: Loader | undefined): ComponentType<FloorplanToolContext> | null {
|
|
23
|
+
if (!loader) return null
|
|
24
|
+
const cached = lazyToolCache.get(loader)
|
|
25
|
+
if (cached) return cached
|
|
26
|
+
const component = lazy(loader)
|
|
27
|
+
lazyToolCache.set(loader, component)
|
|
28
|
+
return component
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
function registeredFloorplanTool(
|
|
21
32
|
tool: string | null,
|
|
@@ -24,13 +35,16 @@ function registeredFloorplanTool(
|
|
|
24
35
|
if (!tool) return null
|
|
25
36
|
const extension = getFloorplanNodeExtension(nodeRegistry.get(tool))
|
|
26
37
|
if (!isFloorplanToolAvailableInMode(extension?.availableModes, mode)) return null
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
return lazyTool(extension?.tool)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The plan sibling of `def.affordanceTools[reshape]`: a kind's layer for its own reshape. */
|
|
42
|
+
function registeredReshapeLayer(
|
|
43
|
+
kind: string | null,
|
|
44
|
+
reshape: string | null,
|
|
45
|
+
): ComponentType<FloorplanToolContext> | null {
|
|
46
|
+
if (!(kind && reshape)) return null
|
|
47
|
+
return lazyTool(getFloorplanNodeExtension(nodeRegistry.get(kind))?.reshapeLayers?.[reshape])
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
export function FloorplanRegisteredToolLayer() {
|
|
@@ -42,6 +56,10 @@ export function FloorplanRegisteredToolLayer() {
|
|
|
42
56
|
const toolDefaults = useEditor((state) =>
|
|
43
57
|
state.tool ? (state.toolDefaults[state.tool] ?? null) : null,
|
|
44
58
|
)
|
|
59
|
+
const reshape = useInteractionScope((state) =>
|
|
60
|
+
state.scope.kind === 'reshaping' ? state.scope.reshape : null,
|
|
61
|
+
)
|
|
62
|
+
const reshapingNode = useReshapingNode()
|
|
45
63
|
const activeLevelId = useViewer((state) => state.selection.levelId)
|
|
46
64
|
const unit = useViewer((state) => state.unit)
|
|
47
65
|
const metricNotation = useViewer((state) => state.metricNotation)
|
|
@@ -55,11 +73,14 @@ export function FloorplanRegisteredToolLayer() {
|
|
|
55
73
|
useEditor.getState().setTool(null)
|
|
56
74
|
useEditor.getState().setMode('select')
|
|
57
75
|
}, [])
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
76
|
+
const Active =
|
|
77
|
+
registeredReshapeLayer(reshapingNode?.type ?? null, reshape) ??
|
|
78
|
+
(mode === 'build' && registeredToolEnabled
|
|
79
|
+
? registeredFloorplanTool(tool, floorplanMode)
|
|
80
|
+
: null)
|
|
81
|
+
return Active ? (
|
|
61
82
|
<Suspense fallback={null}>
|
|
62
|
-
<
|
|
83
|
+
<Active
|
|
63
84
|
activeLevelId={activeLevelId}
|
|
64
85
|
finishTool={finishTool}
|
|
65
86
|
gridSnapStep={gridSnapStep}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
prepareFreshPlacementRootDuplicate,
|
|
28
28
|
} from '../../lib/fresh-planar-placement'
|
|
29
29
|
import { curveReshapeScope } from '../../lib/interaction/scope'
|
|
30
|
+
import { duplicateWithoutMove, registryMoveDisabled } from '../../lib/node-action-movement'
|
|
30
31
|
import { playBlockedQuickActionFeedback } from '../../lib/quick-action-feedback'
|
|
31
32
|
import { collectQuickActionNodeScope } from '../../lib/quick-action-nodes'
|
|
32
33
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
@@ -250,7 +251,8 @@ export function FloorplanRegistryActionMenu() {
|
|
|
250
251
|
// walls land on their bespoke `MoveWallTool` (perpendicular slide
|
|
251
252
|
// with linked-wall cascade) via `affordanceTools.move`.
|
|
252
253
|
const canMove =
|
|
253
|
-
|
|
254
|
+
!registryMoveDisabled(node) &&
|
|
255
|
+
(!!def.capabilities.movable || !!def.floorplanMoveTarget || !!def.affordanceTools?.move)
|
|
254
256
|
const canDuplicate = def.capabilities.duplicable !== false
|
|
255
257
|
const canDelete = def.capabilities.deletable !== false
|
|
256
258
|
const canAddHole = node.type === 'slab' || node.type === 'ceiling'
|
|
@@ -320,6 +322,15 @@ export function FloorplanRegistryActionMenu() {
|
|
|
320
322
|
const handleDuplicate = () => {
|
|
321
323
|
if (!node.parentId) return
|
|
322
324
|
sfxEmitter.emit('sfx:item-pick')
|
|
325
|
+
if (registryMoveDisabled(node)) {
|
|
326
|
+
try {
|
|
327
|
+
const id = duplicateWithoutMove(node)
|
|
328
|
+
if (id) useViewer.getState().setSelection({ selectedIds: [id] })
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error('Failed to duplicate node', error)
|
|
331
|
+
}
|
|
332
|
+
return
|
|
333
|
+
}
|
|
323
334
|
useScene.temporal.getState().pause()
|
|
324
335
|
let draftId: AnyNodeId | null = null
|
|
325
336
|
try {
|