From 763d3af5c00723754902eddee67a83a588eaf2d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 11:08:47 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Fix=20live=20preview=20position=20drift=20w?= =?UTF-8?q?hen=20resize=20=E2=86=92=20rotate=20=E2=86=92=20resize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The anchor compensation in resize() uses world-space AABB coordinates. When a mesh is rotated, the world AABB is larger than the actual mesh bounds, so applying a subsequent resize produced a different position offset in the live preview than at runtime (where resize runs before rotation in the sequential chain). Fix: when any transform block (resize, rotate_to, scale) in a DO chain has a value changed or is moved, reset the mesh to its initial creation state (identity rotation, unit scaling, base position) and replay the entire DO chain in order. This exactly replicates the runtime execution sequence so the live preview always matches what "play" produces. Also extends scale block support to getMeshFromBlock/getMeshesFromBlock and the contextBlock detection in updateMeshFromBlock, and adds BLOCK_MOVE replay so reordering DO-chain blocks updates the preview. https://claude.ai/code/session_011dYQCXitV5ZhAgoT7537XF --- blocks/transform.js | 12 ++- ui/blockmesh.js | 253 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 225 insertions(+), 40 deletions(-) diff --git a/blocks/transform.js b/blocks/transform.js index a30d3d4d..6e300448 100644 --- a/blocks/transform.js +++ b/blocks/transform.js @@ -11,6 +11,15 @@ import { flock } from "../flock.js"; export function defineTransformBlocks() { function handleBlockChange(block, changeEvent) { + // When this block itself is moved/connected, replay its DO chain + if ( + changeEvent.type === Blockly.Events.BLOCK_MOVE && + changeEvent.blockId === block.id + ) { + updateOrCreateMeshFromBlock(block, changeEvent); + return; + } + const changeEventBlock = Blockly.getMainWorkspace().getBlockById( changeEvent.blockId, ); @@ -27,7 +36,8 @@ export function defineTransformBlocks() { ); if ( changeEventBlockType != "rotate_to" && - changeEventBlockType != "resize" + changeEventBlockType != "resize" && + changeEventBlockType != "scale" ) return; const handleChange = handleFieldOrChildChange(block, changeEvent); diff --git a/ui/blockmesh.js b/ui/blockmesh.js index 47d5ecd8..93953b21 100644 --- a/ui/blockmesh.js +++ b/ui/blockmesh.js @@ -130,7 +130,11 @@ export function getMeshFromBlock(block) { return flock?.scene?.getMeshByName("ground"); } - if (block.type === "rotate_to" || block.type === "resize") { + if ( + block.type === "rotate_to" || + block.type === "resize" || + block.type === "scale" + ) { let container = null; let node = block; @@ -186,7 +190,11 @@ export function getMeshesFromBlock(block) { return mesh ? [mesh] : []; } - if (block.type === "rotate_to" || block.type === "resize") { + if ( + block.type === "rotate_to" || + block.type === "resize" || + block.type === "scale" + ) { let container = null; let node = block; @@ -1026,6 +1034,168 @@ function getXYZFromBlock(block) { }; } +// Walks up from a DO-chain block to find its containing create_*/load_* block. +function getContainerBlock(block) { + let node = block; + while (node) { + const parent = node.getParent(); + if (!parent) break; + // Walk to the top of this node's stack within parent + let top = node; + while ( + top.getPrevious && + top.getPrevious() && + top.getPrevious().getParent() === parent + ) { + top = top.getPrevious(); + } + const input = parent.getInputWithBlock + ? parent.getInputWithBlock(top) + : null; + if (input && input.type === Blockly.NEXT_STATEMENT && input.name === "DO") { + return parent; + } + node = parent; + } + return null; +} + +// Applies a single transform block's effect to the given list of meshes. +function applyTransformBlockToMeshes(block, meshes) { + if (!block || !meshes || meshes.length === 0) return; + switch (block.type) { + case "resize": { + const dims = getXYZFromBlock(block); + meshes.forEach((mesh) => { + if (!mesh) return; + flock.resize(mesh.name, { + width: dims.x != null ? Number(dims.x) : null, + height: dims.y != null ? Number(dims.y) : null, + depth: dims.z != null ? Number(dims.z) : null, + xOrigin: block.getFieldValue("X_ORIGIN") || "CENTRE", + yOrigin: block.getFieldValue("Y_ORIGIN") || "BASE", + zOrigin: block.getFieldValue("Z_ORIGIN") || "CENTRE", + }); + }); + break; + } + case "rotate_to": { + const rot = getXYZFromBlock(block); + meshes.forEach((mesh) => { + if (!mesh) return; + flock.rotateTo(mesh.name, { + x: rot.x != null ? Number(rot.x) : 0, + y: rot.y != null ? Number(rot.y) : 0, + z: rot.z != null ? Number(rot.z) : 0, + }); + }); + break; + } + case "rotate_model_xyz": { + const rot = getXYZFromBlock(block); + meshes.forEach((mesh) => { + if (!mesh) return; + flock.rotate(mesh.name, { + x: rot.x != null ? Number(rot.x) : 0, + y: rot.y != null ? Number(rot.y) : 0, + z: rot.z != null ? Number(rot.z) : 0, + }); + }); + break; + } + case "scale": { + const dims = getXYZFromBlock(block); + meshes.forEach((mesh) => { + if (!mesh) return; + flock.scale(mesh.name, { + x: dims.x != null ? Number(dims.x) : 1, + y: dims.y != null ? Number(dims.y) : 1, + z: dims.z != null ? Number(dims.z) : 1, + xOrigin: block.getFieldValue("X_ORIGIN") || "CENTRE", + yOrigin: block.getFieldValue("Y_ORIGIN") || "BASE", + zOrigin: block.getFieldValue("Z_ORIGIN") || "CENTRE", + }); + }); + break; + } + case "move_to_xyz": { + const pos = getXYZFromBlock(block); + meshes.forEach((mesh) => { + if (!mesh) return; + flock.positionAt(mesh.name, { + x: pos.x != null ? Number(pos.x) : 0, + y: pos.y != null ? Number(pos.y) : 0, + z: pos.z != null ? Number(pos.z) : 0, + useY: true, + }); + }); + break; + } + case "move_by_xyz": { + const delta = getXYZFromBlock(block); + meshes.forEach((mesh) => { + if (!mesh) return; + flock.moveByVector(mesh.name, { + x: delta.x != null ? Number(delta.x) : 0, + y: delta.y != null ? Number(delta.y) : 0, + z: delta.z != null ? Number(delta.z) : 0, + }); + }); + break; + } + default: + break; + } +} + +// Resets a mesh to its pre-transform base state, then replays every DO-chain +// transform block in order. This ensures the live preview matches runtime when +// a chain like resize → rotate_to → resize is used (where the anchor +// compensation in resize() must operate on an unrotated mesh to be correct). +function applyDoChainToMeshes(containerBlock, meshes) { + if (!containerBlock || !meshes || meshes.length === 0) return; + + // Step 1: Reset each mesh to its creation-time transform state. + meshes.forEach((mesh) => { + if (!mesh) return; + // Identity rotation + if (mesh.rotationQuaternion) { + mesh.rotationQuaternion.copyFrom(flock.BABYLON.Quaternion.Identity()); + } + mesh.rotation.set(0, 0, 0); + // Unit scale – primitives bake their geometry so the base scale is always 1 + mesh.scaling.set(1, 1, 1); + // Clear resize metadata so the next resize() re-captures from the reset geometry + if (mesh.metadata) { + delete mesh.metadata.originalMin; + delete mesh.metadata.originalMax; + } + mesh.computeWorldMatrix(true); + mesh.refreshBoundingInfo?.(); + }); + + // Step 2: Restore base position from the container block's X/Y/Z inputs. + const posXYZ = getXYZFromBlock(containerBlock); + const baseX = posXYZ.x != null ? Number(posXYZ.x) : 0; + const baseY = posXYZ.y != null ? Number(posXYZ.y) : 0; + const baseZ = posXYZ.z != null ? Number(posXYZ.z) : 0; + meshes.forEach((mesh) => { + if (!mesh) return; + flock.positionAt(mesh.name, { x: baseX, y: baseY, z: baseZ, useY: true }); + }); + + // Step 3: Walk the DO chain and apply each enabled transform block in order. + const doInput = containerBlock.getInput("DO"); + if (!doInput || !doInput.connection) return; + let current = doInput.connection.targetBlock(); + while (current) { + if (current.isEnabled()) { + applyTransformBlockToMeshes(current, meshes); + } + current = current.getNextBlock(); + } +} + export function updateMeshFromBlock(meshesOrMesh, block, changeEvent) { if (flock.meshDebug) { console.log("=== UPDATE MESH FROM BLOCK ==="); @@ -1061,6 +1231,21 @@ export function updateMeshFromBlock(meshesOrMesh, block, changeEvent) { return; } + // For BLOCK_MOVE events on DO-chain transform blocks, replay the full chain + // so the mesh reflects the new block order without touching the changed logic below. + if ( + changeEvent.type === Blockly.Events.BLOCK_MOVE && + (block.type === "resize" || + block.type === "rotate_to" || + block.type === "scale") + ) { + const containerBlock = getContainerBlock(block); + if (containerBlock) { + applyDoChainToMeshes(containerBlock, meshes); + } + return; + } + const changedBlock = changeEvent.blockId ? Blockly.getMainWorkspace().getBlockById(changeEvent.blockId) : null; @@ -1234,49 +1419,39 @@ export function updateMeshFromBlock(meshesOrMesh, block, changeEvent) { changeEvent.element === "field"; // Decide which block actually owns the X/Y/Z inputs: - // - rotate_to / resize child (nested inside DO) + // - rotate_to / resize / scale child (nested inside DO) // - otherwise the root block itself const contextBlock = - parent && (parent.type === "rotate_to" || parent.type === "resize") + parent && + (parent.type === "rotate_to" || + parent.type === "resize" || + parent.type === "scale") ? parent : block; - // --- rotate_to: allow gizmo / non-field events --- - if (contextBlock.type === "rotate_to") { - const rotation = getXYZFromBlock(contextBlock); - meshes.forEach((mesh) => flock.rotateTo(mesh.name, rotation)); - return; - } - - // --- resize: also allow gizmo / non-field events --- - if (contextBlock.type === "resize") { - const dims = getXYZFromBlock(contextBlock); - const resizeOptions = { - width: dims.x ?? null, - height: dims.y ?? null, - depth: dims.z ?? null, - xOrigin: contextBlock.getFieldValue("X_ORIGIN") || "CENTRE", - yOrigin: contextBlock.getFieldValue("Y_ORIGIN") || "BASE", - zOrigin: contextBlock.getFieldValue("Z_ORIGIN") || "CENTRE", - }; - - if (flock.meshDebug) { - console.log( - "Resize", - resizeOptions, - "on mesh", - meshes[0]?.name, - "from block", - block.type, - "event type", - changeEvent.type, - ); + // --- rotate_to / resize / scale: replay the entire DO chain from the + // initial mesh state so anchor compensation in resize() always operates + // on an unrotated mesh, matching exactly what happens at runtime. --- + if ( + contextBlock.type === "rotate_to" || + contextBlock.type === "resize" || + contextBlock.type === "scale" + ) { + const containerBlock = getContainerBlock(contextBlock); + if (containerBlock) { + if (flock.meshDebug) { + console.log( + "Replaying DO chain for container", + containerBlock.type, + "triggered by", + contextBlock.type, + ); + } + applyDoChainToMeshes(containerBlock, meshes); + } else { + // Fallback: no DO container found, apply just this block's transform + applyTransformBlockToMeshes(contextBlock, meshes); } - - meshes.forEach((mesh) => { - flock.resize(mesh.name, resizeOptions); - if (flock.meshDebug) console.log("After resize", mesh); - }); return; } From bae9feedce0a72aa6e16213d03e4d9fd73acb08b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 16:03:29 +0000 Subject: [PATCH 2/2] Fix rotation gizmo: handle non-physics meshes and fix physics-ancestor walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the rotation gizmo onDragEndObservable caused shapes to jump vertically after being rotated with the gizmo: 1. Wrong while-loop condition: `!mesh.parent.physics` checked the PARENT's physics instead of the current node's, stopping one level too early for compound meshes. The subsequent `if (!mesh?.physics) return` then always fired early, skipping the entire block update. 2. Non-physics meshes were completely bypassed by `if (!mesh?.physics) return`, so the rotate_to block was never created/updated. The next time applyDoChainToMeshes ran (e.g. from another block change event), it would replay the DO chain without any rotate_to block, resetting the mesh rotation (and thus the Y position compensation) to zero — causing the visible jump. Fix: separate physicsMesh walk (corrected condition checks current node), remove the early-exit guard, use findParentWithBlockId for reliable mesh→block lookup on both compound and simple meshes. https://claude.ai/code/session_011dYQCXitV5ZhAgoT7537XF --- ui/gizmos.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/ui/gizmos.js b/ui/gizmos.js index 79fed795..68f91837 100644 --- a/ui/gizmos.js +++ b/ui/gizmos.js @@ -851,17 +851,25 @@ export function toggleGizmo(gizmoType) { gizmoManager.gizmos.rotationGizmo.onDragEndObservable.add(function () { let mesh = gizmoManager.attachedMesh; - while (mesh?.parent && !mesh.parent.physics) { - mesh = mesh.parent; - } + if (!mesh) return; - if (!mesh?.physics) return; + // Walk up to find the ancestor that actually owns the physics body. + // Fix: check the CURRENT mesh for physics (not mesh.parent), so we + // stop AT the physics mesh rather than one level below it. + let physicsMesh = mesh; + while (physicsMesh?.parent && !physicsMesh.physics) { + physicsMesh = physicsMesh.parent; + } - if (mesh.savedMotionType) { - mesh.physics.setMotionType(mesh.savedMotionType); + // Restore the saved motion type (set in onDragStartObservable). + if (physicsMesh?.physics && physicsMesh.savedMotionType) { + physicsMesh.physics.setMotionType(physicsMesh.savedMotionType); } - const block = meshMap[mesh.metadata.blockKey]; + // Use findParentWithBlockId so compound / child-mesh picks still find + // the correct container block (non-physics meshes included). + const blockMesh = findParentWithBlockId(mesh) ?? mesh; + const block = meshMap[blockMesh.metadata?.blockKey]; if (!block) return; @@ -927,7 +935,8 @@ export function toggleGizmo(gizmoType) { }); } - const currentRotation = getMeshRotationInDegrees(mesh); + // Read rotation from blockMesh (the mesh the gizmo acts on). + const currentRotation = getMeshRotationInDegrees(blockMesh); setBlockXYZ( rotateBlock,