Fix relative move drag offsets
This commit is contained in:
@@ -41,6 +41,7 @@ export const MoveRoofTool: React.FC<{
|
||||
}, [])
|
||||
|
||||
const previousGridPosRef = useRef<[number, number] | null>(null)
|
||||
const dragAnchorRef = useRef<[number, number] | null>(null)
|
||||
|
||||
const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
|
||||
const obj = sceneRegistry.nodes.get(movingNode.id)
|
||||
@@ -78,6 +79,8 @@ export const MoveRoofTool: React.FC<{
|
||||
|
||||
useEffect(() => {
|
||||
useScene.temporal.getState().pause()
|
||||
dragAnchorRef.current = null
|
||||
previousGridPosRef.current = null
|
||||
|
||||
const meta =
|
||||
typeof movingNode.metadata === 'object' && movingNode.metadata !== null
|
||||
@@ -255,6 +258,24 @@ export const MoveRoofTool: React.FC<{
|
||||
return [buildingLocalX, buildingLocalZ]
|
||||
}
|
||||
|
||||
const localPositionToToolLocal = (
|
||||
position: [number, number, number],
|
||||
): [number, number, number] => {
|
||||
if (
|
||||
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
||||
movingNode.parentId
|
||||
) {
|
||||
const parentObj = sceneRegistry.nodes.get(movingNode.parentId)
|
||||
if (parentObj) {
|
||||
const point = parentObj.localToWorld(new THREE.Vector3(...position))
|
||||
if (buildingObj) buildingObj.worldToLocal(point)
|
||||
return [point.x, point.y, point.z]
|
||||
}
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
const onGridMove = (event: GridEvent) => {
|
||||
const y = event.position[1]
|
||||
|
||||
@@ -263,29 +284,40 @@ export const MoveRoofTool: React.FC<{
|
||||
walls: levelWalls,
|
||||
fences: levelFences,
|
||||
})
|
||||
// Layer alignment snap on top (top-level stair/roof). Recompute the
|
||||
// world point from the aligned building-local point so it stays correct
|
||||
// under building rotation.
|
||||
const [lx, lz] = alignLocalPoint(
|
||||
const [rawGridX, , rawGridZ] = localToWorldPoint(snappedLocal, y)
|
||||
const [rawLocalX, rawLocalZ] = computeLocal(
|
||||
rawGridX,
|
||||
rawGridZ,
|
||||
y,
|
||||
snappedLocal[0],
|
||||
snappedLocal[1],
|
||||
event.nativeEvent?.altKey === true,
|
||||
)
|
||||
const [gridX, , gridZ] = localToWorldPoint([lx, lz], y)
|
||||
const anchor = dragAnchorRef.current ?? [rawLocalX, rawLocalZ]
|
||||
dragAnchorRef.current = anchor
|
||||
|
||||
let localX = movingNode.position[0] + (rawLocalX - anchor[0])
|
||||
let localZ = movingNode.position[2] + (rawLocalZ - anchor[1])
|
||||
|
||||
if (alignTopLevel) {
|
||||
const aligned = alignLocalPoint(localX, localZ, event.nativeEvent?.altKey === true)
|
||||
localX = aligned[0]
|
||||
localZ = aligned[1]
|
||||
}
|
||||
|
||||
if (
|
||||
previousGridPosRef.current &&
|
||||
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
||||
(localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
|
||||
) {
|
||||
triggerSFX('sfx:grid-snap')
|
||||
}
|
||||
|
||||
previousGridPosRef.current = [gridX, gridZ]
|
||||
previousGridPosRef.current = [localX, localZ]
|
||||
|
||||
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
||||
lastLocalPosition = [localX, movingNode.position[1], localZ]
|
||||
const previewPosition = getPreviewPosition(lastLocalPosition)
|
||||
setCursorWorldPos(isFloorPlaced ? previewPosition : [lx, event.localPosition[1], lz])
|
||||
setCursorWorldPos(
|
||||
isFloorPlaced ? previewPosition : localPositionToToolLocal(lastLocalPosition),
|
||||
)
|
||||
|
||||
// Directly update the Three.js mesh — no store update during drag
|
||||
const mesh = sceneRegistry.nodes.get(movingNode.id)
|
||||
@@ -302,26 +334,13 @@ export const MoveRoofTool: React.FC<{
|
||||
// Floor-placed parents (stairs) stay in their committed local frame;
|
||||
// the lifted Y remains presentation-only in the 3D view.
|
||||
useLiveTransforms.getState().set(movingNode.id, {
|
||||
position: isFloorPlaced ? lastLocalPosition : [gridX, y, gridZ],
|
||||
position: lastLocalPosition,
|
||||
rotation: pendingRotation,
|
||||
})
|
||||
}
|
||||
|
||||
const onGridClick = (event: GridEvent) => {
|
||||
const y = event.position[1]
|
||||
const snappedLocal = snapFenceDraftPoint({
|
||||
point: [event.localPosition[0], event.localPosition[2]],
|
||||
walls: levelWalls,
|
||||
fences: levelFences,
|
||||
})
|
||||
const [lx, lz] = alignLocalPoint(
|
||||
snappedLocal[0],
|
||||
snappedLocal[1],
|
||||
event.nativeEvent?.altKey === true,
|
||||
)
|
||||
const [gridX, , gridZ] = localToWorldPoint([lx, lz], y)
|
||||
|
||||
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
||||
const [localX, , localZ] = lastLocalPosition
|
||||
|
||||
useAlignmentGuides.getState().clear()
|
||||
wasCommitted = true
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type RoofEvent,
|
||||
type RoofNode,
|
||||
type RoofSegmentNode,
|
||||
sceneRegistry,
|
||||
} from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import * as THREE from 'three'
|
||||
import { type RoofSegmentHit, resolveRoofSegmentHit } from './roof-segment-hit'
|
||||
import { getSurfaceY } from './roof-surface'
|
||||
|
||||
export type RelativeRoofDragTarget = {
|
||||
segment: RoofSegmentNode
|
||||
localX: number
|
||||
localY: number
|
||||
localZ: number
|
||||
hit: RoofSegmentHit
|
||||
}
|
||||
|
||||
type RelativeRoofDragState = {
|
||||
segmentId: string
|
||||
anchor: [number, number]
|
||||
start: [number, number, number]
|
||||
current: [number, number, number]
|
||||
surfaceOffsetY: number
|
||||
}
|
||||
|
||||
export function roofSegmentLocalToBuildingLocal(
|
||||
segmentId: string,
|
||||
position: [number, number, number],
|
||||
): [number, number, number] {
|
||||
const segmentObj = sceneRegistry.nodes.get(segmentId as AnyNodeId)
|
||||
if (!segmentObj) return position
|
||||
|
||||
const point = segmentObj.localToWorld(new THREE.Vector3(...position))
|
||||
const buildingId = useViewer.getState().selection.buildingId
|
||||
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
||||
if (buildingObj) buildingObj.worldToLocal(point)
|
||||
return [point.x, point.y, point.z]
|
||||
}
|
||||
|
||||
export function createRelativeRoofDrag(original: {
|
||||
position: [number, number, number]
|
||||
roofSegmentId?: string
|
||||
}): {
|
||||
resolve: (event: RoofEvent) => RelativeRoofDragTarget | null
|
||||
} {
|
||||
let state: RelativeRoofDragState | null = null
|
||||
|
||||
const getPositionInSegment = (
|
||||
position: [number, number, number],
|
||||
fromSegmentId: string | undefined,
|
||||
segment: RoofSegmentNode,
|
||||
): [number, number, number] => {
|
||||
if (fromSegmentId === segment.id) return position
|
||||
|
||||
const fromSegmentObj = fromSegmentId
|
||||
? sceneRegistry.nodes.get(fromSegmentId as AnyNodeId)
|
||||
: null
|
||||
const targetSegmentObj = sceneRegistry.nodes.get(segment.id as AnyNodeId)
|
||||
if (!(fromSegmentObj && targetSegmentObj)) return position
|
||||
|
||||
const point = fromSegmentObj.localToWorld(new THREE.Vector3(...position))
|
||||
targetSegmentObj.worldToLocal(point)
|
||||
return [point.x, point.y, point.z]
|
||||
}
|
||||
|
||||
const getStartPositionForSegment = (
|
||||
segment: RoofSegmentNode,
|
||||
previousState: RelativeRoofDragState | null,
|
||||
): [number, number, number] => {
|
||||
if (previousState) {
|
||||
return getPositionInSegment(previousState.current, previousState.segmentId, segment)
|
||||
}
|
||||
|
||||
if (original.roofSegmentId === segment.id) return original.position
|
||||
|
||||
return getPositionInSegment(original.position, original.roofSegmentId, segment)
|
||||
}
|
||||
|
||||
return {
|
||||
resolve(event) {
|
||||
const hit = resolveRoofSegmentHit(
|
||||
event.node as RoofNode,
|
||||
event.position[0],
|
||||
event.position[1],
|
||||
event.position[2],
|
||||
)
|
||||
if (!hit) return null
|
||||
|
||||
if (!state || state.segmentId !== hit.segment.id) {
|
||||
const start = getStartPositionForSegment(hit.segment, state)
|
||||
state = {
|
||||
segmentId: hit.segment.id,
|
||||
anchor: [hit.localX, hit.localZ],
|
||||
start,
|
||||
current: start,
|
||||
surfaceOffsetY: start[1] - getSurfaceY(start[0], start[2], hit.segment),
|
||||
}
|
||||
}
|
||||
|
||||
const localX = state.start[0] + (hit.localX - state.anchor[0])
|
||||
const localZ = state.start[2] + (hit.localZ - state.anchor[1])
|
||||
const localY = getSurfaceY(localX, localZ, hit.segment) + state.surfaceOffsetY
|
||||
state.current = [localX, localY, localZ]
|
||||
return {
|
||||
segment: hit.segment,
|
||||
localX,
|
||||
localY,
|
||||
localZ,
|
||||
hit,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user