Skip to content

Commit 783b2e0

Browse files
authored
Merge pull request #259 from pascalorg/fix/move-tools
Fix/move tools
2 parents 5c16aa4 + 3fcb9cf commit 783b2e0

6 files changed

Lines changed: 93 additions & 29 deletions

File tree

packages/editor/src/components/tools/ceiling/move-ceiling-tool.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
139139
const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
140140

141141
wasCommitted = true
142+
143+
// Restore original baseline while paused so the next resume+update
144+
// registers as a single tracked change (undo reverts to original).
145+
useScene.getState().updateNode(node.id, {
146+
polygon: originalPolygon,
147+
holes: originalHoles,
148+
})
149+
142150
useScene.temporal.getState().resume()
143151
useScene.getState().updateNode(node.id, preview)
144152
useScene.getState().markDirty(node.id as AnyNodeId)

packages/editor/src/components/tools/roof/move-roof-tool.tsx

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ export const MoveRoofTool: React.FC<{
2929
const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
3030
const obj = sceneRegistry.nodes.get(movingNode.id)
3131
if (obj) {
32-
const pos = new THREE.Vector3()
33-
obj.getWorldPosition(pos)
34-
return [pos.x, pos.y, pos.z]
32+
const worldPos = obj.getWorldPosition(new THREE.Vector3())
33+
// Cursor renders inside the building-local ToolManager group, so convert
34+
// world → building-local to honor any building rotation.
35+
const buildingId = useViewer.getState().selection.buildingId
36+
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
37+
if (buildingObj) buildingObj.worldToLocal(worldPos)
38+
return [worldPos.x, worldPos.y, worldPos.z]
3539
}
3640
// Fallback if not registered (e.g. newly created duplicate without mesh yet)
3741
if (
@@ -114,10 +118,15 @@ export const MoveRoofTool: React.FC<{
114118
}
115119
}
116120

117-
const computeLocal = (gridX: number, gridZ: number, y: number): [number, number] => {
118-
let localX = gridX
119-
let localZ = gridZ
120-
121+
const computeLocal = (
122+
gridX: number,
123+
gridZ: number,
124+
y: number,
125+
buildingLocalX: number,
126+
buildingLocalZ: number,
127+
): [number, number] => {
128+
// Segments have a transformed parent (stair/roof). Convert world → parent-local
129+
// via Three.js hierarchy so the segment's stored position stays parent-relative.
121130
if (
122131
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
123132
movingNode.parentId
@@ -128,19 +137,21 @@ export const MoveRoofTool: React.FC<{
128137
if (parentObj) {
129138
const worldVec = new THREE.Vector3(gridX, y, gridZ)
130139
parentObj.worldToLocal(worldVec)
131-
localX = worldVec.x
132-
localZ = worldVec.z
133-
} else {
134-
const dx = gridX - (parentNode.position[0] as number)
135-
const dz = gridZ - (parentNode.position[2] as number)
136-
const angle = -(parentNode.rotation as number)
137-
localX = dx * Math.cos(angle) - dz * Math.sin(angle)
138-
localZ = dx * Math.sin(angle) + dz * Math.cos(angle)
140+
return [worldVec.x, worldVec.z]
139141
}
142+
const dx = gridX - (parentNode.position[0] as number)
143+
const dz = gridZ - (parentNode.position[2] as number)
144+
const angle = -(parentNode.rotation as number)
145+
return [
146+
dx * Math.cos(angle) - dz * Math.sin(angle),
147+
dx * Math.sin(angle) + dz * Math.cos(angle),
148+
]
140149
}
141150
}
142151

143-
return [localX, localZ]
152+
// Stair/roof live directly in the level — their stored position is building-local.
153+
// event.localPosition is already building-local, so using it handles building rotation.
154+
return [buildingLocalX, buildingLocalZ]
144155
}
145156

146157
const onGridMove = (event: GridEvent) => {
@@ -161,7 +172,7 @@ export const MoveRoofTool: React.FC<{
161172
const lz = Math.round(event.localPosition[2] * 2) / 2
162173
setCursorWorldPos([lx, event.localPosition[1], lz])
163174

164-
const [localX, localZ] = computeLocal(gridX, gridZ, y)
175+
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
165176

166177
// Directly update the Three.js mesh — no store update during drag
167178
const mesh = sceneRegistry.nodes.get(movingNode.id)
@@ -181,8 +192,10 @@ export const MoveRoofTool: React.FC<{
181192
const gridX = Math.round(event.position[0] * 2) / 2 // world, for computeLocal
182193
const gridZ = Math.round(event.position[2] * 2) / 2
183194
const y = event.position[1]
195+
const lx = Math.round(event.localPosition[0] * 2) / 2
196+
const lz = Math.round(event.localPosition[2] * 2) / 2
184197

185-
const [localX, localZ] = computeLocal(gridX, gridZ, y)
198+
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
186199

187200
wasCommitted = true
188201

packages/editor/src/components/tools/slab/move-slab-tool.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
112112
const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
113113

114114
wasCommitted = true
115+
116+
// Restore original baseline while paused so the next resume+update
117+
// registers as a single tracked change (undo reverts to original).
118+
useScene.getState().updateNode(node.id, {
119+
polygon: originalPolygon,
120+
holes: originalHoles,
121+
})
122+
115123
useScene.temporal.getState().resume()
116124
useScene.getState().updateNode(node.id, preview)
117125
useScene.getState().markDirty(node.id as AnyNodeId)

packages/editor/src/components/tools/wall/curve-wall-tool.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,18 @@ export const CurveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
110110

111111
const curveOffset = previewOffsetRef.current
112112
wasCommitted = true
113-
useScene.temporal.getState().resume()
114-
if (curveOffset !== getClampedWallCurveOffset(node)) {
113+
114+
if (curveOffset !== originalCurveOffset) {
115+
// Restore original baseline while paused so the next resume+update
116+
// registers as a single tracked change (undo reverts to original).
117+
useScene.getState().updateNode(nodeId, { curveOffset: originalCurveOffset })
118+
useScene.getState().markDirty(nodeId as AnyNodeId)
119+
120+
useScene.temporal.getState().resume()
115121
useScene.getState().updateNode(nodeId, { curveOffset })
116122
useScene.getState().markDirty(nodeId as AnyNodeId)
123+
useScene.temporal.getState().pause()
117124
}
118-
useScene.temporal.getState().pause()
119125

120126
sfxEmitter.emit('sfx:item-place')
121127
useViewer.getState().setSelection({ selectedIds: [nodeId] })

packages/editor/src/components/tools/wall/move-wall-endpoint-tool.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
201201

202202
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
203203
wasCommitted = true
204+
205+
// Restore original baseline while paused so the next resume+update
206+
// registers as a single tracked change (undo reverts to original).
207+
applyNodePreview([
208+
{ id: nodeId, start: originalStart, end: originalEnd },
209+
...linkedOriginalsRef.current,
210+
])
211+
204212
useScene.temporal.getState().resume()
205213
applyNodePreview([
206214
{ id: nodeId, start: preview.start, end: preview.end },

packages/editor/src/components/tools/wall/move-wall-tool.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,22 +229,43 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
229229
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
230230

231231
wasCommitted = true
232-
useScene.temporal.getState().resume()
232+
233+
// Restore original baseline while paused so the next resume+update
234+
// registers as a single tracked change (undo reverts to original).
233235
applyNodePreview([
234-
{ id: nodeId, start: preview.start, end: preview.end },
236+
{ id: nodeId, start: originalStart, end: originalEnd },
237+
...linkedOriginalsRef.current,
238+
])
239+
240+
useScene.temporal.getState().resume()
241+
242+
const commitUpdates = [
243+
{
244+
id: nodeId as AnyNodeId,
245+
data: isNew
246+
? {
247+
start: preview.start,
248+
end: preview.end,
249+
metadata: stripWallIsNewMetadata(node.metadata),
250+
}
251+
: { start: preview.start, end: preview.end },
252+
},
235253
...getLinkedWallUpdates(
236254
linkedOriginalsRef.current,
237255
originalStart,
238256
originalEnd,
239257
preview.start,
240258
preview.end,
241-
),
242-
])
243-
if (isNew) {
244-
useScene.getState().updateNode(nodeId, {
245-
metadata: stripWallIsNewMetadata(node.metadata),
246-
})
259+
).map((entry) => ({
260+
id: entry.id as AnyNodeId,
261+
data: { start: entry.start, end: entry.end },
262+
})),
263+
]
264+
useScene.getState().updateNodes(commitUpdates)
265+
for (const { id } of commitUpdates) {
266+
useScene.getState().markDirty(id)
247267
}
268+
248269
useScene.temporal.getState().pause()
249270

250271
sfxEmitter.emit('sfx:item-place')

0 commit comments

Comments
 (0)