Keyboard controls for fly camera#536
Conversation
📝 WalkthroughWalkthroughThis pull request refactors the gizmo management system by delegating each gizmo type to dedicated handler functions, introduces a Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
ui/gizmos.js (1)
1188-1211: Expose the fly-camera state semantically as well as visually.The new
.activeclass only communicates state visually. Updatingaria-pressedat the same time lets assistive tech announce whether flycam is on or off.♿ Small accessibility follow-up
if (cameraMode === "play") { cameraMode = "fly"; flock.printText({ text: translate("fly_camera_instructions"), duration: 15, color: "white", }); - cameraButton.classList.add("active"); + cameraButton?.classList.add("active"); + cameraButton?.setAttribute("aria-pressed", "true"); } else { cameraMode = "play"; - cameraButton.classList.remove("active"); + cameraButton?.classList.remove("active"); + cameraButton?.setAttribute("aria-pressed", "false"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/gizmos.js` around lines 1188 - 1211, The camera button toggling in handleCameraGizmo currently only updates visual state via classList but not accessibility state; update the same toggle code in handleCameraGizmo (reference: function handleCameraGizmo and variable cameraButton) to set aria-pressed to "true" when enabling fly mode and "false" when disabling, e.g. check cameraButton exists then call cameraButton.setAttribute("aria-pressed", "true") when adding the "active" class and setAttribute("aria-pressed", "false") when removing it so screen readers reflect the fly-camera state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui/gizmos.js`:
- Around line 990-1031: The function handleBoundsGizmo is an unused/unreachable
stub (its toggleGizmo callers are commented out) and is causing an ESLint
failure; either delete the entire handleBoundsGizmo function block or rename it
to _handleBoundsGizmo to mark it intentionally unused, making sure to update any
internal references such as gizmoManager.boundingBoxGizmoEnabled,
gizmoManager.boundingBoxDragBehavior.onDragStartObservable, onDragEndObservable,
mesh.savedMotionType, meshMap and the calls to highlightBlockById/setBlockXYZ so
they remain correct if you keep it; if you retain it for future use, rename to
_handleBoundsGizmo and add a short inline comment noting it’s intentionally
dormant.
- Around line 1044-1077: blockKey is being read from the previously attached
mesh before the newly picked mesh is normalized, so highlighting uses the wrong
block; after you determine and normalize pickedMesh (including the
getRootMesh(pickedMesh.parent) step), derive the block key from that finalized
mesh by calling findParentWithBlockId(finalPickedMesh)?.metadata?.blockKey
(instead of using the earlier blockKey), then look up meshMap[blockKey] and call
highlightBlockById; adjust references to gizmoManager.attachedMesh,
resetAttachedMesh, pickedMesh, getRootMesh, findParentWithBlockId, meshMap, and
highlightBlockById accordingly.
- Around line 854-868: In the rotationGizmo onDragEnd handler
(gizmoManager.gizmos.rotationGizmo.onDragEndObservable) preserve the original
attached mesh in a new variable (e.g., originalMesh = gizmoManager.attachedMesh)
before walking up parents to find a physics body; only use the parent-walked
mesh for physics restoration (mesh.savedMotionType and
mesh.physics.setMotionType), but do not return early if no physics body is
found—always continue using originalMesh to look up the block via
meshMap[originalMesh.metadata.blockKey] and perform Blockly sync; apply the same
pattern to the analogous handler around lines 929–939 so physics restoration is
optional and block synchronization always runs.
- Around line 557-818: The handlers in handleScaleGizmo are repeatedly adding
observers (gizmoManager.onAttachedToMeshObservable.add,
gizmoManager.gizmos.scaleGizmo.onDragObservable.add, onDragStartObservable.add,
onDragEndObservable.add and the per-axis dragBehavior.onDragStartObservable.add
calls) each time the gizmo is reselected; guard these registrations by tracking
whether observers have been registered (e.g. a boolean like
gizmoManager._scaleObserversRegistered or a stored array of observer handles)
and only call .add() once, or store the returned observer handles and remove
them before re-adding (using observable.remove/observer references); apply the
same guarding pattern for handleRotationGizmo and handlePositionGizmo or move
the wiring into setGizmoManager so observers are registered exactly once.
- Around line 753-793: When inserting a new statement block (e.g., the newly
created resizeBlock) into the block.getInput("DO") chain you must preserve
existing statements: capture the original first child via stmt/originalFirst =
block.getInput("DO")?.connection?.targetBlock?.(), then connect the new block
into the DO input using
block.getInput("DO").connection.connect(resizeBlock.previousConnection), and
finally chain the saved originalFirst after the new block using
resizeBlock.nextConnection.connect(originalFirst.previousConnection) (handling
nulls). Apply the same prepend pattern to the other insertion site that creates
a "rotate_to" block (use its previousConnection/nextConnection and the DO input
connection) so existing DO statements are not orphaned.
---
Nitpick comments:
In `@ui/gizmos.js`:
- Around line 1188-1211: The camera button toggling in handleCameraGizmo
currently only updates visual state via classList but not accessibility state;
update the same toggle code in handleCameraGizmo (reference: function
handleCameraGizmo and variable cameraButton) to set aria-pressed to "true" when
enabling fly mode and "false" when disabling, e.g. check cameraButton exists
then call cameraButton.setAttribute("aria-pressed", "true") when adding the
"active" class and setAttribute("aria-pressed", "false") when removing it so
screen readers reflect the fly-camera state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| function handleScaleGizmo() { | ||
| configureScaleGizmo(gizmoManager); | ||
| { | ||
| const sg = gizmoManager.gizmos.scaleGizmo; | ||
| if (!sg._textAxisObserversRegistered) { | ||
| sg.xGizmo.dragBehavior.onDragStartObservable.add( | ||
| () => (textScaleAxis = "x"), | ||
| ); | ||
| sg.yGizmo.dragBehavior.onDragStartObservable.add( | ||
| () => (textScaleAxis = "y"), | ||
| ); | ||
| sg.zGizmo.dragBehavior.onDragStartObservable.add( | ||
| () => (textScaleAxis = "z"), | ||
| ); | ||
| sg.uniformScaleGizmo.dragBehavior.onDragStartObservable.add( | ||
| () => (textScaleAxis = "uniform"), | ||
| ); | ||
| sg._textAxisObserversRegistered = true; | ||
| } | ||
| } | ||
| gizmoManager.onAttachedToMeshObservable.add((mesh) => { | ||
| if (!mesh) return; | ||
|
|
||
| canvas = flock.scene.getEngine().getRenderingCanvas(); // Get the flock.BABYLON.js canvas | ||
| const blockKey = mesh?.metadata?.blockKey; | ||
| const blockId = blockKey ? meshMap[blockKey] : null; | ||
| if (!blockId) return; | ||
|
|
||
| onPickMesh = function (event) { | ||
| const canvasRect = canvas.getBoundingClientRect(); | ||
| highlightBlockById(Blockly.getMainWorkspace(), blockId); | ||
| }); | ||
|
|
||
| if (eventIsOutOfCanvasBounds(event, canvasRect)) { | ||
| window.removeEventListener("click", onPickMesh); | ||
| document.body.style.cursor = "default"; | ||
| return; | ||
| } | ||
| // Track bottom for correct visual anchoring | ||
| let originalBottomY = 0; | ||
|
|
||
| const [canvasX, canvasY] = getCanvasXAndCanvasYValues( | ||
| event, | ||
| canvasRect, | ||
| ); | ||
| gizmoManager.gizmos.scaleGizmo.onDragObservable.add(() => { | ||
| const mesh = gizmoManager.attachedMesh; | ||
|
|
||
| const pickRay = flock.scene.createPickingRay( | ||
| canvasX, | ||
| canvasY, | ||
| flock.BABYLON.Matrix.Identity(), | ||
| flock.scene.activeCamera, | ||
| ); | ||
| mesh.computeWorldMatrix(true); | ||
| mesh.refreshBoundingInfo(); | ||
|
|
||
| const pickResult = flock.scene.pickWithRay( | ||
| pickRay, | ||
| (mesh) => mesh.isPickable, | ||
| ); | ||
| const newBottomY = mesh.getBoundingInfo().boundingBox.minimumWorld.y; | ||
| const deltaY = originalBottomY - newBottomY; | ||
| mesh.position.y += deltaY; | ||
|
|
||
| if (pickResult.hit) { | ||
| const pickedPosition = pickResult.pickedPoint; | ||
| const block = Blockly.getMainWorkspace().getBlockById( | ||
| mesh?.metadata?.blockKey, | ||
| ); | ||
| if (gizmoManager.scaleGizmoEnabled) { | ||
| switch (block?.type) { | ||
| case "create_capsule": | ||
| case "create_cylinder": | ||
| mesh.scaling.z = mesh.scaling.x; | ||
| break; | ||
| case "create_3d_text": | ||
| if (textScaleAxis === "z") { | ||
| // Z handle: depth only — lock X and Y | ||
| mesh.scaling.x = 1; | ||
| mesh.scaling.y = 1; | ||
| } else if (textScaleAxis === "x" || textScaleAxis === "uniform") { | ||
| // X or uniform: size only — keep Y = X, lock Z | ||
| mesh.scaling.y = mesh.scaling.x; | ||
| mesh.scaling.z = textOrigScaleZ; | ||
| } else if (textScaleAxis === "y") { | ||
| // Y handle: size only — keep X = Y, lock Z | ||
| mesh.scaling.x = mesh.scaling.y; | ||
| mesh.scaling.z = textOrigScaleZ; | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| const workspace = Blockly.getMainWorkspace(); | ||
| const originalBlock = workspace.getBlockById(blockId); | ||
| duplicateBlockAndInsert(originalBlock, workspace, pickedPosition); | ||
| } | ||
| }; | ||
| gizmoManager.gizmos.scaleGizmo.onDragStartObservable.add(() => { | ||
| const mesh = gizmoManager.attachedMesh; | ||
| flock.ensureUniqueGeometry(mesh); | ||
| mesh.computeWorldMatrix(true); | ||
| mesh.refreshBoundingInfo(); | ||
| originalBottomY = mesh.getBoundingInfo().boundingBox.minimumWorld.y; | ||
| textOrigScaleZ = mesh.scaling.z; | ||
| textScaleAxis = null; | ||
|
|
||
| const motionType = mesh.physics?.getMotionType(); | ||
| mesh.savedMotionType = motionType; | ||
|
|
||
| if ( | ||
| mesh.physics && | ||
| mesh.physics.getMotionType() !== flock.BABYLON.PhysicsMotionType.ANIMATED | ||
| ) { | ||
| mesh.physics.setMotionType(flock.BABYLON.PhysicsMotionType.ANIMATED); | ||
| mesh.physics.disablePreStep = false; | ||
| } | ||
|
|
||
| // Use setTimeout to defer listener setup | ||
| document.body.style.cursor = "crosshair"; | ||
| setTimeout(() => { | ||
| window.addEventListener("click", onPickMesh); | ||
| }, 50); | ||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
| highlightBlockById(Blockly.getMainWorkspace(), block); | ||
| }); | ||
|
|
||
| break; | ||
| case "select": { | ||
| gizmoManager.selectGizmoEnabled = true; | ||
|
|
||
| // Store the pointer observable | ||
| const pointerObservable = flock.scene.onPointerObservable; | ||
|
|
||
| // Add the observer | ||
| const pointerObserver = pointerObservable.add((event) => { | ||
| if (event.type === flock.BABYLON.PointerEventTypes.POINTERPICK) { | ||
| if (gizmoManager.attachedMesh) { | ||
| resetAttachedMesh(); | ||
| blockKey = findParentWithBlockId(gizmoManager.attachedMesh) | ||
| ?.metadata?.blockKey; | ||
| } | ||
| let pickedMesh = event.pickInfo.pickedMesh; | ||
| gizmoManager.gizmos.scaleGizmo.onDragEndObservable.add(() => { | ||
| const mesh = gizmoManager.attachedMesh; | ||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
| textScaleAxis = null; | ||
|
|
||
| if (pickedMesh && pickedMesh.name !== "ground") { | ||
| const position = pickedMesh.getAbsolutePosition(); | ||
| if (mesh.savedMotionType != null) { | ||
| mesh.physics.setMotionType(mesh.savedMotionType); | ||
| } | ||
|
|
||
| // Round the coordinates to 2 decimal places | ||
| const roundedPosition = roundVectorToFixed(position, 2); | ||
| flock.updatePhysics(mesh); | ||
|
|
||
| flock.printText({ | ||
| text: translate("position_readout").replace( | ||
| "{position}", | ||
| String(roundedPosition), | ||
| ), | ||
| duration: 30, | ||
| color: "black", | ||
| }); | ||
| try { | ||
| const ensureFreshBounds = (m) => { | ||
| m.computeWorldMatrix(true); | ||
| m.refreshBoundingInfo(); | ||
| return m.getBoundingInfo().boundingBox; | ||
| }; | ||
|
|
||
| if (flock.meshDebug) console.log(pickedMesh.parent); | ||
| const bbox = ensureFreshBounds(mesh); | ||
|
|
||
| if (pickedMesh.parent) { | ||
| pickedMesh = getRootMesh(pickedMesh.parent); | ||
| if (flock.meshDebug) console.log(pickedMesh.visibility); | ||
| pickedMesh.visibility = 0.001; | ||
| if (flock.meshDebug) console.log(pickedMesh.visibility); | ||
| } | ||
| const newBottomY = bbox.minimumWorld.y; | ||
| mesh.position.y += originalBottomY - newBottomY; | ||
|
|
||
| const block = meshMap[blockKey]; | ||
| highlightBlockById(Blockly.getMainWorkspace(), block); | ||
| const sizeLocal = bbox.extendSize.scale(2); | ||
| const w = sizeLocal.x * mesh.scaling.x; | ||
| const h = sizeLocal.y * mesh.scaling.y; | ||
| const d = sizeLocal.z * mesh.scaling.z; | ||
|
|
||
| // Attach the gizmo to the selected mesh | ||
| gizmoManager.attachToMesh(pickedMesh); | ||
| switch (block.type) { | ||
| case "create_plane": | ||
| setNumberInputs(block, { WIDTH: w, HEIGHT: h }); | ||
| break; | ||
|
|
||
| // Show bounding box for the selected mesh | ||
| pickedMesh.showBoundingBox = true; | ||
| } else { | ||
| if (pickedMesh && pickedMesh.name === "ground") { | ||
| const position = event.pickInfo.pickedPoint; | ||
|
|
||
| const roundedPosition = roundVectorToFixed(position, 2); | ||
|
|
||
| flock.printText({ | ||
| text: translate("position_readout").replace( | ||
| "{position}", | ||
| String(roundedPosition), | ||
| ), | ||
| duration: 30, | ||
| color: "black", | ||
| }); | ||
| } | ||
| case "create_box": | ||
| setNumberInputs(block, { WIDTH: w, HEIGHT: h, DEPTH: d }); | ||
| break; | ||
|
|
||
| // Deselect if no mesh is picked | ||
| if (gizmoManager.attachedMesh) { | ||
| resetChildMeshesOfAttachedMesh(); | ||
| gizmoManager.attachToMesh(null); // Detach the gizmo | ||
| } | ||
| } | ||
| case "create_capsule": | ||
| setNumberInputs(block, { HEIGHT: h, DIAMETER: w }); | ||
| break; | ||
|
|
||
| pointerObservable.remove(pointerObserver); | ||
| } | ||
| }); | ||
| case "create_cylinder": { | ||
| const newScaledDiameter = w; | ||
|
|
||
| break; | ||
| } | ||
| case "bounds": | ||
| gizmoManager.boundingBoxGizmoEnabled = true; | ||
| gizmoManager.boundingBoxDragBehavior.onDragStartObservable.add( | ||
| function () { | ||
| const mesh = gizmoManager.attachedMesh; | ||
| const currentTop = getNumberInput(block, "DIAMETER_TOP"); | ||
| const currentBottom = getNumberInput(block, "DIAMETER_BOTTOM"); | ||
|
|
||
| if (!mesh?.physics) return; | ||
|
|
||
| const motionType = mesh.physics.getMotionType?.(); | ||
| mesh.savedMotionType = motionType; | ||
| let newTop; | ||
| let newBottom; | ||
|
|
||
| if ( | ||
| mesh.physics && | ||
| motionType != null && | ||
| motionType !== flock.BABYLON.PhysicsMotionType.STATIC | ||
| Number.isFinite(currentTop) && | ||
| Number.isFinite(currentBottom) && | ||
| currentTop > 0 && | ||
| currentBottom > 0 | ||
| ) { | ||
| mesh.physics.setMotionType(flock.BABYLON.PhysicsMotionType.STATIC); | ||
| mesh.physics.disablePreStep = false; | ||
| if (currentTop >= currentBottom) { | ||
| newTop = newScaledDiameter; | ||
| newBottom = newTop * (currentBottom / currentTop); | ||
| } else { | ||
| newBottom = newScaledDiameter; | ||
| newTop = newBottom * (currentTop / currentBottom); | ||
| } | ||
| } else { | ||
| newTop = newScaledDiameter; | ||
| newBottom = newScaledDiameter; | ||
| } | ||
|
|
||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
| highlightBlockById(Blockly.getMainWorkspace(), block); | ||
| }, | ||
| ); | ||
|
|
||
| gizmoManager.boundingBoxDragBehavior.onDragEndObservable.add(function () { | ||
| const mesh = gizmoManager.attachedMesh; | ||
| setNumberInputs(block, { | ||
| HEIGHT: h, | ||
| DIAMETER_TOP: newTop, | ||
| DIAMETER_BOTTOM: newBottom, | ||
| }); | ||
| break; | ||
| } | ||
|
|
||
| if (mesh.savedMotionType != null && mesh.physics) { | ||
| mesh.physics.setMotionType(mesh.savedMotionType); | ||
| case "create_sphere": | ||
| setNumberInputs(block, { | ||
| DIAMETER_X: w, | ||
| DIAMETER_Y: h, | ||
| DIAMETER_Z: d, | ||
| }); | ||
| break; | ||
|
|
||
| case "create_3d_text": { | ||
| const currentSize = getNumberInput(block, "SIZE"); | ||
| const currentDepth = getNumberInput(block, "DEPTH"); | ||
| setNumberInputs(block, { | ||
| SIZE: currentSize * mesh.scaling.y, | ||
| DEPTH: currentDepth * mesh.scaling.z, | ||
| }); | ||
| break; | ||
| } | ||
|
|
||
| mesh.computeWorldMatrix(true); | ||
| case "load_model": | ||
| case "load_multi_object": | ||
| case "load_object": | ||
| case "load_character": { | ||
| const groupId = Blockly.utils.idGenerator.genUid(); | ||
| Blockly.Events.setGroup(groupId); | ||
|
|
||
| let addedDoSection = false; | ||
| if (!block.getInput("DO")) { | ||
| block.appendStatementInput("DO").setCheck(null).appendField(""); | ||
| addedDoSection = true; | ||
| } | ||
|
|
||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
| let resizeBlock = null; | ||
| const modelVariable = block.getFieldValue("ID_VAR"); | ||
|
|
||
| if (block) { | ||
| const blockPosition = flock.getBlockPositionFromMesh(mesh); | ||
| setBlockXYZ(block, blockPosition.x, blockPosition.y, blockPosition.z); | ||
| } | ||
| }); | ||
| const stmt = block.getInput("DO")?.connection?.targetBlock?.(); | ||
| for (let cur = stmt; cur; cur = cur.getNextBlock?.()) { | ||
| if ( | ||
| cur.type === "resize" && | ||
| cur.getFieldValue?.("BLOCK_NAME") === modelVariable | ||
| ) { | ||
| resizeBlock = cur; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| break; | ||
| if (!resizeBlock) { | ||
| resizeBlock = Blockly.getMainWorkspace().newBlock("resize"); | ||
| resizeBlock.setFieldValue(modelVariable, "BLOCK_NAME"); | ||
| resizeBlock.initSvg(); | ||
| resizeBlock.render(); | ||
|
|
||
| ["X", "Y", "Z"].forEach((axis) => { | ||
| const input = resizeBlock.getInput(axis); | ||
| const shadow = Blockly.getMainWorkspace().newBlock("math_number"); | ||
| shadow.setFieldValue("1", "NUM"); | ||
| shadow.setShadow(true); | ||
| shadow.initSvg(); | ||
| shadow.render(); | ||
| input.connection.connect(shadow.outputConnection); | ||
| }); | ||
|
|
||
| case "position": | ||
| configurePositionGizmo(gizmoManager); | ||
| gizmoManager.onAttachedToMeshObservable.add((mesh) => { | ||
| if (!mesh) return; | ||
| resizeBlock.render(); | ||
| block | ||
| .getInput("DO") | ||
| .connection.connect(resizeBlock.previousConnection); | ||
|
|
||
| const blockKey = mesh?.metadata?.blockKey; | ||
| const blockId = blockKey ? meshMap[blockKey] : null; | ||
| if (!blockId) return; | ||
| gizmoCreatedBlocks.set(resizeBlock.id, { | ||
| parentId: block.id, | ||
| createdDoSection: addedDoSection, | ||
| timestamp: Date.now(), | ||
| }); | ||
| } | ||
| mesh.computeWorldMatrix(true); | ||
| mesh.refreshBoundingInfo(); | ||
| const sizeLocalScaled = getScaledSize(mesh); | ||
|
|
||
| setNumberInputs(resizeBlock, { | ||
| X: sizeLocalScaled.x, | ||
| Y: sizeLocalScaled.y, | ||
| Z: sizeLocalScaled.z, | ||
| }); | ||
|
|
||
| highlightBlockById(Blockly.getMainWorkspace(), blockId); | ||
| }); | ||
| Blockly.Events.setGroup(null); | ||
| break; | ||
| } | ||
| } | ||
| } catch (e) { | ||
| console.error("Error updating block values:", e); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "gizmos.js" -type fRepository: flipcomputing/flock
Length of output: 77
🏁 Script executed:
rg "function handleScaleGizmo" -A 5Repository: flipcomputing/flock
Length of output: 358
🏁 Script executed:
rg "toggleGizmo" -B 3 -A 10Repository: flipcomputing/flock
Length of output: 50376
🏁 Script executed:
rg "function setGizmoManager\|let gizmoManager" -B 2 -A 3 ui/gizmos.js | head -40Repository: flipcomputing/flock
Length of output: 45
🏁 Script executed:
rg "disableGizmos" -B 2 -A 10 ui/gizmos.js | head -50Repository: flipcomputing/flock
Length of output: 1366
🏁 Script executed:
rg "let gizmoManager|var gizmoManager" -B 5 -A 5 ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 455
🏁 Script executed:
rg "function setGizmoManager|export function initializeGizmoManager" -B 2 -A 20 ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 721
🏁 Script executed:
sed -n '1043,1120p' ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 2541
🏁 Script executed:
sed -n '1170,1250p' ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 2850
🏁 Script executed:
rg "case \"scale\":|case \"rotation\":|case \"position\":" -A 3 ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 252
🏁 Script executed:
sed -n '557,620p' ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 2242
🏁 Script executed:
sed -n '821,880p' ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 1773
🏁 Script executed:
sed -n '944,1000p' ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 1788
Guard observer registrations to prevent accumulation on repeated gizmo toggles.
Each time a gizmo mode is reselected, the handlers append new onAttachedToMeshObservable and drag observers without cleanup. Babylon.js observables accumulate callbacks with each .add() call. After toggling a gizmo multiple times, one drag fires duplicate callbacks, producing multiple highlight updates, physics transitions, and block state changes.
♻️ One way to guard the registrations
function handleScaleGizmo() {
configureScaleGizmo(gizmoManager);
+ if (gizmoManager._scaleObserversRegistered) return;
+ gizmoManager._scaleObserversRegistered = true;
{
const sg = gizmoManager.gizmos.scaleGizmo;
if (!sg._textAxisObserversRegistered) {
...
}
}
gizmoManager.onAttachedToMeshObservable.add((mesh) => {
...
});
gizmoManager.gizmos.scaleGizmo.onDragObservable.add(() => {
...
});
gizmoManager.gizmos.scaleGizmo.onDragStartObservable.add(() => {
...
});
gizmoManager.gizmos.scaleGizmo.onDragEndObservable.add(() => {
...
});
}Apply the same pattern to handleRotationGizmo() and handlePositionGizmo(), or move the observer wiring into setGizmoManager().
Also applies to: 821–941, 944–988
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/gizmos.js` around lines 557 - 818, The handlers in handleScaleGizmo are
repeatedly adding observers (gizmoManager.onAttachedToMeshObservable.add,
gizmoManager.gizmos.scaleGizmo.onDragObservable.add, onDragStartObservable.add,
onDragEndObservable.add and the per-axis dragBehavior.onDragStartObservable.add
calls) each time the gizmo is reselected; guard these registrations by tracking
whether observers have been registered (e.g. a boolean like
gizmoManager._scaleObserversRegistered or a stored array of observer handles)
and only call .add() once, or store the returned observer handles and remove
them before re-adding (using observable.remove/observer references); apply the
same guarding pattern for handleRotationGizmo and handlePositionGizmo or move
the wiring into setGizmoManager so observers are registered exactly once.
| let addedDoSection = false; | ||
| if (!block.getInput("DO")) { | ||
| block.appendStatementInput("DO").setCheck(null).appendField(""); | ||
| addedDoSection = true; | ||
| } | ||
|
|
||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
| let resizeBlock = null; | ||
| const modelVariable = block.getFieldValue("ID_VAR"); | ||
|
|
||
| if (block) { | ||
| const blockPosition = flock.getBlockPositionFromMesh(mesh); | ||
| setBlockXYZ(block, blockPosition.x, blockPosition.y, blockPosition.z); | ||
| } | ||
| }); | ||
| const stmt = block.getInput("DO")?.connection?.targetBlock?.(); | ||
| for (let cur = stmt; cur; cur = cur.getNextBlock?.()) { | ||
| if ( | ||
| cur.type === "resize" && | ||
| cur.getFieldValue?.("BLOCK_NAME") === modelVariable | ||
| ) { | ||
| resizeBlock = cur; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| break; | ||
| if (!resizeBlock) { | ||
| resizeBlock = Blockly.getMainWorkspace().newBlock("resize"); | ||
| resizeBlock.setFieldValue(modelVariable, "BLOCK_NAME"); | ||
| resizeBlock.initSvg(); | ||
| resizeBlock.render(); | ||
|
|
||
| ["X", "Y", "Z"].forEach((axis) => { | ||
| const input = resizeBlock.getInput(axis); | ||
| const shadow = Blockly.getMainWorkspace().newBlock("math_number"); | ||
| shadow.setFieldValue("1", "NUM"); | ||
| shadow.setShadow(true); | ||
| shadow.initSvg(); | ||
| shadow.render(); | ||
| input.connection.connect(shadow.outputConnection); | ||
| }); | ||
|
|
||
| case "position": | ||
| configurePositionGizmo(gizmoManager); | ||
| gizmoManager.onAttachedToMeshObservable.add((mesh) => { | ||
| if (!mesh) return; | ||
| resizeBlock.render(); | ||
| block | ||
| .getInput("DO") | ||
| .connection.connect(resizeBlock.previousConnection); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "gizmos.js" -type fRepository: flipcomputing/flock
Length of output: 77
🏁 Script executed:
# Get the file size first
wc -l ui/gizmos.jsRepository: flipcomputing/flock
Length of output: 80
🏁 Script executed:
# Read lines 753-793 with context
sed -n '745,800p' ui/gizmos.js | cat -nRepository: flipcomputing/flock
Length of output: 2391
🏁 Script executed:
# Read lines 873-918 with context
sed -n '865,925p' ui/gizmos.js | cat -nRepository: flipcomputing/flock
Length of output: 2731
🏁 Script executed:
# Search for other DO input connections in the file to see patterns
rg "getInput.*DO.*connection\.connect" ui/gizmos.js -B 3 -A 1Repository: flipcomputing/flock
Length of output: 258
🏁 Script executed:
# Search for other patterns of connecting to statement inputs or DO sections
rg "\.connection\.connect\(" ui/gizmos.js -B 2 -A 1 | head -60Repository: flipcomputing/flock
Length of output: 661
🏁 Script executed:
# Check if there's any existing block chaining logic in the file
rg "nextConnection|getNextBlock|previousConnection" ui/gizmos.js | head -20Repository: flipcomputing/flock
Length of output: 475
🏁 Script executed:
# Check context around these connection calls to see if there's any preservation logic
sed -n '10,55p' ui/gizmos.js | cat -nRepository: flipcomputing/flock
Length of output: 1744
🏁 Script executed:
# Look for any cleanup or restoration logic that might handle orphaned blocks
rg "getNextBlock|previousConnection|nextConnection" ui/gizmos.js -B 3 -A 3 | head -80Repository: flipcomputing/flock
Length of output: 1379
🏁 Script executed:
# Search for any block cleanup, disconnection, or restoration logic
rg "disconnect|removeInputValue|unplug" ui/gizmos.js -B 2 -A 2Repository: flipcomputing/flock
Length of output: 45
🏁 Script executed:
# Check what happens with startBlock and remainingChildren to understand block management
sed -n '600,700p' ui/gizmos.js | cat -n | grep -A 20 "remainingChildren"Repository: flipcomputing/flock
Length of output: 45
🏁 Script executed:
# Look for the gizmoCreatedBlocks cleanup logic to understand block lifecycle
rg "gizmoCreatedBlocks" ui/gizmos.js -B 3 -A 3Repository: flipcomputing/flock
Length of output: 1717
🏁 Script executed:
# Search broadly for any block connection management patterns
rg "previousConnection" ui/gizmos.js -B 5 -A 5Repository: flipcomputing/flock
Length of output: 834
🏁 Script executed:
# Check if the rotation block creation also has the same vulnerability
sed -n '850,920p' ui/gizmos.js | cat -nRepository: flipcomputing/flock
Length of output: 3009
🏁 Script executed:
# Search for other DO input statement creations to see if this pattern is used elsewhere
rg "appendStatementInput.*DO" ui/gizmos.js -B 2 -A 10Repository: flipcomputing/flock
Length of output: 1158
Fix block chain insertion to preserve existing DO statements.
Connecting a new resize or rotate_to block directly to the DO input statement connection orphans any existing statements in that input. When the DO input already contains user statements and no matching block exists, calling .connect() on the statement input replaces the connection, disconnecting the previous first child and all its successors.
Both locations require the same fix: prepend new blocks by saving the original first child, connecting the new block to the DO input, then chaining the original first child after the new block.
🧩 Safer insertion pattern
+function prependDoStatement(parentBlock, childBlock) {
+ const doConnection = parentBlock.getInput("DO").connection;
+ const firstChild = doConnection.targetBlock();
+
+ doConnection.connect(childBlock.previousConnection);
+ if (firstChild) {
+ childBlock.nextConnection.connect(firstChild.previousConnection);
+ }
+}
+
...
- block
- .getInput("DO")
- .connection.connect(resizeBlock.previousConnection);
+ prependDoStatement(block, resizeBlock);
...
- block.getInput("DO").connection.connect(rotateBlock.previousConnection);
+ prependDoStatement(block, rotateBlock);Also applies to: 873-918
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/gizmos.js` around lines 753 - 793, When inserting a new statement block
(e.g., the newly created resizeBlock) into the block.getInput("DO") chain you
must preserve existing statements: capture the original first child via
stmt/originalFirst = block.getInput("DO")?.connection?.targetBlock?.(), then
connect the new block into the DO input using
block.getInput("DO").connection.connect(resizeBlock.previousConnection), and
finally chain the saved originalFirst after the new block using
resizeBlock.nextConnection.connect(originalFirst.previousConnection) (handling
nulls). Apply the same prepend pattern to the other insertion site that creates
a "rotate_to" block (use its previousConnection/nextConnection and the DO input
connection) so existing DO statements are not orphaned.
| gizmoManager.gizmos.rotationGizmo.onDragEndObservable.add(function () { | ||
| let mesh = gizmoManager.attachedMesh; | ||
| while (mesh?.parent && !mesh.parent.physics) { | ||
| mesh = mesh.parent; | ||
| } | ||
|
|
||
| const blockKey = mesh?.metadata?.blockKey; | ||
| const blockId = blockKey ? meshMap[blockKey] : null; | ||
| if (!blockId) return; | ||
| if (!mesh?.physics) return; | ||
|
|
||
| highlightBlockById(Blockly.getMainWorkspace(), blockId); | ||
| }); | ||
| if (mesh.savedMotionType != null) { | ||
| mesh.physics.setMotionType(mesh.savedMotionType); | ||
| } | ||
|
|
||
| gizmoManager.gizmos.rotationGizmo.onDragStartObservable.add(() => { | ||
| let mesh = gizmoManager.attachedMesh; | ||
| if (!mesh) return; | ||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
|
|
||
| if (!mesh.physics) return; | ||
| if (!block) return; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and read the gizmos.js file
fd -t f "gizmos.js" | head -5Repository: flipcomputing/flock
Length of output: 75
🏁 Script executed:
# Read the code around lines 854-868
cat -n ui/gizmos.js | sed -n '850,875p'Repository: flipcomputing/flock
Length of output: 946
🏁 Script executed:
# Read the code around lines 929-939
cat -n ui/gizmos.js | sed -n '925,945p'Repository: flipcomputing/flock
Length of output: 635
🏁 Script executed:
# Search for Blockly block update logic in the file
rg -n "rotate_to.*block" ui/gizmos.js | head -20Repository: flipcomputing/flock
Length of output: 268
🏁 Script executed:
# Search for mentions of "Blockly" or block creation/update patterns
rg -n "block\s*=" ui/gizmos.js | head -30Repository: flipcomputing/flock
Length of output: 636
🏁 Script executed:
# Read the full rotation gizmo handler
cat -n ui/gizmos.js | sed -n '854,940p'Repository: flipcomputing/flock
Length of output: 3508
🏁 Script executed:
# Look at the position gizmo handler for comparison
rg -n "handlePositionGizmo|onDragEndObservable.add" ui/gizmos.js -A 20 | head -60Repository: flipcomputing/flock
Length of output: 1715
🏁 Script executed:
# Search for where meshMap is populated to understand which meshes are tracked
rg -n "meshMap\[" ui/gizmos.js | head -20Repository: flipcomputing/flock
Length of output: 635
🏁 Script executed:
# Check if gizmoManager.attachedMesh can be non-physics
rg -n "attachedMesh\s*=" ui/gizmos.js -B 2 -A 2Repository: flipcomputing/flock
Length of output: 45
🏁 Script executed:
# Search for gizmoManager attachments and mesh selection
rg -n "gizmoManager.attachTo|attachedMesh|enabled = true" ui/gizmos.js | head -30Repository: flipcomputing/flock
Length of output: 1630
🏁 Script executed:
# Look for any mesh validation before gizmo attachment
rg -n "function.*attachGizmo|function.*gizmo.*mesh" ui/gizmos.js -A 5Repository: flipcomputing/flock
Length of output: 45
🏁 Script executed:
# Check the position gizmo to see if it has similar structure
cat -n ui/gizmos.js | sed -n '620,680p'Repository: flipcomputing/flock
Length of output: 2306
🏁 Script executed:
# Search for comments explaining physics requirements
rg -n "physics|Physics" ui/gizmos.js -B 2 -A 2 | head -50Repository: flipcomputing/flock
Length of output: 1504
🏁 Script executed:
# Read full position gizmo onDragEnd handler
cat -n ui/gizmos.js | sed -n '589,620p'Repository: flipcomputing/flock
Length of output: 1468
🏁 Script executed:
# Read position gizmo onDragStart handler
cat -n ui/gizmos.js | sed -n '565,590p'Repository: flipcomputing/flock
Length of output: 1041
🏁 Script executed:
# Read rotation gizmo onDragStart handler
cat -n ui/gizmos.js | sed -n '820,855p'Repository: flipcomputing/flock
Length of output: 1420
🏁 Script executed:
# Check if there are any comments explaining why rotation needs physics
rg -n "rotation|rotate" ui/gizmos.js -B 3 -A 1 | grep -E "(//|/\*)" -A 2 -B 2 | head -40Repository: flipcomputing/flock
Length of output: 1429
Separate mesh identity for physics operations and Blockly synchronization.
The early return at line 860 prevents Blockly block updates for non-physics meshes, causing their rotation state to become stale in the workspace. Unlike position and scale gizmos, the rotation handler blocks all execution when no physics body is found. Additionally, the while loop (lines 856–858) overwrites the mesh variable to walk up the parent chain, causing the subsequent blockKey lookup (line 866) to potentially use the wrong mesh. Preserve the original attached mesh for Blockly sync while making physics restoration optional:
🔧 Keep physics restore optional, not the block sync
gizmoManager.gizmos.rotationGizmo.onDragEndObservable.add(function () {
- let mesh = gizmoManager.attachedMesh;
- while (mesh?.parent && !mesh.parent.physics) {
- mesh = mesh.parent;
+ const attachedMesh = gizmoManager.attachedMesh;
+ let physicsMesh = attachedMesh;
+ while (physicsMesh?.parent && !physicsMesh.parent.physics) {
+ physicsMesh = physicsMesh.parent;
}
- if (!mesh?.physics) return;
-
- if (mesh.savedMotionType != null) {
- mesh.physics.setMotionType(mesh.savedMotionType);
+ if (physicsMesh?.physics && physicsMesh.savedMotionType != null) {
+ physicsMesh.physics.setMotionType(physicsMesh.savedMotionType);
}
- const block = meshMap[mesh?.metadata?.blockKey];
+ const block = meshMap[attachedMesh?.metadata?.blockKey];
if (!block) return;
...
- const currentRotation = getMeshRotationInDegrees(mesh);
+ const currentRotation = getMeshRotationInDegrees(attachedMesh);Also applies to: 929–939
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/gizmos.js` around lines 854 - 868, In the rotationGizmo onDragEnd handler
(gizmoManager.gizmos.rotationGizmo.onDragEndObservable) preserve the original
attached mesh in a new variable (e.g., originalMesh = gizmoManager.attachedMesh)
before walking up parents to find a physics body; only use the parent-walked
mesh for physics restoration (mesh.savedMotionType and
mesh.physics.setMotionType), but do not return early if no physics body is
found—always continue using originalMesh to look up the block via
meshMap[originalMesh.metadata.blockKey] and perform Blockly sync; apply the same
pattern to the analogous handler around lines 929–939 so physics restoration is
optional and block synchronization always runs.
| // Bounds: Allow the user to move the mesh | ||
| // Legacy? | ||
| function handleBoundsGizmo() { | ||
| gizmoManager.boundingBoxGizmoEnabled = true; | ||
| gizmoManager.boundingBoxDragBehavior.onDragStartObservable.add(function () { | ||
| const mesh = gizmoManager.attachedMesh; | ||
|
|
||
| highlightBlockById(Blockly.getMainWorkspace(), blockId); | ||
| }); | ||
| if (!mesh?.physics) return; | ||
|
|
||
| // Track bottom for correct visual anchoring | ||
| let originalBottomY = 0; | ||
| const motionType = mesh.physics.getMotionType?.(); | ||
| mesh.savedMotionType = motionType; | ||
|
|
||
| gizmoManager.gizmos.scaleGizmo.onDragObservable.add(() => { | ||
| const mesh = gizmoManager.attachedMesh; | ||
| if ( | ||
| mesh.physics && | ||
| motionType != null && | ||
| motionType !== flock.BABYLON.PhysicsMotionType.STATIC | ||
| ) { | ||
| mesh.physics.setMotionType(flock.BABYLON.PhysicsMotionType.STATIC); | ||
| mesh.physics.disablePreStep = false; | ||
| } | ||
|
|
||
| mesh.computeWorldMatrix(true); | ||
| mesh.refreshBoundingInfo(); | ||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
| highlightBlockById(Blockly.getMainWorkspace(), block); | ||
| }); | ||
|
|
||
| const newBottomY = mesh.getBoundingInfo().boundingBox.minimumWorld.y; | ||
| const deltaY = originalBottomY - newBottomY; | ||
| mesh.position.y += deltaY; | ||
| gizmoManager.boundingBoxDragBehavior.onDragEndObservable.add(function () { | ||
| const mesh = gizmoManager.attachedMesh; | ||
|
|
||
| const block = Blockly.getMainWorkspace().getBlockById( | ||
| mesh?.metadata?.blockKey, | ||
| ); | ||
| if (gizmoManager.scaleGizmoEnabled) { | ||
| switch (block?.type) { | ||
| case "create_capsule": | ||
| case "create_cylinder": | ||
| mesh.scaling.z = mesh.scaling.x; | ||
| break; | ||
| case "create_3d_text": | ||
| if (textScaleAxis === "z") { | ||
| // Z handle: depth only — lock X and Y | ||
| mesh.scaling.x = 1; | ||
| mesh.scaling.y = 1; | ||
| } else if (textScaleAxis === "x" || textScaleAxis === "uniform") { | ||
| // X or uniform: size only — keep Y = X, lock Z | ||
| mesh.scaling.y = mesh.scaling.x; | ||
| mesh.scaling.z = textOrigScaleZ; | ||
| } else if (textScaleAxis === "y") { | ||
| // Y handle: size only — keep X = Y, lock Z | ||
| mesh.scaling.x = mesh.scaling.y; | ||
| mesh.scaling.z = textOrigScaleZ; | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| }); | ||
| if (mesh.savedMotionType != null && mesh.physics) { | ||
| mesh.physics.setMotionType(mesh.savedMotionType); | ||
| } | ||
|
|
||
| gizmoManager.gizmos.scaleGizmo.onDragStartObservable.add(() => { | ||
| const mesh = gizmoManager.attachedMesh; | ||
| flock.ensureUniqueGeometry(mesh); | ||
| mesh.computeWorldMatrix(true); | ||
| mesh.refreshBoundingInfo(); | ||
| originalBottomY = mesh.getBoundingInfo().boundingBox.minimumWorld.y; | ||
| textOrigScaleZ = mesh.scaling.z; | ||
| textScaleAxis = null; | ||
| mesh.computeWorldMatrix(true); | ||
|
|
||
| const motionType = mesh.physics?.getMotionType(); | ||
| mesh.savedMotionType = motionType; | ||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
|
|
||
| if ( | ||
| mesh.physics && | ||
| mesh.physics.getMotionType() !== | ||
| flock.BABYLON.PhysicsMotionType.ANIMATED | ||
| ) { | ||
| mesh.physics.setMotionType(flock.BABYLON.PhysicsMotionType.ANIMATED); | ||
| mesh.physics.disablePreStep = false; | ||
| } | ||
| if (block) { | ||
| const blockPosition = flock.getBlockPositionFromMesh(mesh); | ||
| setBlockXYZ(block, blockPosition.x, blockPosition.y, blockPosition.z); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
Drop or intentionally mark the unused bounds stub.
This handler is unreachable because the only toggleGizmo cases that referenced it are commented out. ESLint is already failing on it, so this will block merge until it is removed or renamed to _handleBoundsGizmo.
🧹 Minimal lint-fix if you want to keep the stub for later
-// Bounds: Allow the user to move the mesh
-// Legacy?
-function handleBoundsGizmo() {
+function _handleBoundsGizmo() {
gizmoManager.boundingBoxGizmoEnabled = true;
...
}As per coding guidelines, "Comments should reflect the current state of code only, keep discussion and historical notes in chat".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Bounds: Allow the user to move the mesh | |
| // Legacy? | |
| function handleBoundsGizmo() { | |
| gizmoManager.boundingBoxGizmoEnabled = true; | |
| gizmoManager.boundingBoxDragBehavior.onDragStartObservable.add(function () { | |
| const mesh = gizmoManager.attachedMesh; | |
| highlightBlockById(Blockly.getMainWorkspace(), blockId); | |
| }); | |
| if (!mesh?.physics) return; | |
| // Track bottom for correct visual anchoring | |
| let originalBottomY = 0; | |
| const motionType = mesh.physics.getMotionType?.(); | |
| mesh.savedMotionType = motionType; | |
| gizmoManager.gizmos.scaleGizmo.onDragObservable.add(() => { | |
| const mesh = gizmoManager.attachedMesh; | |
| if ( | |
| mesh.physics && | |
| motionType != null && | |
| motionType !== flock.BABYLON.PhysicsMotionType.STATIC | |
| ) { | |
| mesh.physics.setMotionType(flock.BABYLON.PhysicsMotionType.STATIC); | |
| mesh.physics.disablePreStep = false; | |
| } | |
| mesh.computeWorldMatrix(true); | |
| mesh.refreshBoundingInfo(); | |
| const block = meshMap[mesh?.metadata?.blockKey]; | |
| highlightBlockById(Blockly.getMainWorkspace(), block); | |
| }); | |
| const newBottomY = mesh.getBoundingInfo().boundingBox.minimumWorld.y; | |
| const deltaY = originalBottomY - newBottomY; | |
| mesh.position.y += deltaY; | |
| gizmoManager.boundingBoxDragBehavior.onDragEndObservable.add(function () { | |
| const mesh = gizmoManager.attachedMesh; | |
| const block = Blockly.getMainWorkspace().getBlockById( | |
| mesh?.metadata?.blockKey, | |
| ); | |
| if (gizmoManager.scaleGizmoEnabled) { | |
| switch (block?.type) { | |
| case "create_capsule": | |
| case "create_cylinder": | |
| mesh.scaling.z = mesh.scaling.x; | |
| break; | |
| case "create_3d_text": | |
| if (textScaleAxis === "z") { | |
| // Z handle: depth only — lock X and Y | |
| mesh.scaling.x = 1; | |
| mesh.scaling.y = 1; | |
| } else if (textScaleAxis === "x" || textScaleAxis === "uniform") { | |
| // X or uniform: size only — keep Y = X, lock Z | |
| mesh.scaling.y = mesh.scaling.x; | |
| mesh.scaling.z = textOrigScaleZ; | |
| } else if (textScaleAxis === "y") { | |
| // Y handle: size only — keep X = Y, lock Z | |
| mesh.scaling.x = mesh.scaling.y; | |
| mesh.scaling.z = textOrigScaleZ; | |
| } | |
| break; | |
| } | |
| } | |
| }); | |
| if (mesh.savedMotionType != null && mesh.physics) { | |
| mesh.physics.setMotionType(mesh.savedMotionType); | |
| } | |
| gizmoManager.gizmos.scaleGizmo.onDragStartObservable.add(() => { | |
| const mesh = gizmoManager.attachedMesh; | |
| flock.ensureUniqueGeometry(mesh); | |
| mesh.computeWorldMatrix(true); | |
| mesh.refreshBoundingInfo(); | |
| originalBottomY = mesh.getBoundingInfo().boundingBox.minimumWorld.y; | |
| textOrigScaleZ = mesh.scaling.z; | |
| textScaleAxis = null; | |
| mesh.computeWorldMatrix(true); | |
| const motionType = mesh.physics?.getMotionType(); | |
| mesh.savedMotionType = motionType; | |
| const block = meshMap[mesh?.metadata?.blockKey]; | |
| if ( | |
| mesh.physics && | |
| mesh.physics.getMotionType() !== | |
| flock.BABYLON.PhysicsMotionType.ANIMATED | |
| ) { | |
| mesh.physics.setMotionType(flock.BABYLON.PhysicsMotionType.ANIMATED); | |
| mesh.physics.disablePreStep = false; | |
| } | |
| if (block) { | |
| const blockPosition = flock.getBlockPositionFromMesh(mesh); | |
| setBlockXYZ(block, blockPosition.x, blockPosition.y, blockPosition.z); | |
| } | |
| }); | |
| } | |
| function _handleBoundsGizmo() { | |
| gizmoManager.boundingBoxGizmoEnabled = true; | |
| gizmoManager.boundingBoxDragBehavior.onDragStartObservable.add(function () { | |
| const mesh = gizmoManager.attachedMesh; | |
| if (!mesh?.physics) return; | |
| const motionType = mesh.physics.getMotionType?.(); | |
| mesh.savedMotionType = motionType; | |
| if ( | |
| mesh.physics && | |
| motionType != null && | |
| motionType !== flock.BABYLON.PhysicsMotionType.STATIC | |
| ) { | |
| mesh.physics.setMotionType(flock.BABYLON.PhysicsMotionType.STATIC); | |
| mesh.physics.disablePreStep = false; | |
| } | |
| const block = meshMap[mesh?.metadata?.blockKey]; | |
| highlightBlockById(Blockly.getMainWorkspace(), block); | |
| }); | |
| gizmoManager.boundingBoxDragBehavior.onDragEndObservable.add(function () { | |
| const mesh = gizmoManager.attachedMesh; | |
| if (mesh.savedMotionType != null && mesh.physics) { | |
| mesh.physics.setMotionType(mesh.savedMotionType); | |
| } | |
| mesh.computeWorldMatrix(true); | |
| const block = meshMap[mesh?.metadata?.blockKey]; | |
| if (block) { | |
| const blockPosition = flock.getBlockPositionFromMesh(mesh); | |
| setBlockXYZ(block, blockPosition.x, blockPosition.y, blockPosition.z); | |
| } | |
| }); | |
| } |
🧰 Tools
🪛 GitHub Check: eslint
[failure] 992-992:
'handleBoundsGizmo' is defined but never used. Allowed unused vars must match /^_/u
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/gizmos.js` around lines 990 - 1031, The function handleBoundsGizmo is an
unused/unreachable stub (its toggleGizmo callers are commented out) and is
causing an ESLint failure; either delete the entire handleBoundsGizmo function
block or rename it to _handleBoundsGizmo to mark it intentionally unused, making
sure to update any internal references such as
gizmoManager.boundingBoxGizmoEnabled,
gizmoManager.boundingBoxDragBehavior.onDragStartObservable, onDragEndObservable,
mesh.savedMotionType, meshMap and the calls to highlightBlockById/setBlockXYZ so
they remain correct if you keep it; if you retain it for future use, rename to
_handleBoundsGizmo and add a short inline comment noting it’s intentionally
dormant.
| if (gizmoManager.attachedMesh) { | ||
| resetAttachedMesh(); | ||
| blockKey = findParentWithBlockId(gizmoManager.attachedMesh)?.metadata | ||
| ?.blockKey; | ||
| } | ||
| let pickedMesh = event.pickInfo.pickedMesh; | ||
|
|
||
| gizmoManager.gizmos.scaleGizmo.onDragEndObservable.add(() => { | ||
| const mesh = gizmoManager.attachedMesh; | ||
| const block = meshMap[mesh?.metadata?.blockKey]; | ||
| textScaleAxis = null; | ||
| if (pickedMesh && pickedMesh.name !== "ground") { | ||
| const position = pickedMesh.getAbsolutePosition(); | ||
|
|
||
| if (mesh.savedMotionType != null) { | ||
| mesh.physics.setMotionType(mesh.savedMotionType); | ||
| } | ||
| // Round the coordinates to 2 decimal places | ||
| const roundedPosition = roundVectorToFixed(position, 2); | ||
|
|
||
| flock.updatePhysics(mesh); | ||
| flock.printText({ | ||
| text: translate("position_readout").replace( | ||
| "{position}", | ||
| String(roundedPosition), | ||
| ), | ||
| duration: 30, | ||
| color: "black", | ||
| }); | ||
|
|
||
| try { | ||
| const ensureFreshBounds = (m) => { | ||
| m.computeWorldMatrix(true); | ||
| m.refreshBoundingInfo(); | ||
| return m.getBoundingInfo().boundingBox; | ||
| }; | ||
| if (flock.meshDebug) console.log(pickedMesh.parent); | ||
|
|
||
| const bbox = ensureFreshBounds(mesh); | ||
| if (pickedMesh.parent) { | ||
| pickedMesh = getRootMesh(pickedMesh.parent); | ||
| if (flock.meshDebug) console.log(pickedMesh.visibility); | ||
| pickedMesh.visibility = 0.001; | ||
| if (flock.meshDebug) console.log(pickedMesh.visibility); | ||
| } | ||
|
|
||
| const newBottomY = bbox.minimumWorld.y; | ||
| mesh.position.y += originalBottomY - newBottomY; | ||
| const block = meshMap[blockKey]; | ||
| highlightBlockById(Blockly.getMainWorkspace(), block); | ||
|
|
There was a problem hiding this comment.
Resolve the block from the newly picked mesh.
blockKey is taken from the previously attached mesh before pickedMesh is normalized. When the user selects a different mesh, this highlights the old block or nothing at all instead of the newly selected one.
🎯 Derive the block after `pickedMesh` has been finalized
- const block = meshMap[blockKey];
+ const pickedBlockKey =
+ findParentWithBlockId(pickedMesh)?.metadata?.blockKey ??
+ pickedMesh.metadata?.blockKey;
+ const block = meshMap[pickedBlockKey];
highlightBlockById(Blockly.getMainWorkspace(), block);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (gizmoManager.attachedMesh) { | |
| resetAttachedMesh(); | |
| blockKey = findParentWithBlockId(gizmoManager.attachedMesh)?.metadata | |
| ?.blockKey; | |
| } | |
| let pickedMesh = event.pickInfo.pickedMesh; | |
| gizmoManager.gizmos.scaleGizmo.onDragEndObservable.add(() => { | |
| const mesh = gizmoManager.attachedMesh; | |
| const block = meshMap[mesh?.metadata?.blockKey]; | |
| textScaleAxis = null; | |
| if (pickedMesh && pickedMesh.name !== "ground") { | |
| const position = pickedMesh.getAbsolutePosition(); | |
| if (mesh.savedMotionType != null) { | |
| mesh.physics.setMotionType(mesh.savedMotionType); | |
| } | |
| // Round the coordinates to 2 decimal places | |
| const roundedPosition = roundVectorToFixed(position, 2); | |
| flock.updatePhysics(mesh); | |
| flock.printText({ | |
| text: translate("position_readout").replace( | |
| "{position}", | |
| String(roundedPosition), | |
| ), | |
| duration: 30, | |
| color: "black", | |
| }); | |
| try { | |
| const ensureFreshBounds = (m) => { | |
| m.computeWorldMatrix(true); | |
| m.refreshBoundingInfo(); | |
| return m.getBoundingInfo().boundingBox; | |
| }; | |
| if (flock.meshDebug) console.log(pickedMesh.parent); | |
| const bbox = ensureFreshBounds(mesh); | |
| if (pickedMesh.parent) { | |
| pickedMesh = getRootMesh(pickedMesh.parent); | |
| if (flock.meshDebug) console.log(pickedMesh.visibility); | |
| pickedMesh.visibility = 0.001; | |
| if (flock.meshDebug) console.log(pickedMesh.visibility); | |
| } | |
| const newBottomY = bbox.minimumWorld.y; | |
| mesh.position.y += originalBottomY - newBottomY; | |
| const block = meshMap[blockKey]; | |
| highlightBlockById(Blockly.getMainWorkspace(), block); | |
| if (gizmoManager.attachedMesh) { | |
| resetAttachedMesh(); | |
| blockKey = findParentWithBlockId(gizmoManager.attachedMesh)?.metadata | |
| ?.blockKey; | |
| } | |
| let pickedMesh = event.pickInfo.pickedMesh; | |
| if (pickedMesh && pickedMesh.name !== "ground") { | |
| const position = pickedMesh.getAbsolutePosition(); | |
| // Round the coordinates to 2 decimal places | |
| const roundedPosition = roundVectorToFixed(position, 2); | |
| flock.printText({ | |
| text: translate("position_readout").replace( | |
| "{position}", | |
| String(roundedPosition), | |
| ), | |
| duration: 30, | |
| color: "black", | |
| }); | |
| if (flock.meshDebug) console.log(pickedMesh.parent); | |
| if (pickedMesh.parent) { | |
| pickedMesh = getRootMesh(pickedMesh.parent); | |
| if (flock.meshDebug) console.log(pickedMesh.visibility); | |
| pickedMesh.visibility = 0.001; | |
| if (flock.meshDebug) console.log(pickedMesh.visibility); | |
| } | |
| const pickedBlockKey = | |
| findParentWithBlockId(pickedMesh)?.metadata?.blockKey ?? | |
| pickedMesh.metadata?.blockKey; | |
| const block = meshMap[pickedBlockKey]; | |
| highlightBlockById(Blockly.getMainWorkspace(), block); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/gizmos.js` around lines 1044 - 1077, blockKey is being read from the
previously attached mesh before the newly picked mesh is normalized, so
highlighting uses the wrong block; after you determine and normalize pickedMesh
(including the getRootMesh(pickedMesh.parent) step), derive the block key from
that finalized mesh by calling
findParentWithBlockId(finalPickedMesh)?.metadata?.blockKey (instead of using the
earlier blockKey), then look up meshMap[blockKey] and call highlightBlockById;
adjust references to gizmoManager.attachedMesh, resetAttachedMesh, pickedMesh,
getRootMesh, findParentWithBlockId, meshMap, and highlightBlockById accordingly.
Summary
toggleGizmoswitch statement (not strictly required for this PR but nice to have!)Claude Sonnet 4.6 was used for advice about the refactor and suggested the code for the camera focus. All code changes made by hand.
Contributes to #360
Summary by CodeRabbit
New Features
Improvements