@@ -139,6 +139,14 @@ export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
|
|||||||
const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
|
const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
|
||||||
|
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
|
|
||||||
|
// Restore original baseline while paused so the next resume+update
|
||||||
|
// registers as a single tracked change (undo reverts to original).
|
||||||
|
useScene.getState().updateNode(node.id, {
|
||||||
|
polygon: originalPolygon,
|
||||||
|
holes: originalHoles,
|
||||||
|
})
|
||||||
|
|
||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
useScene.getState().updateNode(node.id, preview)
|
useScene.getState().updateNode(node.id, preview)
|
||||||
useScene.getState().markDirty(node.id as AnyNodeId)
|
useScene.getState().markDirty(node.id as AnyNodeId)
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ export const MoveRoofTool: React.FC<{
|
|||||||
const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
|
const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
|
||||||
const obj = sceneRegistry.nodes.get(movingNode.id)
|
const obj = sceneRegistry.nodes.get(movingNode.id)
|
||||||
if (obj) {
|
if (obj) {
|
||||||
const pos = new THREE.Vector3()
|
const worldPos = obj.getWorldPosition(new THREE.Vector3())
|
||||||
obj.getWorldPosition(pos)
|
// Cursor renders inside the building-local ToolManager group, so convert
|
||||||
return [pos.x, pos.y, pos.z]
|
// world → building-local to honor any building rotation.
|
||||||
|
const buildingId = useViewer.getState().selection.buildingId
|
||||||
|
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
||||||
|
if (buildingObj) buildingObj.worldToLocal(worldPos)
|
||||||
|
return [worldPos.x, worldPos.y, worldPos.z]
|
||||||
}
|
}
|
||||||
// Fallback if not registered (e.g. newly created duplicate without mesh yet)
|
// Fallback if not registered (e.g. newly created duplicate without mesh yet)
|
||||||
if (
|
if (
|
||||||
@@ -114,10 +118,15 @@ export const MoveRoofTool: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeLocal = (gridX: number, gridZ: number, y: number): [number, number] => {
|
const computeLocal = (
|
||||||
let localX = gridX
|
gridX: number,
|
||||||
let localZ = gridZ
|
gridZ: number,
|
||||||
|
y: number,
|
||||||
|
buildingLocalX: number,
|
||||||
|
buildingLocalZ: number,
|
||||||
|
): [number, number] => {
|
||||||
|
// Segments have a transformed parent (stair/roof). Convert world → parent-local
|
||||||
|
// via Three.js hierarchy so the segment's stored position stays parent-relative.
|
||||||
if (
|
if (
|
||||||
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
||||||
movingNode.parentId
|
movingNode.parentId
|
||||||
@@ -128,19 +137,21 @@ export const MoveRoofTool: React.FC<{
|
|||||||
if (parentObj) {
|
if (parentObj) {
|
||||||
const worldVec = new THREE.Vector3(gridX, y, gridZ)
|
const worldVec = new THREE.Vector3(gridX, y, gridZ)
|
||||||
parentObj.worldToLocal(worldVec)
|
parentObj.worldToLocal(worldVec)
|
||||||
localX = worldVec.x
|
return [worldVec.x, worldVec.z]
|
||||||
localZ = worldVec.z
|
}
|
||||||
} else {
|
|
||||||
const dx = gridX - (parentNode.position[0] as number)
|
const dx = gridX - (parentNode.position[0] as number)
|
||||||
const dz = gridZ - (parentNode.position[2] as number)
|
const dz = gridZ - (parentNode.position[2] as number)
|
||||||
const angle = -(parentNode.rotation as number)
|
const angle = -(parentNode.rotation as number)
|
||||||
localX = dx * Math.cos(angle) - dz * Math.sin(angle)
|
return [
|
||||||
localZ = dx * Math.sin(angle) + dz * Math.cos(angle)
|
dx * Math.cos(angle) - dz * Math.sin(angle),
|
||||||
}
|
dx * Math.sin(angle) + dz * Math.cos(angle),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [localX, localZ]
|
// Stair/roof live directly in the level — their stored position is building-local.
|
||||||
|
// event.localPosition is already building-local, so using it handles building rotation.
|
||||||
|
return [buildingLocalX, buildingLocalZ]
|
||||||
}
|
}
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
@@ -161,7 +172,7 @@ export const MoveRoofTool: React.FC<{
|
|||||||
const lz = Math.round(event.localPosition[2] * 2) / 2
|
const lz = Math.round(event.localPosition[2] * 2) / 2
|
||||||
setCursorWorldPos([lx, event.localPosition[1], lz])
|
setCursorWorldPos([lx, event.localPosition[1], lz])
|
||||||
|
|
||||||
const [localX, localZ] = computeLocal(gridX, gridZ, y)
|
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
||||||
|
|
||||||
// Directly update the Three.js mesh — no store update during drag
|
// Directly update the Three.js mesh — no store update during drag
|
||||||
const mesh = sceneRegistry.nodes.get(movingNode.id)
|
const mesh = sceneRegistry.nodes.get(movingNode.id)
|
||||||
@@ -181,8 +192,10 @@ export const MoveRoofTool: React.FC<{
|
|||||||
const gridX = Math.round(event.position[0] * 2) / 2 // world, for computeLocal
|
const gridX = Math.round(event.position[0] * 2) / 2 // world, for computeLocal
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2
|
const gridZ = Math.round(event.position[2] * 2) / 2
|
||||||
const y = event.position[1]
|
const y = event.position[1]
|
||||||
|
const lx = Math.round(event.localPosition[0] * 2) / 2
|
||||||
|
const lz = Math.round(event.localPosition[2] * 2) / 2
|
||||||
|
|
||||||
const [localX, localZ] = computeLocal(gridX, gridZ, y)
|
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
||||||
|
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,14 @@ export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
|||||||
const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
|
const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
|
||||||
|
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
|
|
||||||
|
// Restore original baseline while paused so the next resume+update
|
||||||
|
// registers as a single tracked change (undo reverts to original).
|
||||||
|
useScene.getState().updateNode(node.id, {
|
||||||
|
polygon: originalPolygon,
|
||||||
|
holes: originalHoles,
|
||||||
|
})
|
||||||
|
|
||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
useScene.getState().updateNode(node.id, preview)
|
useScene.getState().updateNode(node.id, preview)
|
||||||
useScene.getState().markDirty(node.id as AnyNodeId)
|
useScene.getState().markDirty(node.id as AnyNodeId)
|
||||||
|
|||||||
@@ -110,12 +110,18 @@ export const CurveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
|
|
||||||
const curveOffset = previewOffsetRef.current
|
const curveOffset = previewOffsetRef.current
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
|
|
||||||
|
if (curveOffset !== originalCurveOffset) {
|
||||||
|
// Restore original baseline while paused so the next resume+update
|
||||||
|
// registers as a single tracked change (undo reverts to original).
|
||||||
|
useScene.getState().updateNode(nodeId, { curveOffset: originalCurveOffset })
|
||||||
|
useScene.getState().markDirty(nodeId as AnyNodeId)
|
||||||
|
|
||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
if (curveOffset !== getClampedWallCurveOffset(node)) {
|
|
||||||
useScene.getState().updateNode(nodeId, { curveOffset })
|
useScene.getState().updateNode(nodeId, { curveOffset })
|
||||||
useScene.getState().markDirty(nodeId as AnyNodeId)
|
useScene.getState().markDirty(nodeId as AnyNodeId)
|
||||||
}
|
|
||||||
useScene.temporal.getState().pause()
|
useScene.temporal.getState().pause()
|
||||||
|
}
|
||||||
|
|
||||||
sfxEmitter.emit('sfx:item-place')
|
sfxEmitter.emit('sfx:item-place')
|
||||||
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
||||||
|
|||||||
@@ -201,6 +201,14 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
|
|
||||||
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
|
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
|
|
||||||
|
// Restore original baseline while paused so the next resume+update
|
||||||
|
// registers as a single tracked change (undo reverts to original).
|
||||||
|
applyNodePreview([
|
||||||
|
{ id: nodeId, start: originalStart, end: originalEnd },
|
||||||
|
...linkedOriginalsRef.current,
|
||||||
|
])
|
||||||
|
|
||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
applyNodePreview([
|
applyNodePreview([
|
||||||
{ id: nodeId, start: preview.start, end: preview.end },
|
{ id: nodeId, start: preview.start, end: preview.end },
|
||||||
|
|||||||
@@ -229,22 +229,43 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
||||||
|
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
useScene.temporal.getState().resume()
|
|
||||||
|
// Restore original baseline while paused so the next resume+update
|
||||||
|
// registers as a single tracked change (undo reverts to original).
|
||||||
applyNodePreview([
|
applyNodePreview([
|
||||||
{ id: nodeId, start: preview.start, end: preview.end },
|
{ id: nodeId, start: originalStart, end: originalEnd },
|
||||||
|
...linkedOriginalsRef.current,
|
||||||
|
])
|
||||||
|
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
|
|
||||||
|
const commitUpdates = [
|
||||||
|
{
|
||||||
|
id: nodeId as AnyNodeId,
|
||||||
|
data: isNew
|
||||||
|
? {
|
||||||
|
start: preview.start,
|
||||||
|
end: preview.end,
|
||||||
|
metadata: stripWallIsNewMetadata(node.metadata),
|
||||||
|
}
|
||||||
|
: { start: preview.start, end: preview.end },
|
||||||
|
},
|
||||||
...getLinkedWallUpdates(
|
...getLinkedWallUpdates(
|
||||||
linkedOriginalsRef.current,
|
linkedOriginalsRef.current,
|
||||||
originalStart,
|
originalStart,
|
||||||
originalEnd,
|
originalEnd,
|
||||||
preview.start,
|
preview.start,
|
||||||
preview.end,
|
preview.end,
|
||||||
),
|
).map((entry) => ({
|
||||||
])
|
id: entry.id as AnyNodeId,
|
||||||
if (isNew) {
|
data: { start: entry.start, end: entry.end },
|
||||||
useScene.getState().updateNode(nodeId, {
|
})),
|
||||||
metadata: stripWallIsNewMetadata(node.metadata),
|
]
|
||||||
})
|
useScene.getState().updateNodes(commitUpdates)
|
||||||
|
for (const { id } of commitUpdates) {
|
||||||
|
useScene.getState().markDirty(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
useScene.temporal.getState().pause()
|
useScene.temporal.getState().pause()
|
||||||
|
|
||||||
sfxEmitter.emit('sfx:item-place')
|
sfxEmitter.emit('sfx:item-place')
|
||||||
|
|||||||
Reference in New Issue
Block a user