From 741edde4f0d8195c8530ba0f72429acc63382a43 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:20:29 +0100 Subject: [PATCH 1/8] Fix bug with multiple selections --- ui/blocklyutil.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/blocklyutil.js b/ui/blocklyutil.js index 154a7dcc4..c998544a9 100644 --- a/ui/blocklyutil.js +++ b/ui/blocklyutil.js @@ -74,9 +74,11 @@ export function restoreBlockFocus(workspace, blockId) { const block = workspace.getBlockById(blockId); if (!block) return; - ensureAddMenuSelectionCleanup(workspace); - clearAddMenuHighlight(workspace, blockId); - trackBlockHighlight(workspace, blockId); + // On a view switch (canvas -> code) just bring the block back into view. + // Deliberately do NOT select or focus it: re-selecting armed the persistent + // getRestoredFocusableNode override, which hijacked focus on the next tap + // (first tap showed no toolbar) and left a stale selection ring on the old + // block. Leaving nothing selected means the next tap selects cleanly. scrollToBlockTopParentLeft(workspace, blockId); } From 0e9b64cbd3c18080bb6aeaef25fa7e819e9ef5e3 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:30:36 +0100 Subject: [PATCH 2/8] Add eye button to context menu --- main/blocklyinit.js | 28 +++++++++++++++++++++++++++- ui/gizmos.js | 25 +++++++++++++++---------- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 5b1dd5d11..9efe679b7 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -35,6 +35,8 @@ import { defineSensingBlocks } from '../blocks/sensing.js'; import { defineTextBlocks } from '../blocks/text.js'; import { defineGenerators } from '../generators/generators.js'; import { registerCustomCommentIcon } from './customCommentIcon.js'; +import { getMeshFromBlock } from '../ui/blockmesh.js'; +import { showCanvasView } from './view.js'; import { toolbox as toolboxDef } from '../toolbox.js'; // Blockly v13 moved variable methods off the workspace onto VariableMap/Variables. @@ -2769,7 +2771,16 @@ export function createBlocklyWorkspace() { ); commentBtn.innerHTML = commentAddSvg; - blockToolbar.append(duplicateBtn, detachBtn, commentBtn, deleteBtn); + const viewBtn = document.createElement('button'); + viewBtn.type = 'button'; + viewBtn.className = 'fc-block-toolbar-btn'; + viewBtn.setAttribute('aria-label', 'View in canvas'); + viewBtn.innerHTML = mkFaSvg( + '', + '0 0 576 512' + ); + + blockToolbar.append(duplicateBtn, detachBtn, commentBtn, viewBtn, deleteBtn); let toolbarBlock = null; let toolbarShowTimer = null; @@ -2794,6 +2805,9 @@ export function createBlocklyWorkspace() { const hasComment = block.getCommentText() !== null; commentBtn.setAttribute('aria-label', hasComment ? 'Delete comment' : 'Add comment'); commentBtn.innerHTML = hasComment ? commentDeleteSvg : commentAddSvg; + let mesh = null; + try { mesh = getMeshFromBlock(block); } catch { /* scene not ready */ } + viewBtn.disabled = !mesh || mesh.name === 'ground'; positionBlockToolbar(); blockToolbar.classList.add('visible'); } @@ -2872,6 +2886,18 @@ export function createBlocklyWorkspace() { hideBlockToolbar(); }); + viewBtn.addEventListener('pointerdown', async (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!toolbarBlock || viewBtn.disabled) return; + const block = toolbarBlock; + hideBlockToolbar(); + showCanvasView(); + const { viewMeshWithCamera } = await import('../ui/gizmos.js'); + window.currentBlock = block; + viewMeshWithCamera(block); + }); + deleteBtn.addEventListener('pointerdown', (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/ui/gizmos.js b/ui/gizmos.js index 4d92b8c72..4b253fb01 100644 --- a/ui/gizmos.js +++ b/ui/gizmos.js @@ -395,8 +395,8 @@ function deleteBlockWithUndo(blockId) { } } -function focusCameraOnMesh() { - let mesh = gizmoManager.attachedMesh; +function focusCameraOnMesh(overrideMesh) { + let mesh = overrideMesh ?? gizmoManager.attachedMesh; if (mesh && mesh.name === 'ground') mesh = null; if (!mesh && window.currentBlock) { mesh = getMeshFromBlock(window.currentBlock); @@ -452,19 +452,24 @@ function applyMeshSelection(pickedMesh, pickedPoint) { } } -function viewMeshWithCamera() { - let mesh = gizmoManager.attachedMesh; - if (mesh && mesh.name === 'ground') mesh = null; - - if (!mesh && window.currentBlock) { - mesh = getMeshFromBlock(window.currentBlock); - if (mesh && mesh.name === 'ground') mesh = null; +export function viewMeshWithCamera(block) { + let mesh; + if (block) { + mesh = getMeshFromBlock(block); + if (mesh?.name === 'ground') mesh = null; + } else { + mesh = gizmoManager.attachedMesh; + if (mesh?.name === 'ground') mesh = null; + if (!mesh && window.currentBlock) { + mesh = getMeshFromBlock(window.currentBlock); + if (mesh?.name === 'ground') mesh = null; + } } const camera = flock.scene.activeCamera; if (!camera?.metadata?.following) { - if (mesh) focusCameraOnMesh(); + if (mesh) focusCameraOnMesh(mesh); return; } From 442eba73b11c7d05d558b5b2591b6629dad491d8 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:37:17 +0100 Subject: [PATCH 3/8] Add to desktop context too --- main/blocklyinit.js | 65 +++++++++++++++++++++++++++++++++++++++++---- ui/gizmos.js | 2 +- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 9efe679b7..d78506eca 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2262,6 +2262,56 @@ export function createBlocklyWorkspace() { }); })(); + // Add a context menu item to focus the canvas camera on a block's mesh. + (function registerViewInCanvasContextMenuItem() { + const registry = Blockly.ContextMenuRegistry.registry; + const id = 'viewBlockInCanvas'; + if (registry.getItem && registry.getItem(id)) return; + + function renderShortcut(label, shortcut) { + const wrapper = document.createElement('span'); + wrapper.style.cssText = + 'display:flex;align-items:center;justify-content:space-between;gap:1.5em;width:100%'; + const labelEl = document.createElement('span'); + labelEl.textContent = label; + const shortcutEl = document.createElement('span'); + shortcutEl.textContent = shortcut; + shortcutEl.style.color = 'var(--blockly-text-disabled, #aaa)'; + wrapper.append(labelEl, shortcutEl); + return wrapper; + } + + registry.register({ + id, + weight: 8, + displayText: () => { + const text = translate('view_in_canvas_option'); + const label = text === 'view_in_canvas_option' ? 'View in canvas' : text; + return renderShortcut(label, 'V'); + }, + preconditionFn: (scope) => { + const block = scope.block; + if (!block || block.isInFlyout) return 'hidden'; + try { + const mesh = getMeshFromBlock(block); + return mesh && mesh.name !== 'ground' ? 'enabled' : 'hidden'; + } catch { + return 'hidden'; + } + }, + callback: (scope) => { + const block = scope.block; + if (!block) return; + showCanvasView(); + import('../ui/gizmos.js').then(({ viewMeshWithCamera }) => { + window.currentBlock = block; + viewMeshWithCamera(block); + }); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + }); + })(); + // Reorder block context menu items for better grouping. // Cut/copy/paste are registered at weights 1/2/3; push everything else above that. (function adjustBlockContextMenuWeights() { @@ -2270,10 +2320,11 @@ export function createBlocklyWorkspace() { const weights = { blockDuplicate: 9, detachBlockWithShortcut: 10, - blockComment: 11, - blockInline: 12, - blockCollapseExpand: 13, - blockDisable: 14, + viewBlockInCanvas: 10.5, + blockComment: 12, + blockInline: 13, + blockCollapseExpand: 14, + blockDisable: 15, blockDelete: 20, blockHelp: 999, }; @@ -2806,7 +2857,11 @@ export function createBlocklyWorkspace() { commentBtn.setAttribute('aria-label', hasComment ? 'Delete comment' : 'Add comment'); commentBtn.innerHTML = hasComment ? commentDeleteSvg : commentAddSvg; let mesh = null; - try { mesh = getMeshFromBlock(block); } catch { /* scene not ready */ } + try { + mesh = getMeshFromBlock(block); + } catch { + /* scene not ready */ + } viewBtn.disabled = !mesh || mesh.name === 'ground'; positionBlockToolbar(); blockToolbar.classList.add('visible'); diff --git a/ui/gizmos.js b/ui/gizmos.js index 4b253fb01..30d3512c8 100644 --- a/ui/gizmos.js +++ b/ui/gizmos.js @@ -134,7 +134,7 @@ function registerBindings() { }; // Focus on mesh with V or F key KeyboardDispatcher.on('GIZMO', 'KeyF', noMod(focusCameraOnMesh)); - KeyboardDispatcher.on('GIZMO', 'KeyV', noMod(viewMeshWithCamera)); + KeyboardDispatcher.on('GIZMO', 'KeyV', noMod(() => viewMeshWithCamera())); // Delete selected mesh with Del key KeyboardDispatcher.on('GIZMO', 'Delete', (e) => { if (!gizmoManager?.attachedMesh) return; From 71d356903227b9b61e7df76c55d57ecd6a7ef4f8 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:39:58 +0100 Subject: [PATCH 4/8] No eye button if not relevant --- main/blocklyinit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index d78506eca..5bf84658c 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2862,7 +2862,7 @@ export function createBlocklyWorkspace() { } catch { /* scene not ready */ } - viewBtn.disabled = !mesh || mesh.name === 'ground'; + viewBtn.style.display = (!mesh || mesh.name === 'ground') ? 'none' : ''; positionBlockToolbar(); blockToolbar.classList.add('visible'); } @@ -2944,7 +2944,7 @@ export function createBlocklyWorkspace() { viewBtn.addEventListener('pointerdown', async (e) => { e.preventDefault(); e.stopPropagation(); - if (!toolbarBlock || viewBtn.disabled) return; + if (!toolbarBlock || viewBtn.style.display === 'none') return; const block = toolbarBlock; hideBlockToolbar(); showCanvasView(); From 2c3ef5fc10f579b4127d0f75084588d2ad41dd0c Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:49:39 +0100 Subject: [PATCH 5/8] Circular import fix --- main/blocklyinit.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 5bf84658c..a61a45063 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -36,7 +36,6 @@ import { defineTextBlocks } from '../blocks/text.js'; import { defineGenerators } from '../generators/generators.js'; import { registerCustomCommentIcon } from './customCommentIcon.js'; import { getMeshFromBlock } from '../ui/blockmesh.js'; -import { showCanvasView } from './view.js'; import { toolbox as toolboxDef } from '../toolbox.js'; // Blockly v13 moved variable methods off the workspace onto VariableMap/Variables. @@ -2302,11 +2301,13 @@ export function createBlocklyWorkspace() { callback: (scope) => { const block = scope.block; if (!block) return; - showCanvasView(); - import('../ui/gizmos.js').then(({ viewMeshWithCamera }) => { - window.currentBlock = block; - viewMeshWithCamera(block); - }); + Promise.all([import('./view.js'), import('../ui/gizmos.js')]).then( + ([{ showCanvasView }, { viewMeshWithCamera }]) => { + showCanvasView(); + window.currentBlock = block; + viewMeshWithCamera(block); + } + ); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, }); @@ -2947,8 +2948,11 @@ export function createBlocklyWorkspace() { if (!toolbarBlock || viewBtn.style.display === 'none') return; const block = toolbarBlock; hideBlockToolbar(); + const [{ showCanvasView }, { viewMeshWithCamera }] = await Promise.all([ + import('./view.js'), + import('../ui/gizmos.js'), + ]); showCanvasView(); - const { viewMeshWithCamera } = await import('../ui/gizmos.js'); window.currentBlock = block; viewMeshWithCamera(block); }); From 10278c4abcbf4f5636dcc2f8067967e5b9b627e9 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:51:03 +0100 Subject: [PATCH 6/8] Don't exclude standalones --- main/blocklyinit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index a61a45063..5f522240f 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2876,7 +2876,7 @@ export function createBlocklyWorkspace() { } const isToolbarBlock = (block) => - block && !block.isInFlyout && !block.isShadow() && !block.outputConnection; + block && !block.isInFlyout && !block.isShadow(); workspace.addChangeListener((e) => { if (e.type === Blockly.Events.SELECTED) { From 21ce513b529c4de60dec1915a8deee67e8779811 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:55:47 +0100 Subject: [PATCH 7/8] Shift bar on small screen --- main/blocklyinit.js | 15 ++++++++++++++- style.css | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 5f522240f..7760f868c 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2847,8 +2847,21 @@ export function createBlocklyWorkspace() { const svgRoot = toolbarBlock.getSvgRoot?.(); if (!svgRoot) return; const rect = svgRoot.getBoundingClientRect(); - blockToolbar.style.left = `${Math.round(rect.left + rect.width / 2)}px`; + const blockCenterX = Math.round(rect.left + rect.width / 2); + blockToolbar.style.left = `${blockCenterX}px`; blockToolbar.style.top = `${Math.round(rect.top)}px`; + blockToolbar.style.removeProperty('--caret-shift'); + + // Clamp to viewport; shift caret opposite so it still points at the block + const margin = 8; + const tbRect = blockToolbar.getBoundingClientRect(); + let adj = 0; + if (tbRect.left < margin) adj = margin - tbRect.left; + else if (tbRect.right > window.innerWidth - margin) adj = window.innerWidth - margin - tbRect.right; + if (adj !== 0) { + blockToolbar.style.left = `${blockCenterX + adj}px`; + blockToolbar.style.setProperty('--caret-shift', `${-adj}px`); + } } function showBlockToolbar(block) { diff --git a/style.css b/style.css index 27f2830c0..c12c42d77 100644 --- a/style.css +++ b/style.css @@ -2174,7 +2174,7 @@ svg.blocklyTrashcanFlyout { content: ''; position: absolute; bottom: -7px; - left: 50%; + left: calc(50% + var(--caret-shift, 0px)); transform: translateX(-50%); border: 7px solid transparent; border-top-color: var(--color-border, #ddd); @@ -2185,7 +2185,7 @@ svg.blocklyTrashcanFlyout { content: ''; position: absolute; bottom: -5px; - left: 50%; + left: calc(50% + var(--caret-shift, 0px)); transform: translateX(-50%); border: 5px solid transparent; border-top-color: var(--color-menu, #f9f9f9); From c3984fbe6cb31450dc275643823ef76ba223a67e Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:57:05 +0100 Subject: [PATCH 8/8] Hide detach if nothing to detach --- main/blocklyinit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 7760f868c..8aeee5e70 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2866,7 +2866,7 @@ export function createBlocklyWorkspace() { function showBlockToolbar(block) { toolbarBlock = block; - detachBtn.disabled = !isDetachable(block); + detachBtn.style.display = isDetachable(block) ? '' : 'none'; const hasComment = block.getCommentText() !== null; commentBtn.setAttribute('aria-label', hasComment ? 'Delete comment' : 'Add comment'); commentBtn.innerHTML = hasComment ? commentDeleteSvg : commentAddSvg;