From 5f0b96984263f36e0b9fff7ec7327aa2365a4a75 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:36:24 +0100 Subject: [PATCH 01/12] Basic context menu --- main/blocklyinit.js | 121 ++++++++++++++++++++++++++++++++++++++++++++ style.css | 79 +++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index bf29afa0..0bb319c8 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2667,6 +2667,127 @@ export function createBlocklyWorkspace() { { capture: true } ); + // ---- Tablet floating block toolbar ---- + if (navigator.maxTouchPoints > 0) { + const blockToolbar = document.createElement('div'); + blockToolbar.className = 'fc-block-toolbar'; + blockToolbar.setAttribute('role', 'toolbar'); + document.body.appendChild(blockToolbar); + + const mkSvg = (paths) => + `${paths}`; + + const copyBtn = document.createElement('button'); + copyBtn.type = 'button'; + copyBtn.className = 'fc-block-toolbar-btn'; + copyBtn.setAttribute('aria-label', translate('COPY_SHORTCUT') || 'Copy'); + copyBtn.innerHTML = mkSvg( + '' + ); + + const pasteBtn = document.createElement('button'); + pasteBtn.type = 'button'; + pasteBtn.className = 'fc-block-toolbar-btn'; + pasteBtn.setAttribute('aria-label', translate('PASTE_SHORTCUT') || 'Paste'); + pasteBtn.innerHTML = mkSvg( + '' + ); + + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.className = 'fc-block-toolbar-btn fc-block-toolbar-btn--delete'; + deleteBtn.setAttribute('aria-label', 'Delete'); + deleteBtn.innerHTML = mkSvg( + '' + ); + + blockToolbar.append(copyBtn, pasteBtn, deleteBtn); + + let toolbarBlock = null; + let toolbarShowTimer = null; + + function positionBlockToolbar() { + if (!toolbarBlock) return; + const svgRoot = toolbarBlock.getSvgRoot?.(); + if (!svgRoot) return; + const rect = svgRoot.getBoundingClientRect(); + blockToolbar.style.left = `${Math.round(rect.left + rect.width / 2)}px`; + blockToolbar.style.top = `${Math.round(rect.top)}px`; + } + + function showBlockToolbar(block) { + toolbarBlock = block; + pasteBtn.disabled = !Blockly.clipboard?.getLastCopiedData?.(); + positionBlockToolbar(); + blockToolbar.classList.add('visible'); + } + + function hideBlockToolbar() { + clearTimeout(toolbarShowTimer); + toolbarShowTimer = null; + toolbarBlock = null; + blockToolbar.classList.remove('visible'); + } + + const isToolbarBlock = (block) => + block && !block.isInFlyout && !block.isShadow() && !block.outputConnection; + + workspace.addChangeListener((e) => { + if (e.type === Blockly.Events.SELECTED) { + clearTimeout(toolbarShowTimer); + toolbarShowTimer = null; + if (e.newElementId) { + const block = workspace.getBlockById(e.newElementId); + if (isToolbarBlock(block)) { + toolbarShowTimer = setTimeout(() => showBlockToolbar(block), 400); + } else { + hideBlockToolbar(); + } + } else { + hideBlockToolbar(); + } + } else if ( + (e.type === Blockly.Events.BLOCK_MOVE || e.type === Blockly.Events.VIEWPORT_CHANGE) && + toolbarBlock + ) { + positionBlockToolbar(); + } else if (e.type === Blockly.Events.BLOCK_DRAG && e.isStart) { + hideBlockToolbar(); + } + }); + + copyBtn.addEventListener('pointerdown', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!toolbarBlock) return; + copyWithoutToast(toolbarBlock); + pasteBtn.disabled = false; + }); + + pasteBtn.addEventListener('pointerdown', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!toolbarBlock) return; + const data = Blockly.clipboard?.getLastCopiedData?.(); + if (!data) return; + const block = toolbarBlock; + const rect = block.getSvgRoot?.()?.getBoundingClientRect(); + if (rect) lastCM = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; + pasteAsChildOrHere(block, workspace, data); + }); + + deleteBtn.addEventListener('pointerdown', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!toolbarBlock) return; + const block = toolbarBlock; + hideBlockToolbar(); + Blockly.Events.setGroup('toolbar_delete'); + block.dispose(true); + Blockly.Events.setGroup(false); + }); + } + initializeTheme(); // Register comment options for workspace comments diff --git a/style.css b/style.css index 966fa2b7..6d521544 100644 --- a/style.css +++ b/style.css @@ -1916,4 +1916,83 @@ kbd { svg.blocklyTrashcanFlyout { background: gray; +} + +/* ---- Tablet floating block toolbar ---- */ +.fc-block-toolbar { + position: fixed; + display: flex; + gap: 2px; + background: var(--color-menu, #f9f9f9); + border: 1px solid var(--color-border, #ddd); + border-radius: 10px; + padding: 5px; + box-shadow: 0 3px 12px var(--color-shadow-medium, rgba(0, 0, 0, 0.2)); + z-index: 200; + pointer-events: none; + opacity: 0; + transform: translate(-50%, calc(-100% - 10px)); + transition: opacity 0.12s; + user-select: none; + -webkit-user-select: none; +} + +.fc-block-toolbar::after { + content: ''; + position: absolute; + bottom: -7px; + left: 50%; + transform: translateX(-50%); + border: 7px solid transparent; + border-top-color: var(--color-border, #ddd); + border-bottom: 0; +} + +.fc-block-toolbar::before { + content: ''; + position: absolute; + bottom: -5px; + left: 50%; + transform: translateX(-50%); + border: 5px solid transparent; + border-top-color: var(--color-menu, #f9f9f9); + border-bottom: 0; + z-index: 1; +} + +.fc-block-toolbar.visible { + opacity: 1; + pointer-events: auto; +} + +.fc-block-toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border: none; + border-radius: 7px; + background: transparent; + color: var(--color-text-primary, #000); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; +} + +.fc-block-toolbar-btn:active { + background: var(--color-menu-hover, #f0f0f0); +} + +.fc-block-toolbar-btn[disabled] { + opacity: 0.3; +} + +.fc-block-toolbar-btn--delete { + color: #c0392b; +} + +[data-theme='dark'] .fc-block-toolbar-btn--delete, +[data-theme='dark-contrast'] .fc-block-toolbar-btn--delete { + color: #e57373; } \ No newline at end of file From 0259f41205210a79625bac733727f0d3c43696e0 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:17:25 +0100 Subject: [PATCH 02/12] Add delete/duplicate --- main/blocklyinit.js | 80 +++++++++++++++++++++++---------------------- style.css | 4 +-- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 0bb319c8..fe978a82 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2674,34 +2674,26 @@ export function createBlocklyWorkspace() { blockToolbar.setAttribute('role', 'toolbar'); document.body.appendChild(blockToolbar); - const mkSvg = (paths) => - `${paths}`; - - const copyBtn = document.createElement('button'); - copyBtn.type = 'button'; - copyBtn.className = 'fc-block-toolbar-btn'; - copyBtn.setAttribute('aria-label', translate('COPY_SHORTCUT') || 'Copy'); - copyBtn.innerHTML = mkSvg( - '' - ); - - const pasteBtn = document.createElement('button'); - pasteBtn.type = 'button'; - pasteBtn.className = 'fc-block-toolbar-btn'; - pasteBtn.setAttribute('aria-label', translate('PASTE_SHORTCUT') || 'Paste'); - pasteBtn.innerHTML = mkSvg( - '' + const mkFaSvg = (path, vw = '0 0 448 512') => + `${path}`; + + const duplicateBtn = document.createElement('button'); + duplicateBtn.type = 'button'; + duplicateBtn.className = 'fc-block-toolbar-btn'; + duplicateBtn.setAttribute('aria-label', translate('duplicate_button') || 'Duplicate'); + duplicateBtn.innerHTML = mkFaSvg( + '' ); const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.className = 'fc-block-toolbar-btn fc-block-toolbar-btn--delete'; deleteBtn.setAttribute('aria-label', 'Delete'); - deleteBtn.innerHTML = mkSvg( - '' + deleteBtn.innerHTML = mkFaSvg( + '' ); - blockToolbar.append(copyBtn, pasteBtn, deleteBtn); + blockToolbar.append(duplicateBtn, deleteBtn); let toolbarBlock = null; let toolbarShowTimer = null; @@ -2717,7 +2709,6 @@ export function createBlocklyWorkspace() { function showBlockToolbar(block) { toolbarBlock = block; - pasteBtn.disabled = !Blockly.clipboard?.getLastCopiedData?.(); positionBlockToolbar(); blockToolbar.classList.add('visible'); } @@ -2756,24 +2747,18 @@ export function createBlocklyWorkspace() { } }); - copyBtn.addEventListener('pointerdown', (e) => { - e.preventDefault(); - e.stopPropagation(); - if (!toolbarBlock) return; - copyWithoutToast(toolbarBlock); - pasteBtn.disabled = false; - }); - - pasteBtn.addEventListener('pointerdown', (e) => { + duplicateBtn.addEventListener('pointerdown', (e) => { e.preventDefault(); e.stopPropagation(); if (!toolbarBlock) return; - const data = Blockly.clipboard?.getLastCopiedData?.(); - if (!data) return; const block = toolbarBlock; - const rect = block.getSvgRoot?.()?.getBoundingClientRect(); - if (rect) lastCM = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; - pasteAsChildOrHere(block, workspace, data); + Blockly.Events.setGroup('toolbar_duplicate'); + const json = Blockly.serialization.blocks.save(block, { includeShadows: true }); + delete json.next; + const copy = Blockly.serialization.blocks.append(json, workspace); + const orig = block.getRelativeToSurfaceXY(); + copy.moveTo(new Blockly.utils.Coordinate(orig.x + 30, orig.y + 30)); + Blockly.Events.setGroup(false); }); deleteBtn.addEventListener('pointerdown', (e) => { @@ -2781,10 +2766,27 @@ export function createBlocklyWorkspace() { e.stopPropagation(); if (!toolbarBlock) return; const block = toolbarBlock; - hideBlockToolbar(); - Blockly.Events.setGroup('toolbar_delete'); - block.dispose(true); - Blockly.Events.setGroup(false); + // Count only blocks that will actually be deleted: the block + its input + // descendants, but NOT the top-level next chain (which gets healed, not deleted). + const countDeleted = (b, followNext) => { + if (!b || b.isShadow()) return 0; + let n = 1; + for (const input of b.inputList) { + n += countDeleted(input.connection?.targetBlock(), true); + } + if (followNext) n += countDeleted(b.nextConnection?.targetBlock(), true); + return n; + }; + const count = countDeleted(block, false); + if (count > 1) { + const msg = (Blockly.Msg['DELETE_ALL_BLOCKS'] || 'Delete all %1 blocks?').replace('%1', count); + Blockly.dialog.confirm(msg, (ok) => { + if (ok) { hideBlockToolbar(); block.checkAndDelete(); } + }); + } else { + hideBlockToolbar(); + block.checkAndDelete(); + } }); } diff --git a/style.css b/style.css index 6d521544..49e89196 100644 --- a/style.css +++ b/style.css @@ -1984,14 +1984,12 @@ svg.blocklyTrashcanFlyout { background: var(--color-menu-hover, #f0f0f0); } -.fc-block-toolbar-btn[disabled] { - opacity: 0.3; -} .fc-block-toolbar-btn--delete { color: #c0392b; } + [data-theme='dark'] .fc-block-toolbar-btn--delete, [data-theme='dark-contrast'] .fc-block-toolbar-btn--delete { color: #e57373; From c0b5a4e50e3864a87d25d7b7324c7280b33c7d2c Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:20:08 +0100 Subject: [PATCH 03/12] Add detach --- main/blocklyinit.js | 28 +++++++++++++++++++++++++++- style.css | 4 ++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index fe978a82..6cf3a83a 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2693,11 +2693,25 @@ export function createBlocklyWorkspace() { '' ); - blockToolbar.append(duplicateBtn, deleteBtn); + const detachBtn = document.createElement('button'); + detachBtn.type = 'button'; + detachBtn.className = 'fc-block-toolbar-btn'; + detachBtn.setAttribute('aria-label', translate('detach_block_option') || 'Detach'); + detachBtn.innerHTML = mkFaSvg( + '', + '0 0 640 512' + ); + + blockToolbar.append(duplicateBtn, detachBtn, deleteBtn); let toolbarBlock = null; let toolbarShowTimer = null; + const isDetachable = (block) => + !!block?.getParent() || + !!block?.previousConnection?.targetConnection || + !!block?.outputConnection?.targetConnection; + function positionBlockToolbar() { if (!toolbarBlock) return; const svgRoot = toolbarBlock.getSvgRoot?.(); @@ -2709,6 +2723,7 @@ export function createBlocklyWorkspace() { function showBlockToolbar(block) { toolbarBlock = block; + detachBtn.disabled = !isDetachable(block); positionBlockToolbar(); blockToolbar.classList.add('visible'); } @@ -2761,6 +2776,17 @@ export function createBlocklyWorkspace() { Blockly.Events.setGroup(false); }); + detachBtn.addEventListener('pointerdown', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!toolbarBlock || !isDetachable(toolbarBlock)) return; + const block = toolbarBlock; + const healStack = !block.outputConnection?.isConnected(); + Blockly.Events.setGroup('toolbar_detach'); + block.unplug(healStack); + Blockly.Events.setGroup(false); + }); + deleteBtn.addEventListener('pointerdown', (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/style.css b/style.css index 49e89196..4272b43b 100644 --- a/style.css +++ b/style.css @@ -1985,6 +1985,10 @@ svg.blocklyTrashcanFlyout { } +.fc-block-toolbar-btn[disabled] { + opacity: 0.3; +} + .fc-block-toolbar-btn--delete { color: #c0392b; } From 4dcc833bc39fcd2753714a1eca0e3e4270cd8212 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:26:46 +0100 Subject: [PATCH 04/12] Update modal --- main/blocklyinit.js | 45 +++++++++++++++++++++++++++++++++ style.css | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 6cf3a83a..045c02a9 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2667,6 +2667,51 @@ export function createBlocklyWorkspace() { { capture: true } ); + // ---- Touch-friendly confirm dialog ---- + if (navigator.maxTouchPoints > 0) { + Blockly.dialog.setConfirm((message, callback) => { + const overlay = document.createElement('div'); + overlay.className = 'fc-confirm-overlay'; + + const dialog = document.createElement('div'); + dialog.className = 'fc-confirm-dialog'; + dialog.setAttribute('role', 'alertdialog'); + dialog.setAttribute('aria-modal', 'true'); + + const msg = document.createElement('p'); + msg.className = 'fc-confirm-message'; + msg.textContent = message; + + const btnRow = document.createElement('div'); + btnRow.className = 'fc-confirm-buttons'; + + // Icons: Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com + // License: https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. + const cancelBtn = document.createElement('button'); + cancelBtn.type = 'button'; + cancelBtn.className = 'fc-confirm-btn fc-confirm-btn--cancel'; + cancelBtn.setAttribute('aria-label', translate('cancel') || 'Cancel'); + cancelBtn.innerHTML = ''; + + const okBtn = document.createElement('button'); + okBtn.type = 'button'; + okBtn.className = 'fc-confirm-btn fc-confirm-btn--ok'; + okBtn.setAttribute('aria-label', Blockly.Msg['DIALOG_OK'] || 'OK'); + okBtn.innerHTML = ''; + + const close = (result) => { overlay.remove(); callback(result); }; + + cancelBtn.addEventListener('pointerdown', () => close(false)); + okBtn.addEventListener('pointerdown', () => close(true)); + overlay.addEventListener('pointerdown', (e) => { if (e.target === overlay) close(false); }); + + btnRow.append(cancelBtn, okBtn); + dialog.append(msg, btnRow); + overlay.append(dialog); + document.body.appendChild(overlay); + }); + } + // ---- Tablet floating block toolbar ---- if (navigator.maxTouchPoints > 0) { const blockToolbar = document.createElement('div'); diff --git a/style.css b/style.css index 4272b43b..298628b0 100644 --- a/style.css +++ b/style.css @@ -1918,6 +1918,67 @@ svg.blocklyTrashcanFlyout { background: gray; } +/* ---- Touch confirm dialog ---- */ +.fc-confirm-overlay { + position: fixed; + inset: 0; + background: var(--color-bg-overlay, rgba(0, 0, 0, 0.4)); + z-index: 300; + display: flex; + align-items: center; + justify-content: center; +} + +.fc-confirm-dialog { + background: var(--color-menu, #f9f9f9); + border-radius: 16px; + padding: 24px 20px 16px; + max-width: min(340px, 90vw); + width: 100%; + box-shadow: 0 8px 32px var(--color-shadow-medium, rgba(0, 0, 0, 0.3)); +} + +.fc-confirm-message { + margin: 0 0 20px; + font-size: 18px; + line-height: 1.4; + color: var(--color-text-primary, #000); + text-align: center; +} + +.fc-confirm-buttons { + display: flex; + gap: 10px; +} + +.fc-confirm-btn { + flex: 1; + min-height: 64px; + border: none; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; +} + +.fc-confirm-btn:focus, +.fc-confirm-btn:focus-visible { + outline: none; +} + +.fc-confirm-btn--cancel { + background: var(--color-button-bg, #e0e0e0); + color: var(--color-text-primary, #000); +} + +.fc-confirm-btn--ok { + background: var(--color-button-primary, #004d4d); + color: var(--color-text-on-primary, #fff); +} + /* ---- Tablet floating block toolbar ---- */ .fc-block-toolbar { position: fixed; From 28ca9cb0338089eebff711adbe942b31e7860c3b Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:40:33 +0100 Subject: [PATCH 05/12] Toast to remind about undo --- locale/de.js | 1 + locale/en.js | 2282 +++++++++++++++++++++---------------------- locale/es.js | 1 + locale/fr.js | 1 + locale/it.js | 1 + locale/pl.js | 1 + locale/pt.js | 1 + locale/sv.js | 1 + main/blocklyinit.js | 10 +- 9 files changed, 1126 insertions(+), 1173 deletions(-) diff --git a/locale/de.js b/locale/de.js index 9c6e2975..8ff58f5b 100644 --- a/locale/de.js +++ b/locale/de.js @@ -1313,4 +1313,5 @@ export default { KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT: "Halte %1 gedrückt und verwende die Pfeiltasten zum freien Bewegen, dann %2 zum Bestätigen", // machine KEYBOARD_NAV_COPIED_HINT: "Kopiert. Drücke %1 zum Einfügen.", // machine KEYBOARD_NAV_CUT_HINT: "Ausgeschnitten. Drücke %1 zum Einfügen.", // machine + DELETE_UNDO_HINT: "Verwende die Schaltfläche Rückgängig, um gelöschte Blöcke wiederherzustellen.", // machine }; diff --git a/locale/en.js b/locale/en.js index bc2cdec2..5fef51cf 100644 --- a/locale/en.js +++ b/locale/en.js @@ -1,1388 +1,1326 @@ export default { // Blockly category message keys for custom categories - CATEGORY_SCENE: "Scene", - CATEGORY_MESHES: "Objects", - CATEGORY_XR: "XR", - CATEGORY_EFFECTS: "Effects", - CATEGORY_CAMERA: "Camera", - CATEGORY_EVENTS: "Events", - CATEGORY_TRANSFORM: "Transform", - CATEGORY_PHYSICS: "Physics", - CATEGORY_CONNECT: "Connect", - CATEGORY_COMBINE: "Combine", - CATEGORY_ANIMATE: "Animate", - CATEGORY_KEYFRAME: "Keyframe", - CATEGORY_CONTROL: "Control", - CATEGORY_CONDITION: "Condition", - CATEGORY_SENSING: "Sensing", - CATEGORY_TEXT: "Text", - CATEGORY_STRINGS: "Strings", - CATEGORY_MATERIALS: "Materials", - CATEGORY_SOUND: "Sound", - CATEGORY_VARIABLES: "Data", - CATEGORY_VARIABLES_SUBCATEGORY: "Variables", - CATEGORY_LISTS: "Lists", - CATEGORY_MATH: "Math", - CATEGORY_FUNCTIONS: "Functions", - CATEGORY_SNIPPETS: "Snippets", - CATEGORY_MOVEMENT: "Movement", + CATEGORY_SCENE: 'Scene', + CATEGORY_MESHES: 'Objects', + CATEGORY_XR: 'XR', + CATEGORY_EFFECTS: 'Effects', + CATEGORY_CAMERA: 'Camera', + CATEGORY_EVENTS: 'Events', + CATEGORY_TRANSFORM: 'Transform', + CATEGORY_PHYSICS: 'Physics', + CATEGORY_CONNECT: 'Connect', + CATEGORY_COMBINE: 'Combine', + CATEGORY_ANIMATE: 'Animate', + CATEGORY_KEYFRAME: 'Keyframe', + CATEGORY_CONTROL: 'Control', + CATEGORY_CONDITION: 'Condition', + CATEGORY_SENSING: 'Sensing', + CATEGORY_TEXT: 'Text', + CATEGORY_STRINGS: 'Strings', + CATEGORY_MATERIALS: 'Materials', + CATEGORY_SOUND: 'Sound', + CATEGORY_VARIABLES: 'Data', + CATEGORY_VARIABLES_SUBCATEGORY: 'Variables', + CATEGORY_LISTS: 'Lists', + CATEGORY_MATH: 'Math', + CATEGORY_FUNCTIONS: 'Functions', + CATEGORY_SNIPPETS: 'Snippets', + CATEGORY_MOVEMENT: 'Movement', // Color picker translations - choose_a_color: "Choose a Color", - close_color_picker: "Close color picker", - quick_colors: "Quick Colors", - skin_tones: "Skin Tones", - custom_color: "Custom Color", - pick_from_screen: "Pick from Screen", - more_colors: "More Colors", - rgb_values: "RGB Values", - css_color: "CSS Color", - cancel: "Cancel", - use_this_color: "Use This Color", + choose_a_color: 'Choose a Color', + close_color_picker: 'Close color picker', + quick_colors: 'Quick Colors', + skin_tones: 'Skin Tones', + custom_color: 'Custom Color', + pick_from_screen: 'Pick from Screen', + more_colors: 'More Colors', + rgb_values: 'RGB Values', + css_color: 'CSS Color', + cancel: 'Cancel', + use_this_color: 'Use This Color', // Color picker compact translations - lightness: "Lightness", - palette: "Palette", - color_palette: "Color palette", - hue_slider: "Hue slider", - surprise_color: "Surprise color", - pick_color_from_screen: "Pick color from screen", - more_options: "More options", - hex_placeholder: "ff0000 or red", - skin_tone: "Skin tone", - color_wheel_aria: "Color wheel: use arrow keys to select hue and saturation", + lightness: 'Lightness', + palette: 'Palette', + color_palette: 'Color palette', + hue_slider: 'Hue slider', + surprise_color: 'Surprise color', + pick_color_from_screen: 'Pick color from screen', + more_options: 'More options', + hex_placeholder: 'ff0000 or red', + skin_tone: 'Skin tone', + color_wheel_aria: 'Color wheel: use arrow keys to select hue and saturation', // Palette names - palette_bright: "Bright", - palette_earthy: "Earthy", - palette_pastel: "Pastel", - palette_neon: "Neon", - palette_sunset: "Sunset", + palette_bright: 'Bright', + palette_earthy: 'Earthy', + palette_pastel: 'Pastel', + palette_neon: 'Neon', + palette_sunset: 'Sunset', // Color names - color_red: "Red", - color_orange: "Orange", - color_yellow: "Yellow", - color_green: "Green", - color_dark_green: "Dark Green", - color_cyan: "Cyan", - color_blue: "Blue", - color_purple: "Purple", - color_pink: "Pink", - color_brown: "Brown", - color_black: "Black", - color_white: "White", - color_forest_green: "Forest Green", - color_clay: "Clay", - color_walnut: "Walnut", - color_olive: "Olive", - color_lime: "Lime", - color_sky_blue: "Sky Blue", - color_water: "Water", - color_sunflower: "Sunflower", - color_sand: "Sand", - color_terracotta: "Terracotta", - color_cream: "Cream", - color_peach: "Peach", - color_rose: "Rose", - color_lilac: "Lilac", - color_lavender: "Lavender", - color_sky: "Sky", - color_aqua: "Aqua", - color_turquoise: "Turquoise", - color_mint: "Mint", - color_pale_green: "Pale Green", - color_magenta: "Magenta", - color_violet: "Violet", - color_electric_blue: "Electric Blue", - color_cyan_glow: "Cyan Glow", - color_neon_green: "Neon Green", - color_lime_glow: "Lime Glow", - color_lemon: "Lemon", - color_amber: "Amber", - color_neon_orange: "Neon Orange", - color_hot_pink: "Hot Pink", - color_midnight: "Midnight", - color_indigo: "Indigo", - color_plum: "Plum", - color_amethyst: "Amethyst", - color_orchid: "Orchid", - color_fuchsia: "Fuchsia", - color_rose_red: "Rose Red", - color_coral: "Coral", - color_apricot: "Apricot", - color_golden: "Golden", - color_dark_orange: "Dark Orange", - color_teal: "Teal", - color_dodger_blue: "Dodger Blue", - color_blue_violet: "Blue Violet", - color_deep_pink: "Deep Pink", - color_light_gray: "Light Gray", - color_dim_gray: "Dim Gray", - color_saddle_brown: "Saddle Brown", - color_light_coral: "Light Coral", + color_red: 'Red', + color_orange: 'Orange', + color_yellow: 'Yellow', + color_green: 'Green', + color_dark_green: 'Dark Green', + color_cyan: 'Cyan', + color_blue: 'Blue', + color_purple: 'Purple', + color_pink: 'Pink', + color_brown: 'Brown', + color_black: 'Black', + color_white: 'White', + color_forest_green: 'Forest Green', + color_clay: 'Clay', + color_walnut: 'Walnut', + color_olive: 'Olive', + color_lime: 'Lime', + color_sky_blue: 'Sky Blue', + color_water: 'Water', + color_sunflower: 'Sunflower', + color_sand: 'Sand', + color_terracotta: 'Terracotta', + color_cream: 'Cream', + color_peach: 'Peach', + color_rose: 'Rose', + color_lilac: 'Lilac', + color_lavender: 'Lavender', + color_sky: 'Sky', + color_aqua: 'Aqua', + color_turquoise: 'Turquoise', + color_mint: 'Mint', + color_pale_green: 'Pale Green', + color_magenta: 'Magenta', + color_violet: 'Violet', + color_electric_blue: 'Electric Blue', + color_cyan_glow: 'Cyan Glow', + color_neon_green: 'Neon Green', + color_lime_glow: 'Lime Glow', + color_lemon: 'Lemon', + color_amber: 'Amber', + color_neon_orange: 'Neon Orange', + color_hot_pink: 'Hot Pink', + color_midnight: 'Midnight', + color_indigo: 'Indigo', + color_plum: 'Plum', + color_amethyst: 'Amethyst', + color_orchid: 'Orchid', + color_fuchsia: 'Fuchsia', + color_rose_red: 'Rose Red', + color_coral: 'Coral', + color_apricot: 'Apricot', + color_golden: 'Golden', + color_dark_orange: 'Dark Orange', + color_teal: 'Teal', + color_dodger_blue: 'Dodger Blue', + color_blue_violet: 'Blue Violet', + color_deep_pink: 'Deep Pink', + color_light_gray: 'Light Gray', + color_dim_gray: 'Dim Gray', + color_saddle_brown: 'Saddle Brown', + color_light_coral: 'Light Coral', // Color block translations - colour_rgb: "red %1 green %2 blue %3", - colour_random: "random color", - colour_blend: "blend %1 and %2 by %3", + colour_rgb: 'red %1 green %2 blue %3', + colour_random: 'random color', + colour_blend: 'blend %1 and %2 by %3', // Custom block translations - Scene blocks - set_sky_color: "sky %1", - create_ground: "ground %1", - set_background_color: "background %1", - create_map: "map %1 with material %2", - show: "show %1", - hide: "hide %1", - dispose: "dispose %1", - clone_mesh: "add %1 clone of %2", + set_sky_color: 'sky %1', + create_ground: 'ground %1', + set_background_color: 'background %1', + create_map: 'map %1 with material %2', + show: 'show %1', + hide: 'hide %1', + dispose: 'dispose %1', + clone_mesh: 'add %1 clone of %2', // Custom block translations - Models blocks load_character: - "add %1 %2 scale: %3 x: %4 y: %5 z: %6\nhair: %7 | skin: %8 | eyes: %9 | t-shirt: %10 | shorts: %11 | detail: %12", - load_object: "add %1 %2 %3 scale: %4 x: %5 y: %6 z: %7", - load_multi_object: "add %1 %2 scale: %3 x: %4 y: %5 z: %6\ncolors: %7", - load_model: "add %1 %2 scale: %3 x: %4 y: %5 z: %6", + 'add %1 %2 scale: %3 x: %4 y: %5 z: %6\nhair: %7 | skin: %8 | eyes: %9 | t-shirt: %10 | shorts: %11 | detail: %12', + load_object: 'add %1 %2 %3 scale: %4 x: %5 y: %6 z: %7', + load_multi_object: 'add %1 %2 scale: %3 x: %4 y: %5 z: %6\ncolors: %7', + load_model: 'add %1 %2 scale: %3 x: %4 y: %5 z: %6', // Custom block translations - Animate blocks - glide_to: "glide %1 to x: %2 y: %3 z: %4 in %5 ms\n%6 return? %7 loop? %8 %9", - glide_to_seconds: - "glide %1 to x: %2 y: %3 z: %4 in %5 seconds \n%6 return? %7 loop? %8 %9", + glide_to: 'glide %1 to x: %2 y: %3 z: %4 in %5 ms\n%6 return? %7 loop? %8 %9', + glide_to_seconds: 'glide %1 to x: %2 y: %3 z: %4 in %5 seconds \n%6 return? %7 loop? %8 %9', glide_to_object: - "glide %1 to %2 in %6 seconds \noffset x: %3 y: %4 z: %5\n%7 return? %8 loop? %9 %10", - glide_to_axis: "glide %1 %2 %3 in %4 seconds\n%5 return? %6 loop? %7 %8", - rotate_to_object: "rotate %1 %2 %3 in %4 seconds\n%5 reverse? %6 loop? %7 %8", - rotate_anim: - "rotate %1 to x: %2 y: %3 z: %4 in %5 ms\n%6 reverse? %7 loop? %8 %9", - rotate_anim_seconds: - "rotate %1 to x: %2 y: %3 z: %4 in %5 seconds\n%6 reverse? %7 loop? %8 %9", - animate_property: "animate %1 %2 to %3 in %4 ms reverse? %5 loop? %6 %7", - colour_keyframe: "at %1 color: %2", - number_keyframe: "at: %1 value: %2", - xyz_keyframe: "at: %1 x: %2 y: %3 z: %4", + 'glide %1 to %2 in %6 seconds \noffset x: %3 y: %4 z: %5\n%7 return? %8 loop? %9 %10', + glide_to_axis: 'glide %1 %2 %3 in %4 seconds\n%5 return? %6 loop? %7 %8', + rotate_to_object: 'rotate %1 %2 %3 in %4 seconds\n%5 reverse? %6 loop? %7 %8', + rotate_anim: 'rotate %1 to x: %2 y: %3 z: %4 in %5 ms\n%6 reverse? %7 loop? %8 %9', + rotate_anim_seconds: 'rotate %1 to x: %2 y: %3 z: %4 in %5 seconds\n%6 reverse? %7 loop? %8 %9', + animate_property: 'animate %1 %2 to %3 in %4 ms reverse? %5 loop? %6 %7', + colour_keyframe: 'at %1 color: %2', + number_keyframe: 'at: %1 value: %2', + xyz_keyframe: 'at: %1 x: %2 y: %3 z: %4', animate_keyframes: - "animate keyframes on %1 property %2\nkeyframes %3\neasing %4 reverse %5 loop %6 %7", + 'animate keyframes on %1 property %2\nkeyframes %3\neasing %4 reverse %5 loop %6 %7', animation: - "animate keyframes on %1 property %2 group %3\nkeyframes %4\neasing %5 reverse %6 loop %7 mode %8", - control_animation_group: "animation group %1 %2", - animate_from: "animate group %1 from %2 seconds", - stop_animations: "stop animations %1", - switch_animation: "switch animation to %1 on %2", - play_animation: "play animation %1 on %2", + 'animate keyframes on %1 property %2 group %3\nkeyframes %4\neasing %5 reverse %6 loop %7 mode %8', + control_animation_group: 'animation group %1 %2', + animate_from: 'animate group %1 from %2 seconds', + stop_animations: 'stop animations %1', + switch_animation: 'switch animation to %1 on %2', + play_animation: 'play animation %1 on %2', // Custom block translations - Base blocks - xyz: "x: %1 y: %2 z: %3", + xyz: 'x: %1 y: %2 z: %3', // Custom block translations - Camera blocks - camera_control: "camera %1 %2", - camera_follow: "camera follow %1 with radius %2 front %3", - get_camera: "get camera as %1", + camera_control: 'camera %1 %2', + camera_follow: 'camera follow %1 with radius %2 front %3', + get_camera: 'get camera as %1', // Custom block translations - Combine blocks - merge_meshes: "add %1 as merge %2", - subtract_meshes: "add %1 as %2 subtract %3", - intersection_meshes: "add %1 as intersect %2", - hull_meshes: "add %1 as hull of %2", + merge_meshes: 'add %1 as merge %2', + subtract_meshes: 'add %1 as %2 subtract %3', + intersection_meshes: 'add %1 as intersect %2', + hull_meshes: 'add %1 as hull of %2', // Custom block translations - Connect blocks - parent: "parent %1 child %2", - parent_child: "parent %1 child %2\noffset x: %3 y: %4 z: %5", - remove_parent: "remove parent from %1", - stop_follow: "stop following %1", - hold: "make %1 hold %2\noffset x: %3 y: %4 z: %5", - drop: "drop %1", - follow: "make %1 follow %2 at %3\noffset x: %4 y: %5 z: %6", - export_mesh: "export %1 as %2", - attach: "attach %1 to %2 at %3\noffset x: %4 y: %5 z: %6", + parent: 'parent %1 child %2', + parent_child: 'parent %1 child %2\noffset x: %3 y: %4 z: %5', + remove_parent: 'remove parent from %1', + stop_follow: 'stop following %1', + hold: 'make %1 hold %2\noffset x: %3 y: %4 z: %5', + drop: 'drop %1', + follow: 'make %1 follow %2 at %3\noffset x: %4 y: %5 z: %6', + export_mesh: 'export %1 as %2', + attach: 'attach %1 to %2 at %3\noffset x: %4 y: %5 z: %6', // Custom block translations - Control blocks - wait: "wait %1 ms", - wait_seconds: "wait %1 seconds", - wait_until: "wait until %1", - local_variable: "local %1", - for_loop2: "for each %1 from %2 to %3 by %4 do %5", - for_loop: "for each %1 from %2 to %3 by %4 do %5", - get_lexical_variable: "%1", + wait: 'wait %1 ms', + wait_seconds: 'wait %1 seconds', + wait_until: 'wait until %1', + local_variable: 'local %1', + for_loop2: 'for each %1 from %2 to %3 by %4 do %5', + for_loop: 'for each %1 from %2 to %3 by %4 do %5', + get_lexical_variable: '%1', // Custom block translations - Effects blocks - main_light: "light intensity: %1 color: %2 ground: %3", - set_fog: "set fog color %1 mode %2 density %3\nstart %4 end %5", - get_light: "get light as %1", + main_light: 'light intensity: %1 color: %2 ground: %3', + set_fog: 'set fog color %1 mode %2 density %3\nstart %4 end %5', + get_light: 'get light as %1', // Custom block translation - Events blocks - start: "start", - forever: "forever\n%1", - when_clicked: "when %1 %2", - on_collision: "on %1 collision %2 %3", - when_key_event: "when key %1 %2", - when_action_event: "when %1 %2", - broadcast_event: "broadcast event %1", - on_event: "on event %1", + start: 'start', + forever: 'forever\n%1', + when_clicked: 'when %1 %2', + on_collision: 'on %1 collision %2 %3', + when_key_event: 'when key %1 %2', + when_action_event: 'when %1 %2', + broadcast_event: 'broadcast event %1', + on_event: 'on event %1', // Custom block translations - Materials blocks - change_color: "color %1 to %2", - change_material: "apply material %1 to %2 with color %3", - text_material: - "material %1 text %2 color %3 background %4\nwidth: %5 height: %6 size: %7", - place_decal: "decal %1 angle %2", + change_color: 'color %1 to %2', + change_material: 'apply material %1 to %2 with color %3', + text_material: 'material %1 text %2 color %3 background %4\nwidth: %5 height: %6 size: %7', + place_decal: 'decal %1 angle %2', decal: - "decal on %1 from x: %2 y: %3 z: %4 \nangle x: %5 y: %6 z: %7\nsize x: %8 y: %9 z: %10 material: %11", - highlight: "highlight %1 %2", - glow: "glow %1", - tint: "tint %1 %2", - set_alpha: "set alpha of %1 to %2", - clear_effects: "clear effects %1", - colour: "%1", - skin_colour: "%1", - greyscale_colour: "%1", - colour_from_string: "- %1 -", - random_colour: "random color", - material: "material %1 %2 alpha %3", - gradient_material: "material %1 alpha %2", - set_material: "set material of %1 to %2", + 'decal on %1 from x: %2 y: %3 z: %4 \nangle x: %5 y: %6 z: %7\nsize x: %8 y: %9 z: %10 material: %11', + highlight: 'highlight %1 %2', + glow: 'glow %1', + tint: 'tint %1 %2', + set_alpha: 'set alpha of %1 to %2', + clear_effects: 'clear effects %1', + colour: '%1', + skin_colour: '%1', + greyscale_colour: '%1', + colour_from_string: '- %1 -', + random_colour: 'random color', + material: 'material %1 %2 alpha %3', + gradient_material: 'material %1 alpha %2', + set_material: 'set material of %1 to %2', // Custom block translations - Physics blocks - add_physics: "add physics %1 type %2", - add_physics_shape: "add physics shape %1 type %2", - apply_force: "apply force to %1 x: %2 y: %3 z: %4", - show_physics: "show physics shapes %1", + add_physics: 'add physics %1 type %2', + add_physics_shape: 'add physics shape %1 type %2', + apply_force: 'apply force to %1 x: %2 y: %3 z: %4', + show_physics: 'show physics shapes %1', // Custom block translations - Sensing blocks - key_pressed: "key pressed is %1", - action_pressed: "%1", - meshes_touching: "%1 touching %2", - time: "time in %1", - seconds: "seconds", - milliseconds: "milliseconds", - minutes: "minutes", - ground_level: "ground level", - distance_to: "distance from %1 to %2", - touching_surface: "is %1 touching surface", - mesh_exists: "%1 exists?", - get_property: "get %1 of %2", - canvas_controls: "canvas controls %1", - interact_indicator: "interact indicator %1", - button_controls: "button controls %1 enabled: %2 color: %3", - on_screen_controls: "on screen controls movement: %1 actions: %2 enabled: %3 color: %4", - microbit_input: "when micro:bit event %1", - ui_slider: - "ui slider %1 from %2 to %3 default %4 at x: %5 y: %6\ncolor: %7 background: %8 %9", + key_pressed: 'key pressed is %1', + action_pressed: '%1', + meshes_touching: '%1 touching %2', + time: 'time in %1', + seconds: 'seconds', + milliseconds: 'milliseconds', + minutes: 'minutes', + ground_level: 'ground level', + distance_to: 'distance from %1 to %2', + touching_surface: 'is %1 touching surface', + mesh_exists: '%1 exists?', + get_property: 'get %1 of %2', + canvas_controls: 'canvas controls %1', + interact_indicator: 'interact indicator %1', + button_controls: 'button controls %1 enabled: %2 color: %3', + on_screen_controls: 'on screen controls movement: %1 actions: %2 enabled: %3 color: %4', + microbit_input: 'when micro:bit event %1', + ui_slider: 'ui slider %1 from %2 to %3 default %4 at x: %5 y: %6\ncolor: %7 background: %8 %9', // Custom block translations - Shapes blocks create_particle_effect: - "add %1 particle effect on: %2\nshape: %3 start: %4 end: %5 alpha: %6 to %7\nrate: %8 size: %9 to %10 lifetime: %11 to %12\ngravity: %13 force x: %14 y: %15 z: %16\nangular speed: %17 to %18 initial angle: %19 to %20", - control_particle_system: "particle system %1 %2", - create_box: - "add box %1 %2 width: %3 height: %4 depth: %5 \nat x: %6 y: %7 z: %8", + 'add %1 particle effect on: %2\nshape: %3 start: %4 end: %5 alpha: %6 to %7\nrate: %8 size: %9 to %10 lifetime: %11 to %12\ngravity: %13 force x: %14 y: %15 z: %16\nangular speed: %17 to %18 initial angle: %19 to %20', + control_particle_system: 'particle system %1 %2', + create_box: 'add box %1 %2 width: %3 height: %4 depth: %5 \nat x: %6 y: %7 z: %8', create_sphere: - "add sphere %1 %2 diameter x: %3 diameter y: %4 diameter z: %5\nat x: %6 y: %7 z: %8", + 'add sphere %1 %2 diameter x: %3 diameter y: %4 diameter z: %5\nat x: %6 y: %7 z: %8', create_cylinder: - "add cylinder %1 %2 height: %3 top: %4 bottom: %5 sides: %6\nat x: %7 y: %8 z: %9", - create_capsule: - "add capsule %1 %2 diameter: %3 height: %4 \nat x: %5 y: %6 z: %7", - create_plane: "add plane %1 %2 width: %3 height: %4 \nat x: %5 y: %6 z: %7", + 'add cylinder %1 %2 height: %3 top: %4 bottom: %5 sides: %6\nat x: %7 y: %8 z: %9', + create_capsule: 'add capsule %1 %2 diameter: %3 height: %4 \nat x: %5 y: %6 z: %7', + create_plane: 'add plane %1 %2 width: %3 height: %4 \nat x: %5 y: %6 z: %7', // Custom block translations - Sound blocks - play_theme: - "play theme %1 %2 from %3 \nspeed: %4 volume: %5 mode: %6 async: %7", - play_sound: - "play sound %1 %2 from %3 \nspeed: %4 volume: %5 mode: %6 async: %7", - stop_all_sounds: "stop all sounds", - note: "MIDI note %1 %2", - rest: "rest", - play_tune: "play tune", - play_tune_notes: "play notes %1 instrument: %2\nnotes: %3", - set_music_speed: "set %1 music speed %2", - midi_note: "MIDI note %1", - play_notes: - "play notes on %1\nnotes: %2 durations: %3\ninstrument: %4 mode: %5", - set_scene_bpm: "set scene BPM to %1", - set_mesh_bpm: "set BPM of %1 to %2", + play_theme: 'play theme %1 %2 from %3 \nspeed: %4 volume: %5 mode: %6 async: %7', + play_sound: 'play sound %1 %2 from %3 \nspeed: %4 volume: %5 mode: %6 async: %7', + stop_all_sounds: 'stop all sounds', + note: 'MIDI note %1 %2', + rest: 'rest', + play_tune: 'play tune', + play_tune_notes: 'play notes %1 instrument: %2\nnotes: %3', + set_music_speed: 'set %1 music speed %2', + midi_note: 'MIDI note %1', + play_notes: 'play notes on %1\nnotes: %2 durations: %3\ninstrument: %4 mode: %5', + set_scene_bpm: 'set scene BPM to %1', + set_mesh_bpm: 'set BPM of %1 to %2', create_instrument: - "instrument %1 wave: %2\nvolume: %3 effect: %4 rate: %5 depth: %6\nattack: %7 decay: %8 sustain: %9 release: %10", - instrument: "instrument %1", - speak: - "speak %1 %2 voice: %3 language: %4\nrate: %5 pitch: %6 volume: %7 mode: %8", + 'instrument %1 wave: %2\nvolume: %3 effect: %4 rate: %5 depth: %6\nattack: %7 decay: %8 sustain: %9 release: %10', + instrument: 'instrument %1', + speak: 'speak %1 %2 voice: %3 language: %4\nrate: %5 pitch: %6 volume: %7 mode: %8', // Custom block translations - Text blocks - comment: "// %1", - print_text: "print %1 for %2 seconds %3", - say: "say %1 for %2 s %3 \ntext: %4 on %5 alpha: %6 size: %7 %8 %9", - ui_text: "ui text %1 %2 at x: %3 y: %4\nsize: %5 for %6 seconds color: %7", - ui_button: - "ui button %1 %2 at x: %3 y: %4\nsize: %5 text size: %6 text: %7 background: %8", - ui_input: - "ui input %1 %2 at x: %3 y: %4\nsize: %5 text size: %6 text: %7 background: %8", - describe: "describe %1 as %2", - create_3d_text: - "add %1 3D text: %2 font: %3 size: %4 color: %5\ndepth: %6 x: %7 y: %8 z: %9 ", + comment: '// %1', + print_text: 'print %1 for %2 seconds %3', + say: 'say %1 for %2 s %3 \ntext: %4 on %5 alpha: %6 size: %7 %8 %9', + ui_text: 'ui text %1 %2 at x: %3 y: %4\nsize: %5 for %6 seconds color: %7', + ui_button: 'ui button %1 %2 at x: %3 y: %4\nsize: %5 text size: %6 text: %7 background: %8', + ui_input: 'ui input %1 %2 at x: %3 y: %4\nsize: %5 text size: %6 text: %7 background: %8', + describe: 'describe %1 as %2', + create_3d_text: 'add %1 3D text: %2 font: %3 size: %4 color: %5\ndepth: %6 x: %7 y: %8 z: %9 ', // Custom block translations - Math blocks - random_seeded_int: "random integer from %1 to %2 seed: %3", - to_number: "convert %1 to %2", + random_seeded_int: 'random integer from %1 to %2 seed: %3', + to_number: 'convert %1 to %2', // Custom block translations - Transform blocks - move_by_xyz: "change position of %1 by x: %2 y: %3 z: %4", - move_by_xyz_single: "change position of %1 by %2 %3", - move_to_xyz: "set position of %1 to x: %2 y: %3 z: %4 y? %5", - move_to_xyz_single: "set position of %1 to %2 %3", - move_to: /* "move %1 to %2 y? %3" */ "set position of %1 to %2 y? %3", - scale: "scale %1 x: %2 y: %3 z: %4\norigin x: %5 y: %6 z: %7", - resize: "resize %1 x: %2 y: %3 z: %4\norigin x: %5 y: %6 z: %7", - rotate_model_xyz: "rotate %1 by x: %2 y: %3 z: %4", - rotate_to: "rotate %1 to x: %2 y: %3 z: %4", - look_at: "look %1 at %2 y? %3", - move_forward: "move %1 %2 speed: %3", - rotate_camera: "rotate camera by %1 degrees", - up: "up %1 force %2", - set_pivot: "set anchor of %1\nx: %2 y: %3 z: %4", - min_centre_max: "%1", + move_by_xyz: 'change position of %1 by x: %2 y: %3 z: %4', + move_by_xyz_single: 'change position of %1 by %2 %3', + move_to_xyz: 'set position of %1 to x: %2 y: %3 z: %4 y? %5', + move_to_xyz_single: 'set position of %1 to %2 %3', + move_to: /* "move %1 to %2 y? %3" */ 'set position of %1 to %2 y? %3', + scale: 'scale %1 x: %2 y: %3 z: %4\norigin x: %5 y: %6 z: %7', + resize: 'resize %1 x: %2 y: %3 z: %4\norigin x: %5 y: %6 z: %7', + rotate_model_xyz: 'rotate %1 by x: %2 y: %3 z: %4', + rotate_to: 'rotate %1 to x: %2 y: %3 z: %4', + look_at: 'look %1 at %2 y? %3', + move_forward: 'move %1 %2 speed: %3', + rotate_camera: 'rotate camera by %1 degrees', + up: 'up %1 force %2', + set_pivot: 'set anchor of %1\nx: %2 y: %3 z: %4', + min_centre_max: '%1', // Custom block translations - XR blocks - device_camera_background: "use %1 camera as background", - set_xr_mode: "set XR mode to %1", - play_rumble_pattern: "play rumble pattern %1", - controller_rumble: "rumble %1 motor at strength %2 for %3 ms", - controller_rumble_pattern: - "rumble %1 motor strength %2 on %3 ms off %4 ms %5 times", + device_camera_background: 'use %1 camera as background', + set_xr_mode: 'set XR mode to %1', + play_rumble_pattern: 'play rumble pattern %1', + controller_rumble: 'rumble %1 motor at strength %2 for %3 ms', + controller_rumble_pattern: 'rumble %1 motor strength %2 on %3 ms off %4 ms %5 times', // Blockly message overrides for English - LISTS_CREATE_WITH_INPUT_WITH: "list", - TEXT_JOIN_TITLE_CREATEWITH: "text", - CONTROLS_REPEAT_INPUT_DO: "", - CONTROLS_WHILEUNTIL_INPUT_DO: "", - CONTROLS_FOR_INPUT_DO: "", - CONTROLS_FOREACH_INPUT_DO: "", - CONTROLS_IF_MSG_THEN: "", - CONTROLS_IF_MSG_ELSE: "else\n", - CONTROLS_FOR_TITLE: "for each %1 from %2 to %3 by %4", + LISTS_CREATE_WITH_INPUT_WITH: 'list', + TEXT_JOIN_TITLE_CREATEWITH: 'text', + CONTROLS_REPEAT_INPUT_DO: '', + CONTROLS_WHILEUNTIL_INPUT_DO: '', + CONTROLS_FOR_INPUT_DO: '', + CONTROLS_FOREACH_INPUT_DO: '', + CONTROLS_IF_MSG_THEN: '', + CONTROLS_IF_MSG_ELSE: 'else\n', + CONTROLS_FOR_TITLE: 'for each %1 from %2 to %3 by %4', // Block message translations - BLOCK_PRINT_TEXT_MESSAGE: "print %1 for %2 seconds %3", - BLOCK_WAIT_SECONDS_MESSAGE: "wait %1 seconds", - BLOCK_KEY_PRESSED_MESSAGE: "key %1 pressed?", - BLOCK_MOVE_FORWARD_MESSAGE: "move %1 forward by %2", - BLOCK_CREATE_BOX_MESSAGE: - "create box %1 color %2 size %3 × %4 × %5 at %6, %7, %8", + BLOCK_PRINT_TEXT_MESSAGE: 'print %1 for %2 seconds %3', + BLOCK_WAIT_SECONDS_MESSAGE: 'wait %1 seconds', + BLOCK_KEY_PRESSED_MESSAGE: 'key %1 pressed?', + BLOCK_MOVE_FORWARD_MESSAGE: 'move %1 forward by %2', + BLOCK_CREATE_BOX_MESSAGE: 'create box %1 color %2 size %3 × %4 × %5 at %6, %7, %8', // Add more custom block translations as needed // Tooltip translations - Scene Blocks - set_sky_color_tooltip: "Set the sky color of the scene.\nKeyword: sky", + set_sky_color_tooltip: 'Set the sky color of the scene.\nKeyword: sky', create_ground_tooltip: - "Add a ground plane with collisions enabled to the scene.\nKeyword: ground", - set_background_color_tooltip: - "Set the scene's background color.\nKeyword: background", - create_map_tooltip: - "Create a map with the selected name and material.\nKeyword: map", - show_tooltip: "Show the selected object.\nKeyword: show", - hide_tooltip: "Hide the selected object.\nKeyword: hide", - dispose_tooltip: - "Remove the specified object from the scene.\nKeyword: dispose", - clone_mesh_tooltip: - "Clone an object and assign it to a variable.\nKeyword: clone", + 'Add a ground plane with collisions enabled to the scene.\nKeyword: ground', + set_background_color_tooltip: "Set the scene's background color.\nKeyword: background", + create_map_tooltip: 'Create a map with the selected name and material.\nKeyword: map', + show_tooltip: 'Show the selected object.\nKeyword: show', + hide_tooltip: 'Hide the selected object.\nKeyword: hide', + dispose_tooltip: 'Remove the specified object from the scene.\nKeyword: dispose', + clone_mesh_tooltip: 'Clone an object and assign it to a variable.\nKeyword: clone', // Tooltip translations - Models blocks - load_character_tooltip: - "Create a configurable character.\nKeyword: character", - load_object_tooltip: "Create an object.\nKeyword: object", - load_multi_object_tooltip: "Create an object with colors.\nKeyword: object", - load_model_tooltip: "Load a model.\nKeyword: model", + load_character_tooltip: 'Create a configurable character.\nKeyword: character', + load_object_tooltip: 'Create an object.\nKeyword: object', + load_multi_object_tooltip: 'Create an object with colors.\nKeyword: object', + load_model_tooltip: 'Load a model.\nKeyword: model', // Tooltip translations - Animate blocks glide_to_tooltip: - "Glide to a specified position over a duration with options for reversing, looping, and easing.", + 'Glide to a specified position over a duration with options for reversing, looping, and easing.', glide_to_seconds_tooltip: - "Glide to a specified position over a duration with options for reversing, looping, and easing.", + 'Glide to a specified position over a duration with options for reversing, looping, and easing.', glide_to_object_tooltip: - "Glide to the position of a specified object over a duration with optional x/y/z offsets (default 0) plus reversing, looping, and easing controls.", + 'Glide to the position of a specified object over a duration with optional x/y/z offsets (default 0) plus reversing, looping, and easing controls.', glide_to_axis_tooltip: "Glide along one axis (x, y, z) to a target world position, or forward/sideways by a distance relative to the object's local direction. The other two axes stay fixed. Same options as the standard glide block.", rotate_to_object_tooltip: "Rotate an object toward another object or match that object's rotation over a duration with options for reverse, looping, and easing.", rotate_anim_tooltip: - "Rotate an object to specified angles over a duration with options for reverse, looping, and easing.", + 'Rotate an object to specified angles over a duration with options for reverse, looping, and easing.', rotate_anim_seconds_tooltip: - "Rotate an object to specified angles over a duration with options for reverse, looping, and easing.", - animate_property_tooltip: - "Animate a material property of the object and its children.", - colour_keyframe_tooltip: "Set a color and duration for a keyframe.", - number_keyframe_tooltip: "Set a number and duration for a keyframe.", - xyz_keyframe_tooltip: "Set an XYZ keyframe with duration.", + 'Rotate an object to specified angles over a duration with options for reverse, looping, and easing.', + animate_property_tooltip: 'Animate a material property of the object and its children.', + colour_keyframe_tooltip: 'Set a color and duration for a keyframe.', + number_keyframe_tooltip: 'Set a number and duration for a keyframe.', + xyz_keyframe_tooltip: 'Set an XYZ keyframe with duration.', animate_keyframes_tooltip: - "Animate an array of keyframes on the selected object, with easing, optional looping, and reversing.", + 'Animate an array of keyframes on the selected object, with easing, optional looping, and reversing.', animation_tooltip: - "Create an animation group for the selected object and property, with keyframes, easing, optional looping, and reversing. Choose create, start, or await to control behaviour.", + 'Create an animation group for the selected object and property, with keyframes, easing, optional looping, and reversing. Choose create, start, or await to control behaviour.', control_animation_group_tooltip: - "Control the animation group by playing, pausing, or stopping it.", - animate_from_tooltip: - "Start animating the group from the specified time (in seconds).", - stop_animations_tooltip: - "Stop all keyframe animations on the selected object.\nKeyword: stop", + 'Control the animation group by playing, pausing, or stopping it.', + animate_from_tooltip: 'Start animating the group from the specified time (in seconds).', + stop_animations_tooltip: 'Stop all keyframe animations on the selected object.\nKeyword: stop', switch_animation_tooltip: - "Changes the animation of the specified object to the given animation.\nKeyword: switch", + 'Changes the animation of the specified object to the given animation.\nKeyword: switch', play_animation_tooltip: - "Play the selected animation once on the specified object.\nKeyword: play", + 'Play the selected animation once on the specified object.\nKeyword: play', // Tooltip translations - Base blocks - xyz_tooltip: "Creates a vector with X, Y, Z coordinates", + xyz_tooltip: 'Creates a vector with X, Y, Z coordinates', // Tooltip translations - Camera blocks - camera_control_tooltip: "Bind a specific key to a camera control action.", + camera_control_tooltip: 'Bind a specific key to a camera control action.', camera_follow_tooltip: - "Make the camera follow an object with a customizable distance (radius) from the target.\nKeyword: follow", - get_camera_tooltip: "Get the current scene camera", + 'Make the camera follow an object with a customizable distance (radius) from the target.\nKeyword: follow', + get_camera_tooltip: 'Get the current scene camera', // Tooltip translations - Combine blocks - merge_meshes_tooltip: - "Merge a list of objects into one and store the result.\nKeyword: merge", + merge_meshes_tooltip: 'Merge a list of objects into one and store the result.\nKeyword: merge', subtract_meshes_tooltip: - "Subtract a list of objects from a base object and store the result.\nKeyword: subtract", + 'Subtract a list of objects from a base object and store the result.\nKeyword: subtract', intersection_meshes_tooltip: - "Intersect a list of objects and store the resulting geometry.\nKeyword: intersect", + 'Intersect a list of objects and store the resulting geometry.\nKeyword: intersect', hull_meshes_tooltip: - "Create a convex hull from a list of objects and store the result.\nKeyword: hull", + 'Create a convex hull from a list of objects and store the result.\nKeyword: hull', // Tooltip translations - Connect blocks parent_tooltip: - "Set a parent-child relationship between two objects and keeps the child in its world position\nKeyword:parent", + 'Set a parent-child relationship between two objects and keeps the child in its world position\nKeyword:parent', parent_child_tooltip: - "Set a parent-child relationship between two objects with a specified offset in x, y, and z directions.\nKeyword: child", + 'Set a parent-child relationship between two objects with a specified offset in x, y, and z directions.\nKeyword: child', remove_parent_tooltip: - "Remove the parent relationship from the specified object.\nKeyword: unparent", - stop_follow_tooltip: - "Stop the specified object from following another.\nKeyword: stopfollow", + 'Remove the parent relationship from the specified object.\nKeyword: unparent', + stop_follow_tooltip: 'Stop the specified object from following another.\nKeyword: stopfollow', hold_tooltip: - "Attach an object to the specified bone of another object with a specified offset in x, y, and z directions.\nKeyword: hold", - drop_tooltip: - "Detach an object from its currently attached bone.\nKeyword: drop", + 'Attach an object to the specified bone of another object with a specified offset in x, y, and z directions.\nKeyword: hold', + drop_tooltip: 'Detach an object from its currently attached bone.\nKeyword: drop', follow_tooltip: - "Make one object follow another at a specified position (top, center, or bottom) with offset in x, y, and z directions. \nKeyword: follow", - export_mesh_tooltip: "Export an object as STL, OBJ, or GLB.\nKeyword: export", + 'Make one object follow another at a specified position (top, center, or bottom) with offset in x, y, and z directions. \nKeyword: follow', + export_mesh_tooltip: 'Export an object as STL, OBJ, or GLB.\nKeyword: export', // Tooltip translations - Control blocks - wait_tooltip: "Wait for a specified time in milliseconds.\nKeyword: milli", - wait_seconds_tooltip: "Wait for a specified time in seconds.\nKeyword: wait", - wait_until_tooltip: "Wait until the condition is true.\nKeyword:until", + wait_tooltip: 'Wait for a specified time in milliseconds.\nKeyword: milli', + wait_seconds_tooltip: 'Wait for a specified time in seconds.\nKeyword: wait', + wait_until_tooltip: 'Wait until the condition is true.\nKeyword:until', local_variable_tooltip: - "Create a local version of a selected variable. This hides the global variable and can have a different value. \nKeyword: local", - for_loop2_tooltip: - "Loop from a starting number to an ending number by a given step.", + 'Create a local version of a selected variable. This hides the global variable and can have a different value. \nKeyword: local', + for_loop2_tooltip: 'Loop from a starting number to an ending number by a given step.', for_loop_tooltip: - "Loop from a starting number to an ending number by a given step. Click on the dropdown to get the loop variable to use in your code.\nKeyword: for", - get_lexical_variable_tooltip: "Get the value of a lexical variable", + 'Loop from a starting number to an ending number by a given step. Click on the dropdown to get the loop variable to use in your code.\nKeyword: for', + get_lexical_variable_tooltip: 'Get the value of a lexical variable', // Tooltip translations - Effects blocks main_light_tooltip: - "Set the intensity and diffuse and ground colors of the main light.\nKeyword: light intensity", + 'Set the intensity and diffuse and ground colors of the main light.\nKeyword: light intensity', set_fog_tooltip: "Configure the scene's fog. Use start and end to set the near and far distances.\nKeyword: fog", get_light_tooltip: "Get the current scene's main light.\nKeyword: light", // Tooltip translations - Events blocks start_tooltip: - "Run the blocks inside whenthe project starts. You can have multiple start blocks. \nKeyword: start", + 'Run the blocks inside whenthe project starts. You can have multiple start blocks. \nKeyword: start', forever_tooltip: - "Run the blocks inside every frame or when the previous iteration finishes. \nKeyword: forever", - when_clicked_tooltip: - "Run the blocks inside when the object trigger occurs.\nKeyword: click", + 'Run the blocks inside every frame or when the previous iteration finishes. \nKeyword: forever', + when_clicked_tooltip: 'Run the blocks inside when the object trigger occurs.\nKeyword: click', on_collision_tooltip: - "Execute the blocks inside when the object intersects or no longer intersects with another object.\nKeyword: collide", + 'Execute the blocks inside when the object intersects or no longer intersects with another object.\nKeyword: collide', when_key_event_tooltip: - "Execute the blocks inside when the specified key is pressed or released.", + 'Execute the blocks inside when the specified key is pressed or released.', when_action_event_tooltip: - "Execute the blocks inside when the selected action is pressed or released across keyboard, touch, or XR.", - broadcast_event_tooltip: - "Broadcast an event that is received by on event.\nKeyword: broadcast", - on_event_tooltip: "Run code when a broadcast event is received.\nKeyword: on", + 'Execute the blocks inside when the selected action is pressed or released across keyboard, touch, or XR.', + broadcast_event_tooltip: 'Broadcast an event that is received by on event.\nKeyword: broadcast', + on_event_tooltip: 'Run code when a broadcast event is received.\nKeyword: on', // Tooltip translations - Materials blocks - change_color_tooltip: - "Change the color of the selected object.\nKeyword: color", + change_color_tooltip: 'Change the color of the selected object.\nKeyword: color', change_material_tooltip: - "Apply a selected material with a color tint to the specified object.\nKeyword: material", + 'Apply a selected material with a color tint to the specified object.\nKeyword: material', text_material_tooltip: - "Create a material with text or emoji, specifying width, height, background color, and text size.", - place_decal_tooltip: - "Place a decal on an object using the selected material.", - decal_tooltip: - "Create a decal on an object with position, normal, size, and material.", - highlight_tooltip: "Highlight the selected object.\nKeyword: highlight", - glow_tooltip: "Adds a glow effect to the selected object.\nKeyword: glow", - tint_tooltip: "Add color tint effect.\nKeyword: tint", + 'Create a material with text or emoji, specifying width, height, background color, and text size.', + place_decal_tooltip: 'Place a decal on an object using the selected material.', + decal_tooltip: 'Create a decal on an object with position, normal, size, and material.', + highlight_tooltip: 'Highlight the selected object.\nKeyword: highlight', + glow_tooltip: 'Adds a glow effect to the selected object.\nKeyword: glow', + tint_tooltip: 'Add color tint effect.\nKeyword: tint', set_alpha_tooltip: - "Set the alpha (transparency) of the material(s) on a specified object. Values should be 0 to 1.\nKeyword:alpha", - clear_effects_tooltip: - "Clear visual effects from selected object.\nKeyword: clear", - colour_tooltip: "Pick a color.\nKeyword: color", - skin_colour_tooltip: "Pick a skin color.\nKeyword: skin", - greyscale_colour_tooltip: - "Pick a greyscale color for elevation.\nKeyword: grey", - random_colour_tooltip: "Generate a random color.\nKeyword: randcol", - material_tooltip: "Define material properties", - gradient_material_tooltip: "Define material properties", - set_material_tooltip: "Set the specified material on the given object.", + 'Set the alpha (transparency) of the material(s) on a specified object. Values should be 0 to 1.\nKeyword:alpha', + clear_effects_tooltip: 'Clear visual effects from selected object.\nKeyword: clear', + colour_tooltip: 'Pick a color.\nKeyword: color', + skin_colour_tooltip: 'Pick a skin color.\nKeyword: skin', + greyscale_colour_tooltip: 'Pick a greyscale color for elevation.\nKeyword: grey', + random_colour_tooltip: 'Generate a random color.\nKeyword: randcol', + material_tooltip: 'Define material properties', + gradient_material_tooltip: 'Define material properties', + set_material_tooltip: 'Set the specified material on the given object.', // Tooltip translations - Physics blocks add_physics_tooltip: - "Add physics to the object. Options are dynamic, static, animated and none.\nKeyword:physics", + 'Add physics to the object. Options are dynamic, static, animated and none.\nKeyword:physics', add_physics_shape_tooltip: - "Add a physics shape to the object. Options are object or capsule.\nKeyword:physics", - apply_force_tooltip: - "Apply a force to an object in XYZ directions.\nKeyword: force", + 'Add a physics shape to the object. Options are object or capsule.\nKeyword:physics', + apply_force_tooltip: 'Apply a force to an object in XYZ directions.\nKeyword: force', show_physics_tooltip: - "Show or hide physics colliders for debugging. Check to show, uncheck to hide.\nKeyword: debug physics collider", + 'Show or hide physics colliders for debugging. Check to show, uncheck to hide.\nKeyword: debug physics collider', // Tooltip translations - Sensing blocks - key_pressed_tooltip: - "Return true if the specified key is pressed.\nKeyword:ispressed", + key_pressed_tooltip: 'Return true if the specified key is pressed.\nKeyword:ispressed', action_pressed_tooltip: - "Return true if the specified movement or action control is active across keyboard, touch, or XR inputs.", - set_action_key: "set %1 key to %2", + 'Return true if the specified movement or action control is active across keyboard, touch, or XR inputs.', + set_action_key: 'set %1 key to %2', set_action_key_tooltip: - "Set the key that triggers a specific action (forward, backward, left, right, or buttons).", + 'Set the key that triggers a specific action (forward, backward, left, right, or buttons).', meshes_touching_tooltip: - "Return true if the two selected objects are touching.\nKeyword: istouching", - time_tooltip: "Return the current time in seconds.", - ground_level_tooltip: "Return the ground height at the current x/z position.", - distance_to_tooltip: "Calculate the distance between two objects.", - touching_surface_tooltip: - "Check if the object is touching a surface.\nKeyword: surface", - mesh_exists_tooltip: - "Return true if the object with this name is present in the scene.", - get_property_tooltip: - "Get the value of the selected property of an object.\nKeyword: get", - canvas_controls_tooltip: - "Add or remove canvas motion controls.\nKeyword: canvas", + 'Return true if the two selected objects are touching.\nKeyword: istouching', + time_tooltip: 'Return the current time in seconds.', + ground_level_tooltip: 'Return the ground height at the current x/z position.', + distance_to_tooltip: 'Calculate the distance between two objects.', + touching_surface_tooltip: 'Check if the object is touching a surface.\nKeyword: surface', + mesh_exists_tooltip: 'Return true if the object with this name is present in the scene.', + get_property_tooltip: 'Get the value of the selected property of an object.\nKeyword: get', + canvas_controls_tooltip: 'Add or remove canvas motion controls.\nKeyword: canvas', interact_indicator_tooltip: - "Show or hide the interact indicator next to nearby objects.\nKeyword: indicator", - button_controls_tooltip: "Configure button controls.\nKeyword: button", - on_screen_controls_tooltip: "Configure on-screen controls.\nKeyword: onscreen", + 'Show or hide the interact indicator next to nearby objects.\nKeyword: indicator', + button_controls_tooltip: 'Configure button controls.\nKeyword: button', + on_screen_controls_tooltip: 'Configure on-screen controls.\nKeyword: onscreen', microbit_input_tooltip: - "Executes the blocks inside when a specified micro:bit event is triggered.", - ui_slider_tooltip: - "Add a 2D slider UI control and store its reference in a variable.", + 'Executes the blocks inside when a specified micro:bit event is triggered.', + ui_slider_tooltip: 'Add a 2D slider UI control and store its reference in a variable.', // Tooltip translations - Shapes blocks create_particle_effect_tooltip: - "Create a particle effect attached to an object with configurable shape, gravity, size, color, transparency, lifetime, force, and rotation.", + 'Create a particle effect attached to an object with configurable shape, gravity, size, color, transparency, lifetime, force, and rotation.', control_particle_system_tooltip: - "Control the particle system by starting, stopping, or resetting it.", - create_box_tooltip: - "Create a colored box with specified dimensions and position.\nKeyword: box", + 'Control the particle system by starting, stopping, or resetting it.', + create_box_tooltip: 'Create a colored box with specified dimensions and position.\nKeyword: box', create_sphere_tooltip: - "Create a colored sphere with specified dimensions and position.\nKeyword: sphere", + 'Create a colored sphere with specified dimensions and position.\nKeyword: sphere', create_cylinder_tooltip: - "Create a colored cylinder with specified dimensions and position.\nKeyword: cylinder", + 'Create a colored cylinder with specified dimensions and position.\nKeyword: cylinder', create_capsule_tooltip: - "Create a colored capsule with specified dimensions and position.\nKeyword: capsule", + 'Create a colored capsule with specified dimensions and position.\nKeyword: capsule', create_plane_tooltip: - "Create a colored 2D plane with specified width, height, and position.\nKeyword: plane", + 'Create a colored 2D plane with specified width, height, and position.\nKeyword: plane', // Tooltip translations - Sound blocks play_theme_tooltip: - "Play a theme tune on an object with adjustable speed, volume, and mode.\nKeyword: theme", + 'Play a theme tune on an object with adjustable speed, volume, and mode.\nKeyword: theme', play_sound_tooltip: - "Play the selected sound on an object with adjustable speed, volume, and mode.\nKeyword: sound", - stop_all_sounds_tooltip: - "Stop all sounds currently playing in the scene.\nKeyword:nosound", - note_tooltip: "A musical note with a pitch (MIDI 0–127) and duration in beats.\nKeyword: note", - rest_tooltip: "A musical rest (silence). Use as the pitch in a note block.\nKeyword: rest", - play_tune_tooltip: "Import an ABC notation string to generate a tune with notes and repeats.\nKeyword: playtune", + 'Play the selected sound on an object with adjustable speed, volume, and mode.\nKeyword: sound', + stop_all_sounds_tooltip: 'Stop all sounds currently playing in the scene.\nKeyword:nosound', + note_tooltip: 'A musical note with a pitch (MIDI 0–127) and duration in beats.\nKeyword: note', + rest_tooltip: 'A musical rest (silence). Use as the pitch in a note block.\nKeyword: rest', + play_tune_tooltip: + 'Import an ABC notation string to generate a tune with notes and repeats.\nKeyword: playtune', play_tune_notes_tooltip: - "Play a sequence of notes and rests on an object using the specified instrument.\nKeyword: playtunenotes", + 'Play a sequence of notes and rests on an object using the specified instrument.\nKeyword: playtunenotes', set_music_speed_tooltip: - "Set the music playback speed multiplier for an object.\nKeyword: musicspeed", - midi_note_tooltip: "A MIDI note value between 0 and 127.", + 'Set the music playback speed multiplier for an object.\nKeyword: musicspeed', + midi_note_tooltip: 'A MIDI note value between 0 and 127.', play_notes_tooltip: - "Play a sequence of MIDI notes and rests with corresponding durations, using object for panning. Can return immediately or after the notes have finished playing.", - set_scene_bpm_tooltip: "Set the BPM for the entire scene", - set_mesh_bpm_tooltip: "Set the BPM for a selected object", - create_instrument_tooltip: - "Create an instrument and assigns it to the selected variable.", - instrument_tooltip: "Select an instrument to use for playing notes.", + 'Play a sequence of MIDI notes and rests with corresponding durations, using object for panning. Can return immediately or after the notes have finished playing.', + set_scene_bpm_tooltip: 'Set the BPM for the entire scene', + set_mesh_bpm_tooltip: 'Set the BPM for a selected object', + create_instrument_tooltip: 'Create an instrument and assigns it to the selected variable.', + instrument_tooltip: 'Select an instrument to use for playing notes.', speak_tooltip: - "Convert text to speech using the Web Speech API with optional 3D positioning.\nKeyword: speak", + 'Convert text to speech using the Web Speech API with optional 3D positioning.\nKeyword: speak', // Tooltip translations - Text blocks - comment_tooltip: "A comment line to help people understand your code.", - print_text_tooltip: "A text to the output panel.\nKeyword: print", - say_tooltip: - "Display a piece of text as a speech bubble on an object.\nKeyword: say", + comment_tooltip: 'A comment line to help people understand your code.', + print_text_tooltip: 'A text to the output panel.\nKeyword: print', + say_tooltip: 'Display a piece of text as a speech bubble on an object.\nKeyword: say', describe_tooltip: - "Set the display name for an object. This updates the object metadata for accessibility.\nKeyword: describe", + 'Set the display name for an object. This updates the object metadata for accessibility.\nKeyword: describe', ui_text_tooltip: - "Add text to the UI screen, and store control in a variable for later use or disposal.", + 'Add text to the UI screen, and store control in a variable for later use or disposal.', ui_button_tooltip: - "Add a 2D button to the UI screen with a preset size, and store control in a variable for later use or disposal.", - ui_input_tooltip: - "Ask the user a question and wait for input. Stores the result in a variable.", - create_3d_text_tooltip: "Create 3D text in the scene.", + 'Add a 2D button to the UI screen with a preset size, and store control in a variable for later use or disposal.', + ui_input_tooltip: 'Ask the user a question and wait for input. Stores the result in a variable.', + create_3d_text_tooltip: 'Create 3D text in the scene.', // Tooltip translations - Math blocks - random_seeded_int_tooltip: - "Generate a random integer with a seed.\nKeyword: seed", - to_number_tooltip: "Convert a string to an integer or float.", + random_seeded_int_tooltip: 'Generate a random integer with a seed.\nKeyword: seed', + to_number_tooltip: 'Convert a string to an integer or float.', // Tooltip translations - Transform blocks - move_by_xyz_tooltip: - "Move an object a given amount in x y and z directions.\nKeyword: move", + move_by_xyz_tooltip: 'Move an object a given amount in x y and z directions.\nKeyword: move', move_by_xyz_single_tooltip: - "Move an object a given amount in either x y or z direction.\nKeyword: move", + 'Move an object a given amount in either x y or z direction.\nKeyword: move', move_to_xyz_tooltip: - "Teleport the object to the coordinates. Optionally, use the Y axis.\nKeyword: moveby", + 'Teleport the object to the coordinates. Optionally, use the Y axis.\nKeyword: moveby', move_to_xyz_single_tooltip: - "Teleport the object to the specified single coordinate.\nKeyword: moveby", + 'Teleport the object to the specified single coordinate.\nKeyword: moveby', move_to_tooltip: - "Teleport the first object to the location of the second object.\nKeyword: moveto", + 'Teleport the first object to the location of the second object.\nKeyword: moveto', scale_tooltip: - "Resize an object to the given x, y, and z and controls the origin of scaling. \nKeyword: scale", + 'Resize an object to the given x, y, and z and controls the origin of scaling. \nKeyword: scale', resize_tooltip: - "Resize an object to the given x, y, and z and controls the origin of scaling.\nKeyword: resize", + 'Resize an object to the given x, y, and z and controls the origin of scaling.\nKeyword: resize', rotate_model_xyz_tooltip: - "Rotate the object by the given x, y, z values.\nKeyword: rotate\nKeyword: rotateby", - rotate_to_tooltip: - "Rotate the object to point towards the coordinates.\nKeyword: rotateto", + 'Rotate the object by the given x, y, z values.\nKeyword: rotate\nKeyword: rotateby', + rotate_to_tooltip: 'Rotate the object to point towards the coordinates.\nKeyword: rotateto', look_at_tooltip: - "Rotate the first object towards the position of the second object.\nKeyword: look", + 'Rotate the first object towards the position of the second object.\nKeyword: look', move_forward_tooltip: "Move the object in the specified direction. 'Forward' moves it in the direction it's pointing, 'sideways' moves it relative to the camera's direction, and 'strafe' moves it sideways relative to the camera's direction.\nKeyword: push", - rotate_camera_tooltip: - "Rotate the camera left or right by the given degrees.\nKeyword: rotate", - up_tooltip: "Apply the specified upwards force.\nKeyword: up", - set_pivot_tooltip: - "Set the anchor point for an object on the X, Y, and Z axes\nKeyword: anchor", - min_centre_max_tooltip: - "Choose min, center, or max for the pivot point\nKeyword: minmax", + rotate_camera_tooltip: 'Rotate the camera left or right by the given degrees.\nKeyword: rotate', + up_tooltip: 'Apply the specified upwards force.\nKeyword: up', + set_pivot_tooltip: 'Set the anchor point for an object on the X, Y, and Z axes\nKeyword: anchor', + min_centre_max_tooltip: 'Choose min, center, or max for the pivot point\nKeyword: minmax', // Tooltip translations - XR blocks device_camera_background_tooltip: - "Use the device camera as the background for the scene. Works on both mobile and desktop.", - set_xr_mode_tooltip: - "Set the XR mode for the scene.\nOptions: VR, AR, Magic Window.", + 'Use the device camera as the background for the scene. Works on both mobile and desktop.', + set_xr_mode_tooltip: 'Set the XR mode for the scene.\nOptions: VR, AR, Magic Window.', play_rumble_pattern_tooltip: - "Play a preset haptic rumble pattern on all connected controllers.\nKeyword: rumble preset", + 'Play a preset haptic rumble pattern on all connected controllers.\nKeyword: rumble preset', controller_rumble_tooltip: - "Make a connected game controller rumble. Choose all, left, or right motor, set the strength (0 to 1), and how long to rumble in milliseconds.\nKeyword: rumble", + 'Make a connected game controller rumble. Choose all, left, or right motor, set the strength (0 to 1), and how long to rumble in milliseconds.\nKeyword: rumble', controller_rumble_pattern_tooltip: - "Make a connected game controller rumble in a repeating pattern. Set the motor, strength (0 to 1), on time, off time, and number of repeats.\nKeyword: rumble pattern", + 'Make a connected game controller rumble in a repeating pattern. Set the motor, strength (0 to 1), on time, off time, and number of repeats.\nKeyword: rumble pattern', // Dropdown option translations - AWAIT_option: "await", - START_option: "start", - CREATE_option: "create", - - Linear_option: "Linear", - SineEase_option: "Sine Ease", - CubicEase_option: "Cubic Ease", - QuadraticEase_option: "Quadratic Ease", - ExponentialEase_option: "Exponential Ease", - BounceEase_option: "Bounce Ease", - ElasticEase_option: "Elastic Ease", - BackEase_option: "Back Ease", - TOWARDS_option: "towards", - SAME_ROTATION_option: "same rotation as", - - EASEIN_option: "ease-in", - EASEOUT_option: "ease-out", - EASEINOUT_option: "ease-in-out", - - play_option: "▶️ Play", - pause_option: "⏸️ Pause", - stop_option: "⏹️ Stop", - start_option: "▶️ Start", - reset_option: "🔄 Reset", - - diffuseColor_option: "diffuse color", - emissiveColor_option: "emissive color", - ambientColor_option: "ambient color", - specularColor_option: "specular color", - alpha_option: "alpha", - color_option: "color", - position_option: "position", - rotation_option: "rotation", - scaling_option: "scaling", - position_x_option: "position.x", - position_y_option: "position.y", - position_z_option: "position.z", - rotation_x_option: "rotation.x", - rotation_y_option: "rotation.y", - rotation_z_option: "rotation.z", - scaling_x_option: "scaling.x", - scaling_y_option: "scaling.y", - scaling_z_option: "scaling.z", - - rotateLeft_option: "Rotate Left", - rotateRight_option: "Rotate Right", - rotateUp_option: "Look Up", - rotateDown_option: "Look Down", - moveUp_option: "Move Up", - moveDown_option: "Move Down", - moveLeft_option: "Move Left", - moveRight_option: "Move Right", - - _65_option: "A ◁", - _68_option: "D", - _87_option: "W", - _83_option: "S", - _81_option: "Q", - _69_option: "E", - _70_option: "F", - _32_option: "Space", - _38_option: "Up Arrow", - _40_option: "Down Arrow", - _37_option: "Left Arrow", - _39_option: "Right Arrow", - - TOP_option: "top", - CENTER_option: "center", - BOTTOM_option: "bottom", - CENTRE_option: "center", - LEFT_option: "left", - RIGHT_option: "right", - BASE_option: "base", - FRONT_option: "front", - BACK_option: "back", - forward_option: "forward", - sideways_option: "sideways", - strafe_option: "strafe", - MIN_option: "min", - MAX_option: "max", - user_option: "front", - environment_option: "back", - - LINEAR_option: "Linear", - NONE_option: "None", - EXP_option: "Exp", - EXP2_option: "Exp2", - - OnPickTrigger_option: "clicked", - OnLeftPickTrigger_option: "interact", - OnDoublePickTrigger_option: "double interact", - OnPickDownTrigger_option: "interact start", - OnPickUpTrigger_option: "interact end", - - OnIntersectionEnterTrigger_option: "enter", - OnIntersectionExitTrigger_option: "exit", - - _0_option: "0", - _1_option: "1", - _2_option: "2", - _3_option: "3", - _4_option: "4", - _5_option: "5", - _6_option: "6", - _7_option: "7", - _8_option: "8", - _9_option: "9", - a_option: "A", - b_option: "B", - c_option: "C", - d_option: "D", - e_option: "E", - f_option: "F", - g_option: "G", - h_option: "H", - i_option: "I", - j_option: "J", - k_option: "K", - l_option: "L", - m_option: "M", - n_option: "N", - o_option: "O", - p_option: "P", - q_option: "Q", - r_option: "R", - s_option: "S", - t_option: "T", - u_option: "U", - v_option: "V", - w_option: "W", - x_option: "X", - y_option: "Y", - z_option: "Z", - space_option: " ", - comma_option: ",", - dot_option: ".", - slash_option: "/", - ArrowLeft_option: "◁", - ArrowUp_option: "△", - ArrowRight_option: "▷", - ArrowDown_option: "▽", - - pressed_option: "pressed", - released_option: "released", - starts_option: "starts", - ends_option: "ends", - - DYNAMIC_option: "dynamic", - ANIMATED_option: "animated", - STATIC_option: "static", - - MESH_option: "object", - CAPSULE_option: "capsule", - - FLAT_option: "Flat", // Duplicate key NONE - - ANY_option: "any", - all_option: "all", - objectGrab_option: "grab", - objectDrop_option: "drop", - smallCollision_option: "small bump", - heavyCollision_option: "heavy crash", - snapToGrid_option: "snap", - errorInvalid_option: "error", - successConfirmation_option: "success", - slidingGravel_option: "gravel slide", - slidingMetal_option: "metal slide", - machineRunning_option: "machine", - explosion_option: "explosion", - teleport_option: "teleport", - space_infinity_option: "space ❖", // Duplicate key space - q_icon_option: "Q ■", // Duplicate key q - e_icon_option: "E ✿", // Duplicate key e - f_icon_option: "F ✱", // Duplicate key f - - x_coordinate_option: "x", - y_coordinate_option: "y", - z_coordinate_option: "z", - - POSITION_X_option: "position x", - POSITION_Y_option: "position y", - POSITION_Z_option: "position z", - ROTATION_X_option: "rotation x", - ROTATION_Y_option: "rotation y", - ROTATION_Z_option: "rotation z", - MIN_X_option: "min x", - MAX_X_option: "max x", - MIN_Y_option: "min y", - MAX_Y_option: "max y", - MIN_Z_option: "min z", - MAX_Z_option: "max z", - SCALE_X_option: "scale x", - SCALE_Y_option: "scale y", - SCALE_Z_option: "scale z", - SIZE_X_option: "size x", - SIZE_Y_option: "size y", - SIZE_Z_option: "size z", - VISIBLE_option: "visible", - ALPHA_option: "alpha", - COLOUR_option: "color", - DESCRIPTION_option: "description", - AUTO_option: "auto", - ENABLED_option: "enabled", - DISABLED_option: "disabled", - - BOTH_option: "both", - ARROWS_option: "arrows", - ACTIONS_option: "actions", - JOYSTICK_option: "joystick", - YES_option: "yes", - NO_option: "no", - ACTION_FORWARD_option: "forward", - ACTION_BACKWARD_option: "backward", - ACTION_LEFT_option: "left", - ACTION_RIGHT_option: "right", - ACTION_BUTTON1_option: "button 1", - ACTION_BUTTON2_option: "button 2", - ACTION_BUTTON3_option: "button 3", - ACTION_BUTTON4_option: "button 4", - - pin_0_option: "Pin P0 released", // Duplicate key 0 - pin_1_option: "Pin P1 released", // Duplicate key 1 - pin_2_option: "Pin P2 released", // Duplicate key 2 - pin_l_option: "Logo long pressed", // All have duplicate keys - pin_j_option: "Logo touched", - pin_h_option: "Logo pressed", - pin_k_option: "Logo released", - pin_space_option: "Button A pressed", - pin_q_option: "Button B pressed", - pin_r_option: "Button A+B pressed", - pin_t_option: "Gesture: FreeFall", - pin_o_option: "Gesture: LogoUp", - pin_p_option: "Gesture: LogoDown", - pin_a_option: "Gesture: TiltLeft", - pin_d_option: "Gesture: TiltRight", - pin_y_option: "Gesture: ScreenUp", - pin_g_option: "Gesture: ScreenDown", - pin_i_option: "Gesture: Shake", - - SMALL_option: "small", - MEDIUM_option: "medium", - LARGE_option: "large", - - ONCE_option: "once", - LOOP_option: "loop", - everywhere_option: "everywhere", - - theme_bright_option: "Bright", - theme_calm_option: "Calm", - theme_electronic_option: "Electronic", - theme_game_option: "Game", - theme_medieval_option: "Medieval", - theme_metal_option: "Metal", - - sound_highDown_option: "High Down", - sound_highUp_option: "High Up", - sound_laser1_option: "Laser 1", - sound_laser2_option: "Laser 2", - sound_laser3_option: "Laser 3", - sound_lowDown_option: "Low Down", - sound_lowRandom_option: "Low Random", - sound_lowThreeTone_option: "Low Three Tone", - sound_phaseJump1_option: "Phase Jump 1", - sound_powerUp1_option: "Power Up 1", - sound_powerUp2_option: "Power Up 2", - sound_powerUp3_option: "Power Up 3", - sound_powerUp4_option: "Power Up 4", - sound_powerUp5_option: "Power Up 5", - sound_spaceTrash_option: "Space Trash", - sound_threeTone1_option: "Three Tone 1", - sound_threeTone2_option: "Three Tone 2", - sound_chop_option: "Chop", - sound_creak_option: "Creak", - sound_footstep_option: "Footstep", - sound_door_open_option: "Door Open", - sound_door_close_option: "Door Close", - sound_metal_latch_option: "Metal Latch", - - sine_option: "sine", - square_option: "square", - sawtooth_option: "sawtooth", - triangle_option: "triangle", - - none_option: "none", - tremolo_option: "tremolo", - vibrato_option: "vibrato", - warble_option: "warble", - robot_option: "robot", - - default_option: "Default Instrument (Sine)", - piano_option: "Piano (Square)", - guitar_option: "Guitar (Sawtooth)", - violin_option: "Violin (Triangle)", - - female_option: "female", - male_option: "male", - - en_GB_option: "English (UK)", - en_US_option: "English (US)", - - ADD_option: "add", - REPLACE_option: "replace", - - _14px_option: "small", - _18px_option: "medium", - _24px_option: "large", - - __fonts_FreeSans_Bold_json_option: "Free Sans", - - VR_option: "VR (Oculus Quest or phone viewer)", - AR_option: "AR (Augmented Reality)", - MAGIC_WINDOW_option: "Magic Window (look-around)", - - circular_depression_png_option: "Circular Dip", - checkerboard_png_option: "Checkerboard", - sloped_plane_png_option: "Sloped Plane", - cove_plateau_png_option: "Cove Plateau", - random_hills_png_option: "Random Hills", - diagonal_ridge_png_option: "Diagonal Ridge", - mixed_heights_png_option: "Mixed Heights", - uneven_terrain_png_option: "Uneven Terrain", - mountains_png_option: "Mountains", - Islands_png_option: "Islands", - Lookout_png_option: "Lookout", - Valley_png_option: "Valley", - - Idle_option: "Idle", - Walk_option: "Walk", - Run_option: "Run", - Wave_option: "Wave", - Yes_option: "Yes", - No_option: "No", - Duck_option: "Duck", - Fall_option: "Fall", - Fly_option: "Fly", - Jump_option: "Jump", - Flip_option: "Flip", - Dance1_option: "Dance1", - Dance2_option: "Dance2", - Dance3_option: "Dance3", - Dance4_option: "Dance4", - JumpUp_option: "Jump Up", - JumpIdle_option: "Jump Idle", - JumpLand_option: "Jump Land", - Punch_option: "Punch", - HitReact_option: "Hit React", - Idle_Hold_option: "Idle Hold", - Walk_Hold_option: "Walk Hold", - Run_Hold_option: "Run Hold", - Idle_Attack_option: "Idle Reach", - Walk_Attack_option: "Walk Reach", - Run_Attack_option: "Run Reach", - Sit_Down_option: "Sit Down", - Sitting_option: "Sitting", - Stand_Up_option: "Stand Up", - Wobble_option: "Wobble", - Clap_option: "Clap", - Climb_rope_option: "Climb rope", + AWAIT_option: 'await', + START_option: 'start', + CREATE_option: 'create', + + Linear_option: 'Linear', + SineEase_option: 'Sine Ease', + CubicEase_option: 'Cubic Ease', + QuadraticEase_option: 'Quadratic Ease', + ExponentialEase_option: 'Exponential Ease', + BounceEase_option: 'Bounce Ease', + ElasticEase_option: 'Elastic Ease', + BackEase_option: 'Back Ease', + TOWARDS_option: 'towards', + SAME_ROTATION_option: 'same rotation as', + + EASEIN_option: 'ease-in', + EASEOUT_option: 'ease-out', + EASEINOUT_option: 'ease-in-out', + + play_option: '▶️ Play', + pause_option: '⏸️ Pause', + stop_option: '⏹️ Stop', + start_option: '▶️ Start', + reset_option: '🔄 Reset', + + diffuseColor_option: 'diffuse color', + emissiveColor_option: 'emissive color', + ambientColor_option: 'ambient color', + specularColor_option: 'specular color', + alpha_option: 'alpha', + color_option: 'color', + position_option: 'position', + rotation_option: 'rotation', + scaling_option: 'scaling', + position_x_option: 'position.x', + position_y_option: 'position.y', + position_z_option: 'position.z', + rotation_x_option: 'rotation.x', + rotation_y_option: 'rotation.y', + rotation_z_option: 'rotation.z', + scaling_x_option: 'scaling.x', + scaling_y_option: 'scaling.y', + scaling_z_option: 'scaling.z', + + rotateLeft_option: 'Rotate Left', + rotateRight_option: 'Rotate Right', + rotateUp_option: 'Look Up', + rotateDown_option: 'Look Down', + moveUp_option: 'Move Up', + moveDown_option: 'Move Down', + moveLeft_option: 'Move Left', + moveRight_option: 'Move Right', + + _65_option: 'A ◁', + _68_option: 'D', + _87_option: 'W', + _83_option: 'S', + _81_option: 'Q', + _69_option: 'E', + _70_option: 'F', + _32_option: 'Space', + _38_option: 'Up Arrow', + _40_option: 'Down Arrow', + _37_option: 'Left Arrow', + _39_option: 'Right Arrow', + + TOP_option: 'top', + CENTER_option: 'center', + BOTTOM_option: 'bottom', + CENTRE_option: 'center', + LEFT_option: 'left', + RIGHT_option: 'right', + BASE_option: 'base', + FRONT_option: 'front', + BACK_option: 'back', + forward_option: 'forward', + sideways_option: 'sideways', + strafe_option: 'strafe', + MIN_option: 'min', + MAX_option: 'max', + user_option: 'front', + environment_option: 'back', + + LINEAR_option: 'Linear', + NONE_option: 'None', + EXP_option: 'Exp', + EXP2_option: 'Exp2', + + OnPickTrigger_option: 'clicked', + OnLeftPickTrigger_option: 'interact', + OnDoublePickTrigger_option: 'double interact', + OnPickDownTrigger_option: 'interact start', + OnPickUpTrigger_option: 'interact end', + + OnIntersectionEnterTrigger_option: 'enter', + OnIntersectionExitTrigger_option: 'exit', + + _0_option: '0', + _1_option: '1', + _2_option: '2', + _3_option: '3', + _4_option: '4', + _5_option: '5', + _6_option: '6', + _7_option: '7', + _8_option: '8', + _9_option: '9', + a_option: 'A', + b_option: 'B', + c_option: 'C', + d_option: 'D', + e_option: 'E', + f_option: 'F', + g_option: 'G', + h_option: 'H', + i_option: 'I', + j_option: 'J', + k_option: 'K', + l_option: 'L', + m_option: 'M', + n_option: 'N', + o_option: 'O', + p_option: 'P', + q_option: 'Q', + r_option: 'R', + s_option: 'S', + t_option: 'T', + u_option: 'U', + v_option: 'V', + w_option: 'W', + x_option: 'X', + y_option: 'Y', + z_option: 'Z', + space_option: ' ', + comma_option: ',', + dot_option: '.', + slash_option: '/', + ArrowLeft_option: '◁', + ArrowUp_option: '△', + ArrowRight_option: '▷', + ArrowDown_option: '▽', + + pressed_option: 'pressed', + released_option: 'released', + starts_option: 'starts', + ends_option: 'ends', + + DYNAMIC_option: 'dynamic', + ANIMATED_option: 'animated', + STATIC_option: 'static', + + MESH_option: 'object', + CAPSULE_option: 'capsule', + + FLAT_option: 'Flat', // Duplicate key NONE + + ANY_option: 'any', + all_option: 'all', + objectGrab_option: 'grab', + objectDrop_option: 'drop', + smallCollision_option: 'small bump', + heavyCollision_option: 'heavy crash', + snapToGrid_option: 'snap', + errorInvalid_option: 'error', + successConfirmation_option: 'success', + slidingGravel_option: 'gravel slide', + slidingMetal_option: 'metal slide', + machineRunning_option: 'machine', + explosion_option: 'explosion', + teleport_option: 'teleport', + space_infinity_option: 'space ❖', // Duplicate key space + q_icon_option: 'Q ■', // Duplicate key q + e_icon_option: 'E ✿', // Duplicate key e + f_icon_option: 'F ✱', // Duplicate key f + + x_coordinate_option: 'x', + y_coordinate_option: 'y', + z_coordinate_option: 'z', + + POSITION_X_option: 'position x', + POSITION_Y_option: 'position y', + POSITION_Z_option: 'position z', + ROTATION_X_option: 'rotation x', + ROTATION_Y_option: 'rotation y', + ROTATION_Z_option: 'rotation z', + MIN_X_option: 'min x', + MAX_X_option: 'max x', + MIN_Y_option: 'min y', + MAX_Y_option: 'max y', + MIN_Z_option: 'min z', + MAX_Z_option: 'max z', + SCALE_X_option: 'scale x', + SCALE_Y_option: 'scale y', + SCALE_Z_option: 'scale z', + SIZE_X_option: 'size x', + SIZE_Y_option: 'size y', + SIZE_Z_option: 'size z', + VISIBLE_option: 'visible', + ALPHA_option: 'alpha', + COLOUR_option: 'color', + DESCRIPTION_option: 'description', + AUTO_option: 'auto', + ENABLED_option: 'enabled', + DISABLED_option: 'disabled', + + BOTH_option: 'both', + ARROWS_option: 'arrows', + ACTIONS_option: 'actions', + JOYSTICK_option: 'joystick', + YES_option: 'yes', + NO_option: 'no', + ACTION_FORWARD_option: 'forward', + ACTION_BACKWARD_option: 'backward', + ACTION_LEFT_option: 'left', + ACTION_RIGHT_option: 'right', + ACTION_BUTTON1_option: 'button 1', + ACTION_BUTTON2_option: 'button 2', + ACTION_BUTTON3_option: 'button 3', + ACTION_BUTTON4_option: 'button 4', + + pin_0_option: 'Pin P0 released', // Duplicate key 0 + pin_1_option: 'Pin P1 released', // Duplicate key 1 + pin_2_option: 'Pin P2 released', // Duplicate key 2 + pin_l_option: 'Logo long pressed', // All have duplicate keys + pin_j_option: 'Logo touched', + pin_h_option: 'Logo pressed', + pin_k_option: 'Logo released', + pin_space_option: 'Button A pressed', + pin_q_option: 'Button B pressed', + pin_r_option: 'Button A+B pressed', + pin_t_option: 'Gesture: FreeFall', + pin_o_option: 'Gesture: LogoUp', + pin_p_option: 'Gesture: LogoDown', + pin_a_option: 'Gesture: TiltLeft', + pin_d_option: 'Gesture: TiltRight', + pin_y_option: 'Gesture: ScreenUp', + pin_g_option: 'Gesture: ScreenDown', + pin_i_option: 'Gesture: Shake', + + SMALL_option: 'small', + MEDIUM_option: 'medium', + LARGE_option: 'large', + + ONCE_option: 'once', + LOOP_option: 'loop', + everywhere_option: 'everywhere', + + theme_bright_option: 'Bright', + theme_calm_option: 'Calm', + theme_electronic_option: 'Electronic', + theme_game_option: 'Game', + theme_medieval_option: 'Medieval', + theme_metal_option: 'Metal', + + sound_highDown_option: 'High Down', + sound_highUp_option: 'High Up', + sound_laser1_option: 'Laser 1', + sound_laser2_option: 'Laser 2', + sound_laser3_option: 'Laser 3', + sound_lowDown_option: 'Low Down', + sound_lowRandom_option: 'Low Random', + sound_lowThreeTone_option: 'Low Three Tone', + sound_phaseJump1_option: 'Phase Jump 1', + sound_powerUp1_option: 'Power Up 1', + sound_powerUp2_option: 'Power Up 2', + sound_powerUp3_option: 'Power Up 3', + sound_powerUp4_option: 'Power Up 4', + sound_powerUp5_option: 'Power Up 5', + sound_spaceTrash_option: 'Space Trash', + sound_threeTone1_option: 'Three Tone 1', + sound_threeTone2_option: 'Three Tone 2', + sound_chop_option: 'Chop', + sound_creak_option: 'Creak', + sound_footstep_option: 'Footstep', + sound_door_open_option: 'Door Open', + sound_door_close_option: 'Door Close', + sound_metal_latch_option: 'Metal Latch', + + sine_option: 'sine', + square_option: 'square', + sawtooth_option: 'sawtooth', + triangle_option: 'triangle', + + none_option: 'none', + tremolo_option: 'tremolo', + vibrato_option: 'vibrato', + warble_option: 'warble', + robot_option: 'robot', + + default_option: 'Default Instrument (Sine)', + piano_option: 'Piano (Square)', + guitar_option: 'Guitar (Sawtooth)', + violin_option: 'Violin (Triangle)', + + female_option: 'female', + male_option: 'male', + + en_GB_option: 'English (UK)', + en_US_option: 'English (US)', + + ADD_option: 'add', + REPLACE_option: 'replace', + + _14px_option: 'small', + _18px_option: 'medium', + _24px_option: 'large', + + __fonts_FreeSans_Bold_json_option: 'Free Sans', + + VR_option: 'VR (Oculus Quest or phone viewer)', + AR_option: 'AR (Augmented Reality)', + MAGIC_WINDOW_option: 'Magic Window (look-around)', + + circular_depression_png_option: 'Circular Dip', + checkerboard_png_option: 'Checkerboard', + sloped_plane_png_option: 'Sloped Plane', + cove_plateau_png_option: 'Cove Plateau', + random_hills_png_option: 'Random Hills', + diagonal_ridge_png_option: 'Diagonal Ridge', + mixed_heights_png_option: 'Mixed Heights', + uneven_terrain_png_option: 'Uneven Terrain', + mountains_png_option: 'Mountains', + Islands_png_option: 'Islands', + Lookout_png_option: 'Lookout', + Valley_png_option: 'Valley', + + Idle_option: 'Idle', + Walk_option: 'Walk', + Run_option: 'Run', + Wave_option: 'Wave', + Yes_option: 'Yes', + No_option: 'No', + Duck_option: 'Duck', + Fall_option: 'Fall', + Fly_option: 'Fly', + Jump_option: 'Jump', + Flip_option: 'Flip', + Dance1_option: 'Dance1', + Dance2_option: 'Dance2', + Dance3_option: 'Dance3', + Dance4_option: 'Dance4', + JumpUp_option: 'Jump Up', + JumpIdle_option: 'Jump Idle', + JumpLand_option: 'Jump Land', + Punch_option: 'Punch', + HitReact_option: 'Hit React', + Idle_Hold_option: 'Idle Hold', + Walk_Hold_option: 'Walk Hold', + Run_Hold_option: 'Run Hold', + Idle_Attack_option: 'Idle Reach', + Walk_Attack_option: 'Walk Reach', + Run_Attack_option: 'Run Reach', + Sit_Down_option: 'Sit Down', + Sitting_option: 'Sitting', + Stand_Up_option: 'Stand Up', + Wobble_option: 'Wobble', + Clap_option: 'Clap', + Climb_rope_option: 'Climb rope', // HTML translations - loading_ui: "Loading Flock XR...", - loading_success_ui: "Flock XR loaded successfully", - canvas_accessible_name_ui: "3D scene", - loading_title_ui: "Loading Flock XR", - import_project_file_ui: "Import project file", - - demo_ui: "Demo", - new_ui: "New", - starter_ui: "👋🏽 Starter", - character_designer_ui: "👚 Character designer", - controller_starter_ui: "🎮 Controller starter", - snow_globe_ui: "❄️ Snow globe", - forest_base_ui: "🌲 Forest base", - character_animation_ui: "🎥 Character animation", - cube_art_ui: "🎨 Cube art", - physics_fun_ui: "👆🏾 Physics fun", - collect_the_gems_ui: "💎 Collect the gems", - water_map_ui: "💧 Water map", - skittles_ui: "🎳 Skittles", - beetle_ui: "🎲 Beetle", - roominator_ui: "🛋️ Roominator", - sit_down_ui: "🪑 Sit down", - ball_pit_ui: "🟠 Ball pit", - ur_enough_ui: "💗 UR enough!", - tallest_buildings_ui: "📊 Tallest buildings", - candy_dash_ui: "🎃 Candy dash", - flockenspiel_ui: "🎵 Flockenspiel", - pendant_ui: "📿 3D-printable pendant", - tent_lights_ui: "⛺ Festival tent", - my_place_ui: "🏠 My place", - microbit_monkey_ui: "🐵 micro:bit monkey", - tree_jump_ui: "🌳 Tree jump", - shape_push_ui: "🔶 Shape push", - alien_planet_ui: "👽 Alien planet", - boat_trip_ui: "⛵ Boat trip", - main_menu_ui: "Main menu", - menu_button_sr_label_ui: "Menu", - project_submenu_ui: "Project", - project_new_ui: "New", - project_open_ui: "Open", - project_save_ui: "Save", - language_submenu_ui: "Language", - about_submenu_ui: "About", - hub_submenu_ui: "Hub", - - theme_submenu_ui: "Theme", - light_theme_ui: "Light", - dark_theme_ui: "Dark 2", - dark_contrast_theme_ui: "Dark", - low_vision_theme_ui: "Low vision", - contrast_theme_ui: "Contrast", - - run_code_button_ui: "Run your code", - stop_code_button_ui: "Stop your code", - open_button_ui: "Open a project from a file on your computer", - open_file_input_label_ui: "Select project file to open", - export_code_button_ui: "Save this project to a file on your computer.", - example_select_ui: "Choose an example project to load", - - toggle_design_ui: "Design your project", - toggle_play_ui: "Use your project", - fullscreen_toggle_ui: "Switch between fullscreen and normal views.", - - show_shapes_button_ui: "Add shapes and models", - color_picker_button_ui: "Choose a color", - position_button_ui: "Position object", - rotation_button_ui: "Rotate object", - scale_button_ui: "Scale object size", - select_button_ui: "Select object", - duplicate_button_ui: "Duplicate selected object", - delete_button_ui: "Delete selected object", - camera_button_ui: "Camera controls", - - info_panel_link_ui: "Visit Flock XR website (opens in new tab)", - - project_name_ui: "Project name", - - about_heading_ui: "About Flock XR", - about_description_intro_ui: "Flock XR is a", - about_description_prototype_ui: "web app", - about_description_made_by_ui: " made by ", - about_description_company_ui: "Flip Computing", + loading_ui: 'Loading Flock XR...', + loading_success_ui: 'Flock XR loaded successfully', + canvas_accessible_name_ui: '3D scene', + loading_title_ui: 'Loading Flock XR', + import_project_file_ui: 'Import project file', + + demo_ui: 'Demo', + new_ui: 'New', + starter_ui: '👋🏽 Starter', + character_designer_ui: '👚 Character designer', + controller_starter_ui: '🎮 Controller starter', + snow_globe_ui: '❄️ Snow globe', + forest_base_ui: '🌲 Forest base', + character_animation_ui: '🎥 Character animation', + cube_art_ui: '🎨 Cube art', + physics_fun_ui: '👆🏾 Physics fun', + collect_the_gems_ui: '💎 Collect the gems', + water_map_ui: '💧 Water map', + skittles_ui: '🎳 Skittles', + beetle_ui: '🎲 Beetle', + roominator_ui: '🛋️ Roominator', + sit_down_ui: '🪑 Sit down', + ball_pit_ui: '🟠 Ball pit', + ur_enough_ui: '💗 UR enough!', + tallest_buildings_ui: '📊 Tallest buildings', + candy_dash_ui: '🎃 Candy dash', + flockenspiel_ui: '🎵 Flockenspiel', + pendant_ui: '📿 3D-printable pendant', + tent_lights_ui: '⛺ Festival tent', + my_place_ui: '🏠 My place', + microbit_monkey_ui: '🐵 micro:bit monkey', + tree_jump_ui: '🌳 Tree jump', + shape_push_ui: '🔶 Shape push', + alien_planet_ui: '👽 Alien planet', + boat_trip_ui: '⛵ Boat trip', + main_menu_ui: 'Main menu', + menu_button_sr_label_ui: 'Menu', + project_submenu_ui: 'Project', + project_new_ui: 'New', + project_open_ui: 'Open', + project_save_ui: 'Save', + language_submenu_ui: 'Language', + about_submenu_ui: 'About', + hub_submenu_ui: 'Hub', + + theme_submenu_ui: 'Theme', + light_theme_ui: 'Light', + dark_theme_ui: 'Dark 2', + dark_contrast_theme_ui: 'Dark', + low_vision_theme_ui: 'Low vision', + contrast_theme_ui: 'Contrast', + + run_code_button_ui: 'Run your code', + stop_code_button_ui: 'Stop your code', + open_button_ui: 'Open a project from a file on your computer', + open_file_input_label_ui: 'Select project file to open', + export_code_button_ui: 'Save this project to a file on your computer.', + example_select_ui: 'Choose an example project to load', + + toggle_design_ui: 'Design your project', + toggle_play_ui: 'Use your project', + fullscreen_toggle_ui: 'Switch between fullscreen and normal views.', + + show_shapes_button_ui: 'Add shapes and models', + color_picker_button_ui: 'Choose a color', + position_button_ui: 'Position object', + rotation_button_ui: 'Rotate object', + scale_button_ui: 'Scale object size', + select_button_ui: 'Select object', + duplicate_button_ui: 'Duplicate selected object', + delete_button_ui: 'Delete selected object', + camera_button_ui: 'Camera controls', + + info_panel_link_ui: 'Visit Flock XR website (opens in new tab)', + + project_name_ui: 'Project name', + + about_heading_ui: 'About Flock XR', + about_description_intro_ui: 'Flock XR is a', + about_description_prototype_ui: 'web app', + about_description_made_by_ui: ' made by ', + about_description_company_ui: 'Flip Computing', about_description_disclaimer_ui: " We're working on improvements to Flock XR all the time. Please let us know if you have suggestions or you are able to support the development of Flock XR.", about_run_intro_ui: - "Take a look at the demos to see what you can do. Make some changes and click", - about_run_action_ui: "run.", - about_links_privacy_prefix_ui: "View the ", - about_links_privacy_label_ui: "privacy policy", - about_links_privacy_suffix_ui: " for Flock XR. ", - about_links_contact_label_ui: "Get in touch", - - keyboard_controls_ui: "Keyboard controls info [Ctrl + /]", - keyboard_menu_ui: "Main menu", - keyboard_play_ui: "Play", - keyboard_gizmos_ui: "Gizmos", - - keyboard_workspace_ui: "Code editor", - keyboard_navigation_ui: - "Browser navigation bar (overridden shortcuts work from here)", + 'Take a look at the demos to see what you can do. Make some changes and click', + about_run_action_ui: 'run.', + about_links_privacy_prefix_ui: 'View the ', + about_links_privacy_label_ui: 'privacy policy', + about_links_privacy_suffix_ui: ' for Flock XR. ', + about_links_contact_label_ui: 'Get in touch', + + keyboard_controls_ui: 'Keyboard controls info [Ctrl + /]', + keyboard_menu_ui: 'Main menu', + keyboard_play_ui: 'Play', + keyboard_gizmos_ui: 'Gizmos', + + keyboard_workspace_ui: 'Code editor', + keyboard_navigation_ui: 'Browser navigation bar (overridden shortcuts work from here)', // Accessibility and announcements - unmute_audio_aria: "Unmute audio.", - focused_main_content: "Focused main content.", - toolbox_search_results_aria: "Toolbox search results.", - context_delete_option: "Delete", - context_delete_all_blocks_option: "Delete all blocks", - context_inline_inputs_option: "Horizontal inputs", - context_external_inputs_option: "Vertical inputs", - context_collapse_option: "Collapse", - context_expand_option: "Expand", - context_collapse_all_option: "Collapse all", - context_expand_all_option: "Expand all", - context_disable_option: "Disable", - context_enable_option: "Enable", - context_copy_option: "Copy", - context_paste_option: "Paste", - context_cut_option: "Cut", - canvas_focus_navigation: - "3D canvas focused. Use arrow keys or WASD to navigate.", - design_tool_label: "Design tool", - focused_element_suffix: "{name} focused", - search_toolbox_focused: "Search toolbox focused", - workspace_search_placeholder: "Find in workspace", - close: "Close", - toolbox_search_placeholder: "Search", - search_no_matching: "No matching blocks found", - code_workspace_focused: "Code workspace focused", - interactive_element_label: "Interactive element", - panel_resizer_focused: - "Panel resizer focused. Use arrow keys to resize panels, Home to reset.", - undo_performed: "Undo performed", - redo_performed: "Redo performed", - camera_moving_forward: "Camera moving forward", - camera_moving_backward: "Camera moving backward", - camera_moving_left: "Camera moving left", - camera_moving_right: "Camera moving right", - moving_forward: "Moving forward", - moving_backward: "Moving backward", - moving_left: "Moving left", - moving_right: "Moving right", - action_triggered: "Action triggered", - snippet_file_description: "Flock XR Snippet", - snippet_filename_prompt: "Enter a filename for the snippet:", - project_file_description: "Flock XR Project", - file_too_large_alert: "File too large. Maximum size is 5MB.", - invalid_filetype_alert: "Only .json or .flock project files are allowed.", + unmute_audio_aria: 'Unmute audio.', + focused_main_content: 'Focused main content.', + toolbox_search_results_aria: 'Toolbox search results.', + context_delete_option: 'Delete', + context_delete_all_blocks_option: 'Delete all blocks', + context_inline_inputs_option: 'Horizontal inputs', + context_external_inputs_option: 'Vertical inputs', + context_collapse_option: 'Collapse', + context_expand_option: 'Expand', + context_collapse_all_option: 'Collapse all', + context_expand_all_option: 'Expand all', + context_disable_option: 'Disable', + context_enable_option: 'Enable', + context_copy_option: 'Copy', + context_paste_option: 'Paste', + context_cut_option: 'Cut', + canvas_focus_navigation: '3D canvas focused. Use arrow keys or WASD to navigate.', + design_tool_label: 'Design tool', + focused_element_suffix: '{name} focused', + search_toolbox_focused: 'Search toolbox focused', + workspace_search_placeholder: 'Find in workspace', + close: 'Close', + toolbox_search_placeholder: 'Search', + search_no_matching: 'No matching blocks found', + code_workspace_focused: 'Code workspace focused', + interactive_element_label: 'Interactive element', + panel_resizer_focused: 'Panel resizer focused. Use arrow keys to resize panels, Home to reset.', + undo_performed: 'Undo performed', + redo_performed: 'Redo performed', + camera_moving_forward: 'Camera moving forward', + camera_moving_backward: 'Camera moving backward', + camera_moving_left: 'Camera moving left', + camera_moving_right: 'Camera moving right', + moving_forward: 'Moving forward', + moving_backward: 'Moving backward', + moving_left: 'Moving left', + moving_right: 'Moving right', + action_triggered: 'Action triggered', + snippet_file_description: 'Flock XR Snippet', + snippet_filename_prompt: 'Enter a filename for the snippet:', + project_file_description: 'Flock XR Project', + file_too_large_alert: 'File too large. Maximum size is 5MB.', + invalid_filetype_alert: 'Only .json or .flock project files are allowed.', invalid_project_alert: "This file isn't a valid Flock XR project.", - failed_to_read_file_alert: "Failed to read file.", - drag_drop_hint: "Drop to open project or import snippet", - drop_unsupported_file_alert: - "Only .flock, .json, .fsnip, or .png files can be dropped.", + failed_to_read_file_alert: 'Failed to read file.', + drag_drop_hint: 'Drop to open project or import snippet', + drop_unsupported_file_alert: 'Only .flock, .json, .fsnip, or .png files can be dropped.', // UI status messages - max_mesh_limit_reached: - "⚠️ Limit reached: You can only have {max} objects in your world.", - high_memory_usage_warning: "Warning: High memory usage ({percent}%)", - physics_out_of_memory_log: - "Havok physics aborted, likely due to running out of memory.", + max_mesh_limit_reached: '⚠️ Limit reached: You can only have {max} objects in your world.', + high_memory_usage_warning: 'Warning: High memory usage ({percent}%)', + physics_out_of_memory_log: 'Havok physics aborted, likely due to running out of memory.', physics_out_of_memory_banner_ui: - "Physics engine ran out of memory. Try reducing the number of physics objects or reloading your project.", - runtime_error_message: "Error: {message}", - error_audio: "Sound is not available on this device. Your project will run without audio.", + 'Physics engine ran out of memory. Try reducing the number of physics objects or reloading your project.', + runtime_error_message: 'Error: {message}', + error_audio: 'Sound is not available on this device. Your project will run without audio.', error_startup: "Flock couldn't start up. Try reloading the page.", error_project_crash: - "Your project hit a problem. Press Stop, check your blocks, then press Play again.", - error_webgl_restoring: "3D view paused, restoring…", - error_webgl_lost: "The 3D view stopped working. Try reloading the page.", + 'Your project hit a problem. Press Stop, check your blocks, then press Play again.', + error_webgl_restoring: '3D view paused, restoring…', + error_webgl_lost: 'The 3D view stopped working. Try reloading the page.', error_physics_oom: - "Your project ran out of memory. Try reloading the page and using fewer blocks.", - banner_reload: "Reload", - banner_dismiss: "Dismiss", - xr_mode_message: "XR Mode!", - fly_camera_instructions: "ℹ️ Fly camera, use arrow keys and page up/down", - select_mesh_delete_prompt: "ℹ️ Click an object to delete it.", - select_mesh_duplicate_prompt: - "ℹ️ Select an object to duplicate, then click to place copies.", - place_object_prompt: "ℹ️ Click on a surface to place the object.", - position_readout: "Position: {position}", + 'Your project ran out of memory. Try reloading the page and using fewer blocks.', + banner_reload: 'Reload', + banner_dismiss: 'Dismiss', + xr_mode_message: 'XR Mode!', + fly_camera_instructions: 'ℹ️ Fly camera, use arrow keys and page up/down', + select_mesh_delete_prompt: 'ℹ️ Click an object to delete it.', + select_mesh_duplicate_prompt: 'ℹ️ Select an object to duplicate, then click to place copies.', + place_object_prompt: 'ℹ️ Click on a surface to place the object.', + position_readout: 'Position: {position}', eyedropper_not_supported_alert: - "Color picker tool is not supported in this browser. Try using Chrome or Edge.", - blocks_copied_alert: "Blocks copied to local storage!", - no_blocks_to_copy_alert: "No blocks available to copy.", - copy_blocks_failed_alert: "Failed to copy blocks.", + 'Color picker tool is not supported in this browser. Try using Chrome or Edge.', + blocks_copied_alert: 'Blocks copied to local storage!', + no_blocks_to_copy_alert: 'No blocks available to copy.', + copy_blocks_failed_alert: 'Failed to copy blocks.', // Model display names - model_display_liz1: "Cat", - model_display_liz2: "Monkey", - model_display_liz3: "Character with long ponytail", - model_display_liz4: "Character with spiky hair", - model_display_liz5: "Alien", - model_display_liz6: "Elf", - model_display_block1: "Block character with high hair bunches", - model_display_block2: "Block character with long hair bunches", - model_display_block3: "Block character with ponytail", - model_display_block4: "Block character with short curly hair", - model_display_block5: "Block character with short hair", - model_display_block6: "Block character with long hair", - model_display_tree: "Tree", - model_display_tree2: "Tree with two branches", - model_display_tree3: "Winter tree", - model_display_tree4: "Fir tree", - model_display_hut: "Round hut", - model_display_hut2: "Cabin", - model_display_hut3: "Festival tent", - model_display_hut4: "Toadstool hut", - model_display_rocks: "Rock platform", - model_display_rocks2: "Corner rock", - model_display_rocks3: "Multi level rock", - model_display_rocks4: "Waterfall", - model_display_pond: "Pond", - model_display_boat: "Ship", - model_display_airplane: "Airplane", - model_display_airplane2: "Airplane with seat", - model_display_skateboard: "Skateboard", - model_display_humped: "Humped bridge", - model_display_jetty: "Jetty", - model_display_flower: "Flower", - model_display_flower2: "Tulip", - model_display_star: "Star", - model_display_heart: "Heart", - model_display_coin: "Coin", - model_display_egg: "Egg", - model_display_gem1: "Square gem", - model_display_gem2: "Diamond", - model_display_gem3: "Long gem", - model_display_key: "Key", - model_display_wand: "Wand", - model_display_hat: "Hat", - model_display_donut: "Donut", - model_display_pumpkin: "Pumpkin", - model_display_apple: "Apple", - model_display_starboppers: "Starboppers", - model_display_headphones: "Headphones", - model_display_flock: "Flock bird", - model_display_flock_santa: "Flock bird with Santa hat", - model_display_character: "Character", - model_display_rhino: "Rhino", + model_display_liz1: 'Cat', + model_display_liz2: 'Monkey', + model_display_liz3: 'Character with long ponytail', + model_display_liz4: 'Character with spiky hair', + model_display_liz5: 'Alien', + model_display_liz6: 'Elf', + model_display_block1: 'Block character with high hair bunches', + model_display_block2: 'Block character with long hair bunches', + model_display_block3: 'Block character with ponytail', + model_display_block4: 'Block character with short curly hair', + model_display_block5: 'Block character with short hair', + model_display_block6: 'Block character with long hair', + model_display_tree: 'Tree', + model_display_tree2: 'Tree with two branches', + model_display_tree3: 'Winter tree', + model_display_tree4: 'Fir tree', + model_display_hut: 'Round hut', + model_display_hut2: 'Cabin', + model_display_hut3: 'Festival tent', + model_display_hut4: 'Toadstool hut', + model_display_rocks: 'Rock platform', + model_display_rocks2: 'Corner rock', + model_display_rocks3: 'Multi level rock', + model_display_rocks4: 'Waterfall', + model_display_pond: 'Pond', + model_display_boat: 'Ship', + model_display_airplane: 'Airplane', + model_display_airplane2: 'Airplane with seat', + model_display_skateboard: 'Skateboard', + model_display_humped: 'Humped bridge', + model_display_jetty: 'Jetty', + model_display_flower: 'Flower', + model_display_flower2: 'Tulip', + model_display_star: 'Star', + model_display_heart: 'Heart', + model_display_coin: 'Coin', + model_display_egg: 'Egg', + model_display_gem1: 'Square gem', + model_display_gem2: 'Diamond', + model_display_gem3: 'Long gem', + model_display_key: 'Key', + model_display_wand: 'Wand', + model_display_hat: 'Hat', + model_display_donut: 'Donut', + model_display_pumpkin: 'Pumpkin', + model_display_apple: 'Apple', + model_display_starboppers: 'Starboppers', + model_display_headphones: 'Headphones', + model_display_flock: 'Flock bird', + model_display_flock_santa: 'Flock bird with Santa hat', + model_display_character: 'Character', + model_display_rhino: 'Rhino', // Context menu option translations - export_JSON_snippet: "Export block as snippet", - import_snippet: "Import snippet", - export_PNG_snippet: "Export as PNG", - export_SVG_snippet: "Export as SVG", + export_JSON_snippet: 'Export block as snippet', + import_snippet: 'Import snippet', + export_PNG_snippet: 'Export as PNG', + export_SVG_snippet: 'Export as SVG', // New variable option - new_variable_decision: "New variable...", + new_variable_decision: 'New variable...', // Attachment point options - LeftHand_option: "Left Hand", - RightHand_option: "Right Hand", - Head_option: "Head", - Hips_option: "Hips", - Spine_option: "Spine", - Spine1_option: "Spine 1", - Spine2_option: "Spine 2", - Neck_option: "Neck", - LeftShoulder_option: "Left Shoulder", - LeftArm_option: "Left Upper Arm", - LeftForeArm_option: "Left Forearm", - RightShoulder_option: "Right Shoulder", - RightArm_option: "Right Upper Arm", - RightForeArm_option: "Right Forearm", - LeftUpLeg_option: "Left Thigh", - LeftLeg_option: "Left Shin", - LeftFoot_option: "Left Foot", - RightUpLeg_option: "Right Thigh", - RightLeg_option: "Right Shin", - RightFoot_option: "Right Foot", + LeftHand_option: 'Left Hand', + RightHand_option: 'Right Hand', + Head_option: 'Head', + Hips_option: 'Hips', + Spine_option: 'Spine', + Spine1_option: 'Spine 1', + Spine2_option: 'Spine 2', + Neck_option: 'Neck', + LeftShoulder_option: 'Left Shoulder', + LeftArm_option: 'Left Upper Arm', + LeftForeArm_option: 'Left Forearm', + RightShoulder_option: 'Right Shoulder', + RightArm_option: 'Right Upper Arm', + RightForeArm_option: 'Right Forearm', + LeftUpLeg_option: 'Left Thigh', + LeftLeg_option: 'Left Shin', + LeftFoot_option: 'Left Foot', + RightUpLeg_option: 'Right Thigh', + RightLeg_option: 'Right Shin', + RightFoot_option: 'Right Foot', // Service worker update notification - update_available_ui: "A new version of Flock is available.", - reload_button_ui: "Reload", + update_available_ui: 'A new version of Flock is available.', + reload_button_ui: 'Reload', // Workspace toolbar - toolbar_undo_ui: "Undo", - toolbar_redo_ui: "Redo", - toolbar_zoom_out_ui: "Zoom out", - toolbar_zoom_in_ui: "Zoom in", + toolbar_undo_ui: 'Undo', + toolbar_redo_ui: 'Redo', + toolbar_zoom_out_ui: 'Zoom out', + toolbar_zoom_in_ui: 'Zoom in', // Keyboard shortcuts panel — title and close button - shortcut_panel_title: "Keyboard Controls", - shortcut_panel_close: "Close keyboard shortcuts", - shortcut_panel_help_link: "Open keyboard controls help link", + shortcut_panel_title: 'Keyboard Controls', + shortcut_panel_close: 'Close keyboard shortcuts', + shortcut_panel_help_link: 'Open keyboard controls help link', // Keyboard shortcuts panel — labels - shortcut_show_hide_help: "Show/hide shortcut help", - shortcut_move_between_areas: "Move between menus, canvas and editor", - shortcut_confirm: "Confirm", - shortcut_exit: "Exit", - shortcut_play: "Play", - shortcut_undo: "Undo", - shortcut_redo: "Redo", - shortcut_browser_nav: - "Browser navigation bar (overridden shortcuts work from here)", - shortcut_main_menu: "Main menu", - shortcut_open_file: "Open file", - shortcut_save_export: "Save / export", - shortcut_open_close_area_menu: "Open/close area menu", - shortcut_toggle_area: "Toggle area", - shortcut_select_area: "Select area", - shortcut_code_editor: "Code editor", - shortcut_toolbox: "Toolbox", - shortcut_add_block_by_name: "Add block by name", - shortcut_add_block: "Add selected block", - shortcut_search_block: "Search for a block", - shortcut_select_next_result: "Search: Next result", - shortcut_select_previous_result: "Search: Previous result", - shortcut_focus_result: "Search: Go to selected block", - shortcut_nav_toolbox_blocks: "Navigate categories and blocks", - shortcut_toolbox_typing: "Skip to category", - shortcut_toolbox_typing_hint: "Start typing its name", - shortcut_context_menu: "Open context menu", - shortcut_duplicate_block: "Duplicate block", - shortcut_detach_block: "Detach block", - shortcut_start_move_block: "Move block", - shortcut_move_arrows: "Move: to connection", - shortcut_move_anywhere: "Move: anywhere", - shortcut_select_workspace: "Select workspace", - shortcut_move_through_blocks: "Move through blocks", - shortcut_move_in_out_blocks: "Move in/out of a block", - shortcut_next_block_stack: "Next block stack", - shortcut_prev_block_stack: "Previous block stack", - shortcut_open_gizmos: "Gizmos", - shortcut_select_gizmo: "Select gizmo", - shortcut_keyboard_cursor_gizmos: "Keyboard cursor for gizmos", - shortcut_slow_cursor_gizmos: "Slow cursor movement", - shortcut_uniform_scale: "Uniform scale (all axes)", - shortcut_lock_transform: "Lock transform to axis", - shortcut_transform_3d: "Transform in 3D", - shortcut_focus_camera: "Focus camera on object", - shortcut_quick_colour: "Quick use colour in colour picker", - shortcut_delete_object: "Delete object", - - axis_x: "X axis", - axis_y: "Y axis", - axis_z: "Z axis", - axis_free: "Free", - axis_all: "Uniform (all axes)", + shortcut_show_hide_help: 'Show/hide shortcut help', + shortcut_move_between_areas: 'Move between menus, canvas and editor', + shortcut_confirm: 'Confirm', + shortcut_exit: 'Exit', + shortcut_play: 'Play', + shortcut_undo: 'Undo', + shortcut_redo: 'Redo', + shortcut_browser_nav: 'Browser navigation bar (overridden shortcuts work from here)', + shortcut_main_menu: 'Main menu', + shortcut_open_file: 'Open file', + shortcut_save_export: 'Save / export', + shortcut_open_close_area_menu: 'Open/close area menu', + shortcut_toggle_area: 'Toggle area', + shortcut_select_area: 'Select area', + shortcut_code_editor: 'Code editor', + shortcut_toolbox: 'Toolbox', + shortcut_add_block_by_name: 'Add block by name', + shortcut_add_block: 'Add selected block', + shortcut_search_block: 'Search for a block', + shortcut_select_next_result: 'Search: Next result', + shortcut_select_previous_result: 'Search: Previous result', + shortcut_focus_result: 'Search: Go to selected block', + shortcut_nav_toolbox_blocks: 'Navigate categories and blocks', + shortcut_toolbox_typing: 'Skip to category', + shortcut_toolbox_typing_hint: 'Start typing its name', + shortcut_context_menu: 'Open context menu', + shortcut_duplicate_block: 'Duplicate block', + shortcut_detach_block: 'Detach block', + shortcut_start_move_block: 'Move block', + shortcut_move_arrows: 'Move: to connection', + shortcut_move_anywhere: 'Move: anywhere', + shortcut_select_workspace: 'Select workspace', + shortcut_move_through_blocks: 'Move through blocks', + shortcut_move_in_out_blocks: 'Move in/out of a block', + shortcut_next_block_stack: 'Next block stack', + shortcut_prev_block_stack: 'Previous block stack', + shortcut_open_gizmos: 'Gizmos', + shortcut_select_gizmo: 'Select gizmo', + shortcut_keyboard_cursor_gizmos: 'Keyboard cursor for gizmos', + shortcut_slow_cursor_gizmos: 'Slow cursor movement', + shortcut_uniform_scale: 'Uniform scale (all axes)', + shortcut_lock_transform: 'Lock transform to axis', + shortcut_transform_3d: 'Transform in 3D', + shortcut_focus_camera: 'Focus camera on object', + shortcut_quick_colour: 'Quick use colour in colour picker', + shortcut_delete_object: 'Delete object', + + axis_x: 'X axis', + axis_y: 'Y axis', + axis_z: 'Z axis', + axis_free: 'Free', + axis_all: 'Uniform (all axes)', // Keyboard shortcuts panel — category names - shortcut_category_main: "Main", - shortcut_category_menu: "Menu", - shortcut_category_area_menu: "Area menu", - shortcut_category_toolbox: "Toolbox", - shortcut_category_editor: "Editor", - shortcut_category_gizmos: "Gizmos", + shortcut_category_main: 'Main', + shortcut_category_menu: 'Menu', + shortcut_category_area_menu: 'Area menu', + shortcut_category_toolbox: 'Toolbox', + shortcut_category_editor: 'Editor', + shortcut_category_gizmos: 'Gizmos', // Blockly keyboard navigation toast messages - KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT: "Use the arrow keys to navigate", - KEYBOARD_NAV_BLOCK_NAVIGATION_HINT: - "Use the right arrow key to navigate inside of blocks", - KEYBOARD_NAV_CONSTRAINED_MOVE_HINT: - "Use the arrow keys to move, then %1 to accept the position", + KEYBOARD_NAV_WORKSPACE_NAVIGATION_HINT: 'Use the arrow keys to navigate', + KEYBOARD_NAV_BLOCK_NAVIGATION_HINT: 'Use the right arrow key to navigate inside of blocks', + KEYBOARD_NAV_CONSTRAINED_MOVE_HINT: 'Use the arrow keys to move, then %1 to accept the position', KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT: - "Hold %1 and use arrow keys to move freely, then %2 to accept the position", - KEYBOARD_NAV_COPIED_HINT: "Copied. Press %1 to paste.", - KEYBOARD_NAV_CUT_HINT: "Cut. Press %1 to paste.", + 'Hold %1 and use arrow keys to move freely, then %2 to accept the position', + KEYBOARD_NAV_COPIED_HINT: 'Copied. Press %1 to paste.', + KEYBOARD_NAV_CUT_HINT: 'Cut. Press %1 to paste.', + DELETE_UNDO_HINT: 'Use the undo button to restore deleted blocks.', }; diff --git a/locale/es.js b/locale/es.js index 25df9dcc..6c925a03 100644 --- a/locale/es.js +++ b/locale/es.js @@ -1336,4 +1336,5 @@ export default { KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT: "Mantén %1 y usa las teclas de flecha para mover libremente, luego %2 para aceptar", // machine KEYBOARD_NAV_COPIED_HINT: "Copiado. Presiona %1 para pegar.", // machine KEYBOARD_NAV_CUT_HINT: "Cortado. Presiona %1 para pegar.", // machine + DELETE_UNDO_HINT: "Usa el botón deshacer para restaurar los bloques eliminados.", // machine }; diff --git a/locale/fr.js b/locale/fr.js index 8dab55e1..e2fbb296 100644 --- a/locale/fr.js +++ b/locale/fr.js @@ -1325,4 +1325,5 @@ export default { KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT: "Maintenez %1 et utilisez les touches fléchées pour vous déplacer librement, puis %2 pour accepter", // machine KEYBOARD_NAV_COPIED_HINT: "Copié. Appuyez sur %1 pour coller.", // machine KEYBOARD_NAV_CUT_HINT: "Coupé. Appuyez sur %1 pour coller.", // machine + DELETE_UNDO_HINT: "Utilisez le bouton Annuler pour restaurer les blocs supprimés.", // machine }; diff --git a/locale/it.js b/locale/it.js index 65c2413d..942446d3 100644 --- a/locale/it.js +++ b/locale/it.js @@ -1315,4 +1315,5 @@ export default { KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT: "Tieni premuto %1 e usa i tasti freccia per spostarti liberamente, poi %2 per accettare", // machine KEYBOARD_NAV_COPIED_HINT: "Copiato. Premi %1 per incollare.", // machine KEYBOARD_NAV_CUT_HINT: "Tagliato. Premi %1 per incollare.", // machine + DELETE_UNDO_HINT: "Usa il pulsante Annulla per ripristinare i blocchi eliminati.", // machine }; diff --git a/locale/pl.js b/locale/pl.js index 262a858a..e01ece8f 100644 --- a/locale/pl.js +++ b/locale/pl.js @@ -1320,4 +1320,5 @@ export default { KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT: "Przytrzymaj %1 i użyj klawiszy strzałek do swobodnego przesuwania, następnie %2 aby zaakceptować", // machine KEYBOARD_NAV_COPIED_HINT: "Skopiowano. Naciśnij %1, aby wkleić.", // machine KEYBOARD_NAV_CUT_HINT: "Wycięto. Naciśnij %1, aby wkleić.", // machine + DELETE_UNDO_HINT: "Użyj przycisku Cofnij, aby przywrócić usunięte bloki.", // machine }; diff --git a/locale/pt.js b/locale/pt.js index 987a9ba3..f7d66cb8 100644 --- a/locale/pt.js +++ b/locale/pt.js @@ -1312,4 +1312,5 @@ export default { KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT: "Segure %1 e use as teclas de seta para mover livremente, depois %2 para aceitar", // machine KEYBOARD_NAV_COPIED_HINT: "Copiado. Pressione %1 para colar.", // machine KEYBOARD_NAV_CUT_HINT: "Recortado. Pressione %1 para colar.", // machine + DELETE_UNDO_HINT: "Use o botão desfazer para restaurar os blocos excluídos.", // machine }; diff --git a/locale/sv.js b/locale/sv.js index 03245a4e..ad61420e 100644 --- a/locale/sv.js +++ b/locale/sv.js @@ -1302,4 +1302,5 @@ export default { KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT: "Håll inne %1 och använd piltangenterna för att flytta fritt, sedan %2 för att acceptera", // machine KEYBOARD_NAV_COPIED_HINT: "Kopierat. Tryck %1 för att klistra in.", // machine KEYBOARD_NAV_CUT_HINT: "Klippt ut. Tryck %1 för att klistra in.", // machine + DELETE_UNDO_HINT: "Använd ångra-knappen för att återställa borttagna block.", // machine }; diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 045c02a9..94d64325 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2852,7 +2852,15 @@ export function createBlocklyWorkspace() { if (count > 1) { const msg = (Blockly.Msg['DELETE_ALL_BLOCKS'] || 'Delete all %1 blocks?').replace('%1', count); Blockly.dialog.confirm(msg, (ok) => { - if (ok) { hideBlockToolbar(); block.checkAndDelete(); } + if (!ok) return; + hideBlockToolbar(); + block.checkAndDelete(); + Blockly.Toast.show(workspace, { + message: translate('DELETE_UNDO_HINT'), + id: 'delete-undo-tip', + oncePerSession: true, + duration: 8, + }); }); } else { hideBlockToolbar(); From 6dff7765c5e019096688b39fcc266193015bc15d Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:04:42 +0100 Subject: [PATCH 06/12] Add comment button --- main/blocklyinit.js | 34 +++++++++++++++++++++++++++++++++- style/blockly.css | 6 ++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 94d64325..4e5227dd 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -2747,7 +2747,21 @@ export function createBlocklyWorkspace() { '0 0 640 512' ); - blockToolbar.append(duplicateBtn, detachBtn, deleteBtn); + const commentBtn = document.createElement('button'); + commentBtn.type = 'button'; + commentBtn.className = 'fc-block-toolbar-btn'; + commentBtn.setAttribute('aria-label', 'Add comment'); + const commentAddSvg = mkFaSvg( + '', + '0 0 512 512' + ); + const commentDeleteSvg = mkFaSvg( + '', + '0 0 640 512' + ); + commentBtn.innerHTML = commentAddSvg; + + blockToolbar.append(duplicateBtn, detachBtn, commentBtn, deleteBtn); let toolbarBlock = null; let toolbarShowTimer = null; @@ -2769,6 +2783,9 @@ export function createBlocklyWorkspace() { function showBlockToolbar(block) { toolbarBlock = block; detachBtn.disabled = !isDetachable(block); + const hasComment = block.getCommentText() !== null; + commentBtn.setAttribute('aria-label', hasComment ? 'Delete comment' : 'Add comment'); + commentBtn.innerHTML = hasComment ? commentDeleteSvg : commentAddSvg; positionBlockToolbar(); blockToolbar.classList.add('visible'); } @@ -2832,6 +2849,21 @@ export function createBlocklyWorkspace() { Blockly.Events.setGroup(false); }); + commentBtn.addEventListener('pointerdown', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!toolbarBlock) return; + const block = toolbarBlock; + if (block.getCommentText() !== null) { + block.setCommentText(null); + } else { + block.setCommentText(''); + const icon = block.getIcons?.().find(i => typeof i.setBubbleVisible === 'function'); + icon?.setBubbleVisible(true); + } + hideBlockToolbar(); + }); + deleteBtn.addEventListener('pointerdown', (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/style/blockly.css b/style/blockly.css index 7e8a2503..68946fc8 100644 --- a/style/blockly.css +++ b/style/blockly.css @@ -1169,6 +1169,12 @@ body[data-theme='low-vision'] .blocklyField text.blocklyText.blocklyFieldText.bl stroke-width: 2px !important; } +@media (pointer: coarse) { + .blocklyBubble.blocklyTextInputBubble .blocklyEmboss rect.blocklyDraggable { + stroke-width: 16px !important; + } +} + /* Keep block comment editor constrained to its bubble */ .blocklyCommentForeignObject > body.blocklyMinimalBody, .blocklyCommentForeignObject textarea.blocklyCommentText { From 3660e6e5bd259770f040963e72ab0adf7cb9c500 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:14:45 +0100 Subject: [PATCH 07/12] Respect the theme --- style.css | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/style.css b/style.css index 298628b0..56c35e1e 100644 --- a/style.css +++ b/style.css @@ -108,6 +108,7 @@ --color-dropdown-bg: #333333; --color-dropdown-hover: #555555; + --color-menu: var(--color-dropdown-bg); --color-menu-hover: var(--color-dropdown-hover); --color-menu-item-hover: #444444; @@ -172,6 +173,7 @@ --color-dropdown-bg: #333333; --color-dropdown-hover: #555555; + --color-menu: var(--color-dropdown-bg); --color-menu-hover: var(--color-dropdown-hover); --color-menu-item-hover: #444444; @@ -220,8 +222,8 @@ --color-text-loading: #511d91; /* Border Colors */ - --color-border: #ddd; - --color-border-light: #eee; + --color-border: #ffffff; + --color-border-light: #cccccc; /* Loading Screen Colors */ --color-loading-bg: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); @@ -233,9 +235,9 @@ --color-focus-ring: #511d91; /* Menu Colors */ - --color-menu-hover: #f0f0f0; + --color-menu: var(--color-bg); + --color-menu-hover: #333333; --color-menu-item: #ffffff; - --color-menu: #f9f9f9; --color-button-bg: #f0f0f0; /* Shadow Colors */ @@ -267,6 +269,7 @@ --color-button-primary: #f0f0f0; --color-button-scroll: rgba(224, 224, 224, 0.5); --color-text-on-primary: #f0f0f0; + --color-menu: var(--color-bg-alt); --color-menu-hover: #2a2a2a; --color-focus-ring: #e0e0e0; --color-outline-focus: #e0e0e0; @@ -1979,6 +1982,15 @@ svg.blocklyTrashcanFlyout { color: var(--color-text-on-primary, #fff); } +[data-theme='dark'] .fc-confirm-btn--ok, +[data-theme='dark-contrast'] .fc-confirm-btn--ok, +[data-theme='low-vision'] .fc-confirm-btn--ok, +[data-theme='contrast'] .fc-confirm-btn--ok, +[data-theme='contrast'] .fc-confirm-btn--cancel, +[data-theme='contrast'] .fc-block-toolbar-btn { + color: #1a1a1a; +} + /* ---- Tablet floating block toolbar ---- */ .fc-block-toolbar { position: fixed; @@ -2034,7 +2046,7 @@ svg.blocklyTrashcanFlyout { height: 44px; border: none; border-radius: 7px; - background: transparent; + background: var(--color-button-bg, #f0f0f0); color: var(--color-text-primary, #000); cursor: pointer; -webkit-tap-highlight-color: transparent; @@ -2042,7 +2054,7 @@ svg.blocklyTrashcanFlyout { } .fc-block-toolbar-btn:active { - background: var(--color-menu-hover, #f0f0f0); + background: var(--color-button-bg-hover, #d0d0d0); } From 616f3c38622438f50f96a4749a2e8054c9140f16 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:57:51 +0100 Subject: [PATCH 08/12] Prettier --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 +- .github/workflows/run-mocha-tests.yml | 6 +- .github/workflows/static.yml | 6 +- AGENTS.md | 2 +- AI_POLICY.md | 14 +- API.md | 108 +- CLAUDE.md | 2 +- CONTRIBUTING.md | 2 + README.md | 34 +- TRADEMARK.md | 2 +- TRANSLATION.md | 10 +- accessibility/accessibility.js | 580 +++-- accessibility/keyboardui.js | 526 ++-- accessibility/uiA11y.js | 87 +- api/animate.js | 8 +- api/camera.js | 138 +- api/control.js | 20 +- api/csg.js | 1314 +++++----- api/effects.js | 89 +- api/events.js | 65 +- api/material.js | 579 ++--- api/models.js | 155 +- api/movement.js | 3 +- api/physics.js | 8 +- api/sensing.js | 123 +- api/sound.js | 496 ++-- api/ui.js | 398 ++- api/xr.js | 97 +- blocks/animate.js | 1002 ++++---- blocks/base.js | 28 +- blocks/blockIcons.js | 324 ++- blocks/blocks.js | 828 +++---- blocks/camera.js | 180 +- blocks/colour.js | 114 +- blocks/combine.js | 138 +- blocks/condition.js | 93 +- blocks/connect.js | 304 ++- blocks/control.js | 311 ++- blocks/effects.js | 110 +- blocks/events.js | 512 ++-- blocks/materials.js | 717 +++--- blocks/models.js | 370 ++- blocks/physics.js | 148 +- blocks/scene.js | 161 +- blocks/sensing.js | 617 +++-- blocks/shapes.js | 775 +++--- blocks/sound.js | 1061 ++++---- blocks/text.js | 428 ++-- blocks/transform.js | 555 ++--- blocks/xr.js | 175 +- config.js | 712 +++--- cubeart.html | 5 +- dev-docs/API_QUALITY_TOOLS.md | 10 +- dev-docs/API_RECONCILIATION_PLAN.md | 24 +- docs/screen-reader.md | 32 +- embed-example.html | 46 +- eslint.config.mjs | 44 +- generators/generators-animate.js | 393 ++- generators/generators-condition.js | 32 +- generators/generators-control.js | 158 +- generators/generators-data.js | 175 +- generators/generators-deprecated.js | 329 +-- generators/generators-events.js | 94 +- generators/generators-functions.js | 74 +- generators/generators-material.js | 112 +- generators/generators-math.js | 37 +- generators/generators-scene.js | 482 ++-- generators/generators-sensing.js | 94 +- generators/generators-sound.js | 260 +- generators/generators-text.js | 298 +-- generators/generators-transform.js | 446 ++-- generators/generators-utilities.js | 161 +- generators/generators.js | 62 +- generators/mesh-state.js | 7 +- index.html | 382 +-- input/bindings.js | 48 +- input/gamepadSource.js | 16 +- input/inputManager.js | 25 +- input/joystickSource.js | 26 +- input/keyboardSource.js | 22 +- input/normaliseKey.js | 2 +- input/onScreenSource.js | 42 +- input/xrSource.js | 106 +- locale/de.js | 2153 ++++++++-------- locale/es.js | 2158 ++++++++-------- locale/fr.js | 1991 ++++++++------- locale/it.js | 2065 ++++++++-------- locale/pl.js | 2160 ++++++++--------- locale/pt.js | 2130 ++++++++-------- locale/sv.js | 2141 ++++++++-------- main/blockhandling.js | 114 +- main/blocklyinit.js | 43 +- main/context.js | 75 +- main/customCommentIcon.js | 23 +- main/debug.js | 76 +- main/execution.js | 20 +- main/export.js | 239 +- main/files.js | 496 ++-- main/input.js | 3 +- main/keyboardDispatcher.js | 20 +- main/loading.js | 16 +- main/menu.js | 162 +- main/themes.js | 481 ++-- main/translation.js | 180 +- playwright.config.js | 22 +- scripts/api-coverage-report.mjs | 146 +- scripts/check-link-security.mjs | 46 +- scripts/csp-smoke.mjs | 88 +- scripts/run-api-tests.mjs | 637 +++-- scripts/run-input-tests.mjs | 18 +- scripts/test-coverage-matrix.mjs | 111 +- scripts/utils/extract-api-methods.mjs | 65 +- scripts/utils/parse-api-md.mjs | 28 +- scripts/utils/parse-jsdoc.mjs | 50 +- scripts/utils/test-analyzer.mjs | 42 +- style.css | 7 +- tests/accessibility/getPlayerMesh.test.js | 50 +- tests/accessibility/index.test.js | 2 +- tests/animate.test.js | 376 ++- tests/babylon.test.js | 18 +- tests/blocks.test.js | 422 ++-- tests/boxes.test.js | 67 +- tests/buttoncontrols.test.js | 205 +- tests/camera.test.js | 60 +- tests/characterAnimations.test.js | 96 +- tests/concurrency.test.js | 151 +- tests/control.test.js | 22 +- tests/effects.test.js | 147 +- tests/events.test.js | 251 +- tests/getProperty.test.js | 175 +- tests/glide.test.js | 38 +- tests/glideToObject.test.js | 55 +- tests/input/bindings.test.js | 91 +- tests/input/consumers.test.js | 142 +- tests/input/gamepadSource.test.js | 172 +- tests/input/index.test.js | 16 +- tests/input/inputManager.test.js | 238 +- tests/input/joystickSource.test.js | 194 +- tests/input/keyboardSource.test.js | 226 +- tests/input/onScreenSource.test.js | 182 +- tests/input/xrSource.test.js | 210 +- tests/materials.test.js | 301 ++- tests/math.test.js | 26 +- tests/mesh-hierarchy.test.js | 111 +- tests/movement.test.js | 124 +- tests/notifications.test.js | 101 +- tests/objects.test.js | 205 +- tests/onscreencontrols.test.js | 233 +- tests/physics.test.js | 211 +- tests/playwright/aria-input-labels.spec.js | 91 +- tests/playwright/blocks.spec.js | 174 +- tests/playwright/example-loading.spec.js | 59 +- tests/playwright/flock.spec.js | 20 +- tests/printtext.test.js | 53 +- tests/scene.test.js | 236 +- tests/sensing.test.js | 74 +- tests/shapes.test.js | 133 +- tests/sound-integration.test.js | 132 +- tests/sound-verification.test.js | 115 +- tests/sound.test.js | 128 +- tests/sound2.test.js | 26 +- tests/tests.html | 550 +++-- tests/transform.rotate.test.js | 113 +- tests/transform.scale.test.js | 209 +- tests/transform.translate.test.js | 138 +- tests/ui/interactIndicator.test.js | 158 +- tests/uitextbutton.test.js | 291 ++- tests/utils/audioTestUtils.js | 32 +- tests/xr-export.test.js | 46 +- tests/xr.test.js | 20 +- toolbox.js | 2563 ++++++++++---------- tutorial.js | 99 +- ui/addmenu.js | 322 ++- ui/addmeshes.js | 344 +-- ui/axis-keyboard.js | 102 +- ui/blocklyshadowutil.js | 33 +- ui/blocklyutil.js | 153 +- ui/blockmesh.js | 987 +++----- ui/canvas-utils.js | 118 +- ui/colourpicker.css | 78 +- ui/colourpicker.js | 941 ++++--- ui/designview.js | 6 +- ui/gizmos.js | 80 +- ui/meshhelpers.js | 9 +- ui/notifications.js | 71 +- vite.config.js | 256 +- 187 files changed, 23477 insertions(+), 26994 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 55fa7764..d153ba57 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,9 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: "" +title: '' labels: bug -assignees: "" +assignees: '' --- **Describe the bug** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 8eec841f..a080037a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,9 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: "" +title: '' labels: enhancement -assignees: "" +assignees: '' --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/run-mocha-tests.yml b/.github/workflows/run-mocha-tests.yml index f63ff2c6..7b3d7d3b 100644 --- a/.github/workflows/run-mocha-tests.yml +++ b/.github/workflows/run-mocha-tests.yml @@ -5,9 +5,9 @@ name: Run mocha tests on: push: - branches: ["testable"] + branches: ['testable'] pull_request: - branches: ["testable"] + branches: ['testable'] jobs: build: @@ -28,7 +28,7 @@ jobs: uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} - cache: "npm" + cache: 'npm' - run: npm ci - name: Get installed Playwright version id: playwright-version diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 27bcd98e..5c553102 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -3,7 +3,7 @@ name: Vite Github Pages Deploy on: # Runs on pushes targeting the default branch push: - branches: ["master", "main"] + branches: ['master', 'main'] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -14,11 +14,11 @@ permissions: id-token: write concurrency: - group: "pages" + group: 'pages' cancel-in-progress: false env: - VITE_BASE_URL: "/${{ github.event.repository.name }}/" + VITE_BASE_URL: '/${{ github.event.repository.name }}/' jobs: # Build job diff --git a/AGENTS.md b/AGENTS.md index 348289d7..d4dfef15 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ - Review code for unnecessary complexity ## Style + - Match the existing style - Comments should be infrequent - Comments should be genuinely noteworthy @@ -27,4 +28,3 @@ - If user verification is needed, describe the test to be carried out and request confirmation - Don't introduce security issues - Don't introduce changes that would break offline PWA behaviour - diff --git a/AI_POLICY.md b/AI_POLICY.md index 88cdeb7c..d21095d5 100644 --- a/AI_POLICY.md +++ b/AI_POLICY.md @@ -4,15 +4,15 @@ AI is a tool to support human creativity and responsibility, not replace it. ## Our overall goals are: -- Open source principles -- Efficiency, particularly when funded with public money and adding accessibility features -- Quality and maintainability of code -- Consider workload on maintainers and contributors and avoiding barriers to contribution -- Humans are responsible for code and other assets, AI is a tool +- Open source principles +- Efficiency, particularly when funded with public money and adding accessibility features +- Quality and maintainability of code +- Consider workload on maintainers and contributors and avoiding barriers to contribution +- Humans are responsible for code and other assets, AI is a tool ## Contribution expectations -The world has changed. We believe that open source projects need to be able to benefit from generative AI tools that have largely been trained on open source content. We need to do this in a way that is aligned with the goals of the project. +The world has changed. We believe that open source projects need to be able to benefit from generative AI tools that have largely been trained on open source content. We need to do this in a way that is aligned with the goals of the project. At this point we are not able to accept large contributions from fully automated contributors or contributors who are not known to the project. @@ -54,4 +54,4 @@ We recognise that some potential contributors may wish to write code completely This policy will be reviewed regularly as the generative AI landscape changes. -This policy is influenced by [NLnet Generative AI Policy](https://nlnet.nl/foundation/policies/generativeAI/) (we’re grateful that they listened to feedback as the open source community navigates this unprecedented change in how we develop software), the measured policy from [Apache Generative Tooling Guidelines](https://www.apache.org/legal/generative-tooling.html), and the ideas around community and human maintainers from [Ghostty AI Policy](https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md). \ No newline at end of file +This policy is influenced by [NLnet Generative AI Policy](https://nlnet.nl/foundation/policies/generativeAI/) (we’re grateful that they listened to feedback as the open source community navigates this unprecedented change in how we develop software), the measured policy from [Apache Generative Tooling Guidelines](https://www.apache.org/legal/generative-tooling.html), and the ideas around community and human maintainers from [Ghostty AI Policy](https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md). diff --git a/API.md b/API.md index 4aa52ace..62f4bd46 100644 --- a/API.md +++ b/API.md @@ -43,8 +43,8 @@ Plays an animation on a specified mesh. **Example:** ```javascript -await playAnimation("character1", { - animationName: "walk", +await playAnimation('character1', { + animationName: 'walk', loop: true, }); ``` @@ -64,8 +64,8 @@ Switches to a different animation on a mesh. **Example:** ```javascript -await switchAnimation("character1", { - animationName: "run", +await switchAnimation('character1', { + animationName: 'run', }); ``` @@ -86,12 +86,12 @@ Animates a property using keyframes. **Example:** ```javascript -await animateKeyFrames("box1", { +await animateKeyFrames('box1', { keyframes: [ - { duration: 0, value: "#FF0000" }, - { duration: 2, value: "#00FF00" }, + { duration: 0, value: '#FF0000' }, + { duration: 2, value: '#00FF00' }, ], - property: "color", + property: 'color', loop: true, }); ``` @@ -113,12 +113,12 @@ Smoothly moves a mesh to a new position. **Example:** ```javascript -await glideTo("player", { +await glideTo('player', { x: 5, y: 0, z: 3, duration: 2, - easing: "SineEase", + easing: 'SineEase', }); ``` @@ -139,7 +139,7 @@ Rotates a mesh with animation. **Example:** ```javascript -await rotateAnim("box1", { +await rotateAnim('box1', { x: 90, y: 180, z: 0, @@ -199,14 +199,14 @@ Creates a character model in the scene. ```javascript const player = createCharacter({ - modelName: "Character2.glb", - modelId: "player_unique_id", + modelName: 'Character2.glb', + modelId: 'player_unique_id', scale: 1, position: { x: 0, y: 0, z: 0 }, colors: { - hair: "#ffcc00", - skin: "#f0d5b1", - eyes: "#33cc00", + hair: '#ffcc00', + skin: '#f0d5b1', + eyes: '#33cc00', }, }); ``` @@ -235,7 +235,7 @@ Creates a box geometry. **Example:** ```javascript -const box1 = createBox("myBox", "#ff0000", 2, 2, 2, [0, 1, 0]); +const box1 = createBox('myBox', '#ff0000', 2, 2, 2, [0, 1, 0]); ``` #### `createSphere(name, options)` @@ -268,7 +268,7 @@ Sets the transparency of a mesh. **Example:** ```javascript -await setAlpha("box1", 0.75); +await setAlpha('box1', 0.75); ``` #### `changeColor(meshName, color)` @@ -337,7 +337,7 @@ Applies physics properties to a mesh. **Example:** ```javascript -await setPhysics("player", "DYNAMIC"); +await setPhysics('player', 'DYNAMIC'); ``` #### `applyForce(meshName, force)` @@ -358,7 +358,7 @@ Creates a ground plane. **Example:** ```javascript -createGround("#ffffff", "ground"); +createGround('#ffffff', 'ground'); ``` #### `setSky(color)` @@ -372,7 +372,7 @@ Sets the skybox/environment color. **Example:** ```javascript -setSky("#ffffff"); +setSky('#ffffff'); ``` #### `lightIntensity(intensity)` @@ -395,8 +395,8 @@ Adds fog to the scene for atmospheric effects. ```javascript setFog({ - fogColorHex: "#ffffff", - fogMode: "LINEAR", + fogColorHex: '#ffffff', + fogMode: 'LINEAR', fogDensity: 0.1, fogStart: 50, fogEnd: 100, @@ -431,7 +431,7 @@ Attaches the camera to follow a specific mesh. **Example:** ```javascript -await attachCamera("player", 7); +await attachCamera('player', 7); ``` ### UI @@ -449,7 +449,7 @@ Displays text in the UI. **Example:** ```javascript -printText("🌈 Hello", 30, "#000080"); +printText('🌈 Hello', 30, '#000080'); ``` #### `buttonControls(type, enabled, color)` @@ -465,7 +465,7 @@ Creates button controls for user interaction. **Example:** ```javascript -buttonControls("ARROWS", true, "#cc33cc"); +buttonControls('ARROWS', true, '#cc33cc'); ``` #### `say(meshName, text, duration)` @@ -527,7 +527,7 @@ Checks if a specific key is currently pressed. **Example:** ```javascript -if (keyPressed("w")) { +if (keyPressed('w')) { // Move forward } ``` @@ -543,7 +543,7 @@ Checks if a movement or action input is active across keyboard, touch, or XR con **Example:** ```javascript -if (actionPressed("FORWARD")) { +if (actionPressed('FORWARD')) { // Move forward regardless of whether the player is using W, Z, touch, or XR input } ``` @@ -561,7 +561,7 @@ Run a callback when the chosen action is pressed or released across keyboard, to **Example:** ```javascript -whenActionEvent("BUTTON1", async () => { +whenActionEvent('BUTTON1', async () => { // Respond to the action button, regardless of whether it came from E, touch, or XR }); ``` @@ -589,11 +589,11 @@ Runs a function in an infinite loop. ```javascript forever(async () => { - if (keyPressed("w")) { - moveForward("player", 3); - await switchAnimation("player", { animationName: "Walk" }); + if (keyPressed('w')) { + moveForward('player', 3); + await switchAnimation('player', { animationName: 'Walk' }); } else { - await switchAnimation("player", { animationName: "Idle" }); + await switchAnimation('player', { animationName: 'Idle' }); } }); ``` @@ -617,19 +617,19 @@ For a complete working example, see [example.html](example.html) in the reposito ### Basic Scene Setup ```javascript -setSky("#ffffff"); -createGround("#ffffff", "ground"); -printText("🌈 Hello", 30, "#000080"); -buttonControls("ARROWS", true, "#cc33cc"); +setSky('#ffffff'); +createGround('#ffffff', 'ground'); +printText('🌈 Hello', 30, '#000080'); +buttonControls('ARROWS', true, '#cc33cc'); const player = createCharacter({ - modelName: "Character2.glb", - modelId: "player_unique_id", + modelName: 'Character2.glb', + modelId: 'player_unique_id', scale: 1, position: { x: 0, y: 0, z: 0 }, }); -await setPhysics(player, "DYNAMIC"); +await setPhysics(player, 'DYNAMIC'); await attachCamera(player, 7); ``` @@ -637,14 +637,14 @@ await attachCamera(player, 7); ```javascript forever(async () => { - if (keyPressed("w")) { - moveForward("player", 3); - await switchAnimation("player", { animationName: "Walk" }); - } else if (keyPressed("s")) { - moveForward("player", -3); - await switchAnimation("player", { animationName: "Walk" }); + if (keyPressed('w')) { + moveForward('player', 3); + await switchAnimation('player', { animationName: 'Walk' }); + } else if (keyPressed('s')) { + moveForward('player', -3); + await switchAnimation('player', { animationName: 'Walk' }); } else { - await switchAnimation("player", { animationName: "Idle" }); + await switchAnimation('player', { animationName: 'Idle' }); } }); ``` @@ -653,23 +653,23 @@ forever(async () => { ```javascript // Keyframe animation -await animateKeyFrames("box1", { +await animateKeyFrames('box1', { keyframes: [ - { duration: 0, value: "#FF0000" }, - { duration: 2, value: "#00FF00" }, - { duration: 4, value: "#0000FF" }, + { duration: 0, value: '#FF0000' }, + { duration: 2, value: '#00FF00' }, + { duration: 4, value: '#0000FF' }, ], - property: "color", + property: 'color', loop: true, }); // Smooth movement -await glideTo("player", { +await glideTo('player', { x: 10, y: 0, z: 5, duration: 3, - easing: "SineEase", + easing: 'SineEase', }); ``` diff --git a/CLAUDE.md b/CLAUDE.md index d250fcfd..bcb6d35e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,3 @@ ## claude.md -See [AGENTS.md](AGENTS.md) \ No newline at end of file +See [AGENTS.md](AGENTS.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5fa2d899..607bfa3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,9 @@ Before starting, we suggest you [get in touch](https://flipcomputing.com/contact 7. **Create a Pull Request** on GitHub ### Deploying your fork to GitHub Pages + 1. **Push your fork to GitHub**: + ```bash git push origin main ``` diff --git a/README.md b/README.md index 60f38bd1..456834d7 100644 --- a/README.md +++ b/README.md @@ -8,34 +8,35 @@ It enables educators, students, and families to build interactive 3D experiences ## Key features -- No login required -- Runs entirely in the browser (including Chromebooks, tablets, and low-spec devices) -- Designed for schools, clubs, and home learning -- Supported by curriculum resources and ready-to-use lesson materials +- No login required +- Runs entirely in the browser (including Chromebooks, tablets, and low-spec devices) +- Designed for schools, clubs, and home learning +- Supported by curriculum resources and ready-to-use lesson materials ### What can you do with Flock XR? -- Build and explore interactive 3D environments -- Create worlds using visual gizmos and block-based coding -- Teach coding, game design, and digital storytelling through hands-on projects +- Build and explore interactive 3D environments +- Create worlds using visual gizmos and block-based coding +- Teach coding, game design, and digital storytelling through hands-on projects - Run engaging learning activities without installing software -- Use games engine concepts such as aniamtions, particle effects and physics. +- Use games engine concepts such as aniamtions, particle effects and physics. -Flock XR has been designed to start with younger children aged 7+, often using tablets. Then take them through upper primary, middle school and high school right through to professional 3D tools, such as Babylon JS, UEFN, Unity and Godot. +Flock XR has been designed to start with younger children aged 7+, often using tablets. Then take them through upper primary, middle school and high school right through to professional 3D tools, such as Babylon JS, UEFN, Unity and Godot. ### Why 3D and XR? -3D is engaging and age appropriate for young people and can lead to careers across a range of industries. Technology that originated in games is now used in many sectors. XR and spatial computing are changing how humans interact with technology, bridging the physical and digital worlds. + +3D is engaging and age appropriate for young people and can lead to careers across a range of industries. Technology that originated in games is now used in many sectors. XR and spatial computing are changing how humans interact with technology, bridging the physical and digital worlds. - Young people love 3D games and social platforms such as Minecraft, Roblox, Fortnite and more. Flock XR brings that knowledge into the classroom. - The real world is 3D. It turns out that some things are just easier when you don't have the complexity of having to map to 3D. -- 3D is the basis for eXtended Reality and spatial computing, the metaverse and immersive tech which are growing dramatically across industries. +- 3D is the basis for eXtended Reality and spatial computing, the metaverse and immersive tech which are growing dramatically across industries. ### Who is it for? - Young people age 7-14+ (primary through secondary education, K-12) -- Teachers and educators worldwide -- Clubs, coding groups, and informal learning communities -- Parents and home educators supporting creative learning +- Teachers and educators worldwide +- Clubs, coding groups, and informal learning communities +- Parents and home educators supporting creative learning 👉 Try it now: [app.flockxr.com](https://app.flockxr.com/) A project from: [Flip Computing](https://flipcomputing.com/). @@ -52,9 +53,9 @@ Please see our [documentation hub](https://hub.flockxr.com) and [free resources ## Development info -Full details of Flock XR versions including the latest Development version can be found at [flockxr.com/versions/](https://flockxr.com/versions/) +Full details of Flock XR versions including the latest Development version can be found at [flockxr.com/versions/](https://flockxr.com/versions/) -You will also find full dev setup for [contributing to Flock XR](https://github.com/flipcomputing/flockupdate/blob/main/CONTRIBUTING.md) in our guide. +You will also find full dev setup for [contributing to Flock XR](https://github.com/flipcomputing/flockupdate/blob/main/CONTRIBUTING.md) in our guide. Flock XR is licensed under the MIT License. By contributing, you agree that your contributions will be licensed under the same license. @@ -72,4 +73,5 @@ Flock XR is based on Blockly, the Babylon JS library and the Havok physics engin - Playwright - End-to-end testing framework **📚 Documentation:** + - [Getting Started](docs/GETTING_STARTED.md) - Quick start for improving API quality diff --git a/TRADEMARK.md b/TRADEMARK.md index 166257dd..9c0dfd6e 100644 --- a/TRADEMARK.md +++ b/TRADEMARK.md @@ -1 +1 @@ -Flock XR® and the Flock XR purple bird are trademarks registered to Flip Computing Ltd. The marks may not be used to endorse or promote products derived from this software without specific prior written permission. \ No newline at end of file +Flock XR® and the Flock XR purple bird are trademarks registered to Flip Computing Ltd. The marks may not be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/TRANSLATION.md b/TRANSLATION.md index 8bf0f4d2..14789f33 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -21,8 +21,8 @@ Follow these steps to wire a new locale into the app: 2. **Import the locale in the translation module** - In `main/translation.js`, add the Blockly pack import (if it exists) and the custom locale import: ```javascript - import * as xx from "blockly/msg/xx"; - import xxLocale from "../locale/xx.js"; + import * as xx from 'blockly/msg/xx'; + import xxLocale from '../locale/xx.js'; ``` - Add the locale to the `translations` map (e.g., `xx: xxLocale,`). - Extend `applySavedLanguageTranslations` and `setLanguage` with a branch that applies the Blockly pack for the new language (mirroring the existing languages). @@ -59,10 +59,10 @@ When you introduce new UI text in the codebase, wire it to the translation syste - **JavaScript** (manual assignment): ```javascript - import { translate } from "../main/translation.js"; + import { translate } from '../main/translation.js'; - const snackbar = document.querySelector("#snackbar"); - snackbar.textContent = translate("snackbar_saved_ui"); + const snackbar = document.querySelector('#snackbar'); + snackbar.textContent = translate('snackbar_saved_ui'); ``` Add `snackbar_saved_ui: "Saved",` to `locale/en.js` and provide translations for the same key. diff --git a/accessibility/accessibility.js b/accessibility/accessibility.js index 06659153..c4667ace 100644 --- a/accessibility/accessibility.js +++ b/accessibility/accessibility.js @@ -1,6 +1,6 @@ // flock/accessibility/accessibility.js -import { translate } from "../main/translation.js"; -import { onInteractObservable } from "../ui/interactIndicator.js"; +import { translate } from '../main/translation.js'; +import { onInteractObservable } from '../ui/interactIndicator.js'; let speechMuted = false; let currentScene = null; @@ -10,17 +10,16 @@ let keyListenerAttached = false; let pointerObserverRef = null; let pointerObserverScene = null; - // Message sequencing to stop stale/laggy announcements let announceSeq = 0; -let lastAnnouncedText = ""; +let lastAnnouncedText = ''; let lastAnnouncedAt = 0; // Separate channel for say-block announcements so they don't cancel printText let sayAnnounceSeq = 0; // Helps avoid repeating the same click announcement too many times in a row -let lastInteractionKey = ""; +let lastInteractionKey = ''; let lastInteractionTime = 0; // Two persistent assertive regions that alternate so each announce writes to whichever @@ -31,7 +30,7 @@ let _interactRegionIdx = 0; // Track whether initial intro has been announced for the current scene let lastIntroScene = null; let introInProgress = false; -let queuedIntroSayText = ""; +let queuedIntroSayText = ''; let suppressPointerUntil = 0; let suppressRuntimeTextUntil = 0; let objectSayTextCache = new Map(); @@ -42,18 +41,18 @@ let hasSpokenInitialPageIntro = false; let worldInstructionTexts = []; function createA11yRoot() { - let root = document.getElementById("flock-a11y-root"); + let root = document.getElementById('flock-a11y-root'); if (!root) { - root = document.createElement("div"); - root.id = "flock-a11y-root"; + root = document.createElement('div'); + root.id = 'flock-a11y-root'; // Visually hidden but readable by screen readers - root.style.position = "absolute"; - root.style.left = "-9999px"; - root.style.top = "0"; - root.style.width = "1px"; - root.style.height = "1px"; - root.style.overflow = "hidden"; + root.style.position = 'absolute'; + root.style.left = '-9999px'; + root.style.top = '0'; + root.style.width = '1px'; + root.style.height = '1px'; + root.style.overflow = 'hidden'; document.body.appendChild(root); } @@ -61,17 +60,17 @@ function createA11yRoot() { } export function createLiveRegion() { - let region = document.getElementById("flock-live-region"); + let region = document.getElementById('flock-live-region'); if (!region) { const root = createA11yRoot(); - region = document.createElement("div"); - region.id = "flock-live-region"; + region = document.createElement('div'); + region.id = 'flock-live-region'; // Status (not log) avoids a backlog of old messages - region.setAttribute("role", "status"); - region.setAttribute("aria-live", "polite"); - region.setAttribute("aria-atomic", "true"); + region.setAttribute('role', 'status'); + region.setAttribute('aria-live', 'polite'); + region.setAttribute('aria-atomic', 'true'); root.appendChild(region); } @@ -79,14 +78,14 @@ export function createLiveRegion() { } function createSayLiveRegion() { - let region = document.getElementById("flock-say-region"); + let region = document.getElementById('flock-say-region'); if (!region) { const root = createA11yRoot(); - region = document.createElement("div"); - region.id = "flock-say-region"; - region.setAttribute("role", "status"); - region.setAttribute("aria-live", "polite"); - region.setAttribute("aria-atomic", "true"); + region = document.createElement('div'); + region.id = 'flock-say-region'; + region.setAttribute('role', 'status'); + region.setAttribute('aria-live', 'polite'); + region.setAttribute('aria-atomic', 'true'); root.appendChild(region); } return region; @@ -101,7 +100,7 @@ export function announceObjectSay(text, options = {}) { const region = createSayLiveRegion(); const mySeq = ++sayAnnounceSeq; - region.textContent = ""; + region.textContent = ''; setTimeout(() => { if (mySeq !== sayAnnounceSeq) return; region.textContent = spoken; @@ -112,7 +111,7 @@ export function announce(message, options = {}) { const { force = false, noDedup = false } = options; if (speechMuted && !force) return; - const text = String(message ?? "").trim(); + const text = String(message ?? '').trim(); if (!text) return; const now = Date.now(); @@ -129,10 +128,11 @@ export function announce(message, options = {}) { if (!_interactRegions) { const root = createA11yRoot(); _interactRegions = [0, 1].map(() => { - const el = document.createElement("div"); - el.setAttribute("aria-live", "assertive"); - el.setAttribute("aria-atomic", "true"); - el.style.cssText = "position:absolute;left:-9999px;top:0;width:1px;height:1px;overflow:hidden"; + const el = document.createElement('div'); + el.setAttribute('aria-live', 'assertive'); + el.setAttribute('aria-atomic', 'true'); + el.style.cssText = + 'position:absolute;left:-9999px;top:0;width:1px;height:1px;overflow:hidden'; root.appendChild(el); return el; }); @@ -140,15 +140,17 @@ export function announce(message, options = {}) { _interactRegionIdx = 1 - _interactRegionIdx; const next = _interactRegions[_interactRegionIdx]; const prev = _interactRegions[1 - _interactRegionIdx]; - prev.textContent = ""; - setTimeout(() => { next.textContent = text; }, 0); + prev.textContent = ''; + setTimeout(() => { + next.textContent = text; + }, 0); return; } const region = createLiveRegion(); const mySeq = ++announceSeq; - region.textContent = ""; + region.textContent = ''; setTimeout(() => { if (mySeq !== announceSeq) return; @@ -157,41 +159,36 @@ export function announce(message, options = {}) { } function normaliseName(name) { - return String(name || "object") - .replace(/[_-]/g, " ") - .replace(/([a-z])([A-Z])/g, "$1 $2") - .replace(/\s+/g, " ") + return String(name || 'object') + .replace(/[_-]/g, ' ') + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/\s+/g, ' ') .trim(); } function looksLikeInternalMeshName(name) { - const n = String(name || "").toLowerCase(); + const n = String(name || '').toLowerCase(); return ( !n || - n === "__root__" || - n === "textplane" || - n.startsWith("__flock_") || - n.includes("camera") || - n.includes("light") || - n.includes("highlighter") || - n.includes("gizmo") || - n.includes("bounding") || - n.includes("debug") || - n.includes("hitbox") || - n.includes("collider") || - n.includes("armature") || - n.includes("mixamo") + n === '__root__' || + n === 'textplane' || + n.startsWith('__flock_') || + n.includes('camera') || + n.includes('light') || + n.includes('highlighter') || + n.includes('gizmo') || + n.includes('bounding') || + n.includes('debug') || + n.includes('hitbox') || + n.includes('collider') || + n.includes('armature') || + n.includes('mixamo') ); } function looksLikeTextName(name) { - const n = String(name || "").toLowerCase(); - return ( - n.includes("text") || - n.includes("label") || - n.includes("caption") || - n.includes("title") - ); + const n = String(name || '').toLowerCase(); + return n.includes('text') || n.includes('label') || n.includes('caption') || n.includes('title'); } function getEntityRoot(mesh) { @@ -199,7 +196,7 @@ function getEntityRoot(mesh) { let lastValid = mesh; while (node) { - const n = node.name || ""; + const n = node.name || ''; if (!looksLikeInternalMeshName(n)) { lastValid = node; } @@ -226,23 +223,23 @@ function getMetadataText(mesh) { md.description; if (text && String(text).trim()) return resolveSpokenText(text); } - return ""; + return ''; } function cleanSpokenAnnouncement(text) { - let s = String(text || ""); + let s = String(text || ''); // Remove variation selectors - s = s.replace(/[\uFE0E\uFE0F]/g, ""); + s = s.replace(/[\uFE0E\uFE0F]/g, ''); // Remove Fitzpatrick skin tone modifiers - s = s.replace(/[\u{1F3FB}-\u{1F3FF}]/gu, ""); + s = s.replace(/[\u{1F3FB}-\u{1F3FF}]/gu, ''); // Remove most emoji / pictographs - s = s.replace(/[\p{Extended_Pictographic}]/gu, ""); + s = s.replace(/[\p{Extended_Pictographic}]/gu, ''); // Collapse extra spacing left behind - s = s.replace(/\s+/g, " ").trim(); + s = s.replace(/\s+/g, ' ').trim(); return s; } @@ -250,40 +247,35 @@ function cleanSpokenAnnouncement(text) { function resolveSpokenText(value) { // Unwrap common object shapes let rawValue = value; - if (rawValue && typeof rawValue === "object") { + if (rawValue && typeof rawValue === 'object') { rawValue = - rawValue.key ?? - rawValue.id ?? - rawValue.name ?? - rawValue.label ?? - rawValue.value ?? - ""; + rawValue.key ?? rawValue.id ?? rawValue.name ?? rawValue.label ?? rawValue.value ?? ''; } - const raw = String(rawValue ?? "").trim(); - if (!raw) return ""; + const raw = String(rawValue ?? '').trim(); + if (!raw) return ''; const original = raw; const lower = raw.toLowerCase(); const underscored = lower - .replace(/[\s-]+/g, "_") - .replace(/_+/g, "_") - .replace(/^_+|_+$/g, ""); + .replace(/[\s-]+/g, '_') + .replace(/_+/g, '_') + .replace(/^_+|_+$/g, ''); - const stripped = underscored.replace(/^(key|id|name|label)[:=]\s*/i, ""); + const stripped = underscored.replace(/^(key|id|name|label)[:=]\s*/i, ''); const candidates = [original, lower, underscored, stripped].filter( (v, i, arr) => v && arr.indexOf(v) === i ); const looksKeyLike = (s) => - s.startsWith("model_display_") || - s.endsWith("_ui") || - s.endsWith("_aria") || - s.endsWith("_tooltip") || - s.includes("_option") || - s.includes("_message") || - s.includes("_label") || + s.startsWith('model_display_') || + s.endsWith('_ui') || + s.endsWith('_aria') || + s.endsWith('_tooltip') || + s.includes('_option') || + s.includes('_message') || + s.includes('_label') || /^[a-z0-9]+(_[a-z0-9]+){1,}$/.test(s); for (const c of candidates) { @@ -297,8 +289,8 @@ function resolveSpokenText(value) { // If it still looks like a key, humanise it if (looksKeyLike(stripped)) { const human = stripped - .replace(/^model_display_/, "") - .replace(/_/g, " ") + .replace(/^model_display_/, '') + .replace(/_/g, ' ') .trim(); if (human) return human; } @@ -318,9 +310,9 @@ export function recordWorldInstructionText(text) { // Skip generic startup chatter if ( - lower.includes("flock xr loaded successfully") || - lower.includes("flock world successfully loaded") || - lower.includes("loading flock xr") + lower.includes('flock xr loaded successfully') || + lower.includes('flock world successfully loaded') || + lower.includes('loading flock xr') ) { return; } @@ -345,43 +337,41 @@ export function getObjectLabel(mesh) { const root = getEntityRoot(mesh); const rootMd = root?.metadata || {}; - const rootExplicit = - rootMd.a11yLabel || rootMd.label || rootMd.displayName || rootMd.name; + const rootExplicit = rootMd.a11yLabel || rootMd.label || rootMd.displayName || rootMd.name; if (rootExplicit) return resolveSpokenText(rootExplicit); - const rootName = normaliseName(root?.name || ""); + const rootName = normaliseName(root?.name || ''); if (rootName && !/^mesh\b/i.test(rootName) && !/^node\b/i.test(rootName)) { return rootName; } - return normaliseName(mesh?.name || "object"); + return normaliseName(mesh?.name || 'object'); } function getDistanceLabel(distance) { - if (distance < 1.5) return "very close"; - if (distance < 4) return "nearby"; - if (distance < 8) return "a short distance away"; - if (distance < 15) return "further away"; - return "far away"; + if (distance < 1.5) return 'very close'; + if (distance < 4) return 'nearby'; + if (distance < 8) return 'a short distance away'; + if (distance < 15) return 'further away'; + return 'far away'; } function getVerticalLabel(dy) { - if (dy > 1.5) return "above you"; - if (dy < -1.5) return "below you"; - return ""; + if (dy > 1.5) return 'above you'; + if (dy < -1.5) return 'below you'; + return ''; } function getHorizontalLabel(dot, cross) { - const frontBack = - dot > 0.45 ? "in front of you" : dot < -0.45 ? "behind you" : "beside you"; + const frontBack = dot > 0.45 ? 'in front of you' : dot < -0.45 ? 'behind you' : 'beside you'; - let leftRight = ""; + let leftRight = ''; if (Math.abs(cross) > 0.3) { - leftRight = cross > 0 ? "to your left" : "to your right"; + leftRight = cross > 0 ? 'to your left' : 'to your right'; } - if (frontBack === "beside you" && leftRight) return leftRight; + if (frontBack === 'beside you' && leftRight) return leftRight; if (leftRight) return `${frontBack}, ${leftRight}`; return frontBack; } @@ -397,7 +387,7 @@ function getCameraForward(scene) { return { x: dir.x / len, z: dir.z / len }; } } catch (error) { - console.warn("Suppressed non-critical error:", error); + console.warn('Suppressed non-critical error:', error); } try { @@ -410,40 +400,40 @@ function getCameraForward(scene) { return { x: x / len, z: z / len }; } } catch (error) { - console.warn("Suppressed non-critical error:", error); + console.warn('Suppressed non-critical error:', error); } return { x: 0, z: 1 }; } function isEnvironmentObject(label) { - const n = String(label || "").toLowerCase(); + const n = String(label || '').toLowerCase(); return ( - n.includes("sky") || - n.includes("ground") || - n.includes("floor") || - n.includes("terrain") || - n.includes("land") || - n.includes("grass") || - n.includes("road") || - n.includes("path") + n.includes('sky') || + n.includes('ground') || + n.includes('floor') || + n.includes('terrain') || + n.includes('land') || + n.includes('grass') || + n.includes('road') || + n.includes('path') ); } function isSkyLike(label) { - const n = String(label || "").toLowerCase(); - return n.includes("sky") || n.includes("cloud"); + const n = String(label || '').toLowerCase(); + return n.includes('sky') || n.includes('cloud'); } function isGroundLike(label) { - const n = String(label || "").toLowerCase(); + const n = String(label || '').toLowerCase(); return ( - n.includes("ground") || - n.includes("floor") || - n.includes("terrain") || - n.includes("grass") || - n.includes("road") || - n.includes("path") + n.includes('ground') || + n.includes('floor') || + n.includes('terrain') || + n.includes('grass') || + n.includes('road') || + n.includes('path') ); } @@ -469,11 +459,8 @@ function getInteractionHint(mesh) { } const interactive = candidates.some( - (m) => - m?.actionManager || m?.metadata?.interactive || m?.metadata?.clickable, + (m) => m?.actionManager || m?.metadata?.interactive || m?.metadata?.clickable ); - - } function closestBoundingBoxDistance(mesh, point) { @@ -484,7 +471,9 @@ function closestBoundingBoxDistance(mesh, point) { const cx = Math.max(bb.minimumWorld.x, Math.min(point.x, bb.maximumWorld.x)); const cy = Math.max(bb.minimumWorld.y, Math.min(point.y, bb.maximumWorld.y)); const cz = Math.max(bb.minimumWorld.z, Math.min(point.z, bb.maximumWorld.z)); - const dx = cx - point.x, dy = cy - point.y, dz = cz - point.z; + const dx = cx - point.x, + dy = cy - point.y, + dz = cz - point.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); } catch (_) { return null; @@ -496,9 +485,14 @@ function isInsideBoundingBox(mesh, point) { mesh.computeWorldMatrix?.(true); const bb = mesh.getBoundingInfo?.()?.boundingBox; if (!bb) return false; - return point.x > bb.minimumWorld.x && point.x < bb.maximumWorld.x && - point.y > bb.minimumWorld.y && point.y < bb.maximumWorld.y && - point.z > bb.minimumWorld.z && point.z < bb.maximumWorld.z; + return ( + point.x > bb.minimumWorld.x && + point.x < bb.maximumWorld.x && + point.y > bb.minimumWorld.y && + point.y < bb.maximumWorld.y && + point.z > bb.minimumWorld.z && + point.z < bb.maximumWorld.z + ); } catch (_) { return false; } @@ -509,8 +503,11 @@ function isUnderfootOf(mesh, playerPos) { mesh.computeWorldMatrix?.(true); const bb = mesh.getBoundingInfo?.()?.boundingBox; if (!bb || bb.maximumWorld.y === bb.minimumWorld.y) return false; - const withinXZ = playerPos.x > bb.minimumWorld.x && playerPos.x < bb.maximumWorld.x && - playerPos.z > bb.minimumWorld.z && playerPos.z < bb.maximumWorld.z; + const withinXZ = + playerPos.x > bb.minimumWorld.x && + playerPos.x < bb.maximumWorld.x && + playerPos.z > bb.minimumWorld.z && + playerPos.z < bb.maximumWorld.z; return withinXZ && Math.abs(playerPos.y - bb.maximumWorld.y) < 2.0; } catch (_) { return false; @@ -526,7 +523,7 @@ function getRepresentativePosition(root, fallbackMesh) { try { node.computeWorldMatrix?.(true); } catch (error) { - console.warn("Suppressed non-critical error:", error); + console.warn('Suppressed non-critical error:', error); } try { @@ -540,21 +537,16 @@ function getRepresentativePosition(root, fallbackMesh) { return center; } } catch (error) { - console.warn("Suppressed non-critical error:", error); + console.warn('Suppressed non-critical error:', error); } try { const p = node.getAbsolutePosition?.() ?? node.position; - if ( - p && - Number.isFinite(p.x) && - Number.isFinite(p.y) && - Number.isFinite(p.z) - ) { + if (p && Number.isFinite(p.x) && Number.isFinite(p.y) && Number.isFinite(p.z)) { return p; } } catch (error) { - console.warn("Suppressed non-critical error:", error); + console.warn('Suppressed non-critical error:', error); } } @@ -570,17 +562,17 @@ export function getPlayerMesh(scene) { // 1. Camera-attached mesh (highest priority) const locked = camera.lockedTarget; - if (locked && typeof locked.name === "string" && !locked.isDisposed?.()) { + if (locked && typeof locked.name === 'string' && !locked.isDisposed?.()) { return locked; } const parent = camera.parent; - if (parent && typeof parent.name === "string" && !parent.isDisposed?.()) { + if (parent && typeof parent.name === 'string' && !parent.isDisposed?.()) { return parent; } const following = camera.metadata?.following; - if (following && typeof following.name === "string" && !following.isDisposed?.()) { + if (following && typeof following.name === 'string' && !following.isDisposed?.()) { return following; } @@ -603,16 +595,16 @@ export function getPlayerMesh(scene) { let score = 0; - if (md.a11yAnchor === "player" || md.a11yRole === "player") score += 200; - if (md.a11yRole === "character" || md.role === "character") score += 150; + if (md.a11yAnchor === 'player' || md.a11yRole === 'player') score += 200; + if (md.a11yRole === 'character' || md.role === 'character') score += 150; if (md.character === true) score += 120; if (md.isPlayer === true) score += 200; if (md.player === true) score += 180; if (md.mainCharacter === true) score += 180; - if (label.includes("player")) score += 80; - if (label.includes("avatar")) score += 70; - if (label.includes("character")) score += 60; + if (label.includes('player')) score += 80; + if (label.includes('avatar')) score += 70; + if (label.includes('character')) score += 60; const p = getRepresentativePosition(root, mesh); if (p && cameraPos) { @@ -645,17 +637,17 @@ function getReferenceAnchor(scene) { // Use feet position (bottom of bounding box) so ground-level objects // measure correctly — using the centre inflates distance to flat surfaces. const pos = { x: bb.centerWorld.x, y: bb.minimumWorld.y, z: bb.centerWorld.z }; - return { kind: "character", mesh: playerMesh, position: pos }; + return { kind: 'character', mesh: playerMesh, position: pos }; } } catch (_) {} const pos = getRepresentativePosition(playerMesh, playerMesh); if (pos) { - return { kind: "character", mesh: playerMesh, position: pos }; + return { kind: 'character', mesh: playerMesh, position: pos }; } } return { - kind: "camera", + kind: 'camera', mesh: camera || null, position: cameraPos || { x: 0, y: 0, z: 0 }, }; @@ -735,9 +727,7 @@ function getSceneObjects(scene, options = {}) { const dxCam = p.x - cameraPos.x; const dyCam = p.y - cameraPos.y; const dzCam = p.z - cameraPos.z; - const distFromCamera = Math.sqrt( - dxCam * dxCam + dyCam * dyCam + dzCam * dzCam, - ); + const distFromCamera = Math.sqrt(dxCam * dxCam + dyCam * dyCam + dzCam * dzCam); if (distFromCamera < 0.2) continue; const lenXZ = Math.sqrt(dxCam * dxCam + dzCam * dzCam) || 1; @@ -771,7 +761,7 @@ function getSceneObjects(scene, options = {}) { mesh.actionManager || root?.metadata?.interactive || root?.metadata?.clickable || - interactionHint, + interactionHint ); const textLabels = collectNearbyTextForObject(scene, p, root); @@ -803,21 +793,18 @@ function getSceneObjects(scene, options = {}) { return Array.from(byEntityName.values()); } -function objectToSentence( - obj, - { includeActionHint = false, includeText = false } = {}, -) { - const where = [obj.horizontal, obj.vertical].filter(Boolean).join(" and "); - let sentence = `${obj.label} is ${where || "nearby"}, ${obj.distanceLabel}.`; +function objectToSentence(obj, { includeActionHint = false, includeText = false } = {}) { + const where = [obj.horizontal, obj.vertical].filter(Boolean).join(' and '); + let sentence = `${obj.label} is ${where || 'nearby'}, ${obj.distanceLabel}.`; if (includeText && obj.sayText) { sentence += ` ${obj.label} says: ${obj.sayText}.`; } else if (includeText && obj.textLabels?.length) { - sentence += ` Text: ${obj.textLabels.join(". ")}.`; + sentence += ` Text: ${obj.textLabels.join('. ')}.`; } if (includeActionHint && obj.interactive) { - const hint = obj.interactionHint || "You can interact with this."; + const hint = obj.interactionHint || 'You can interact with this.'; sentence += ` ${hint}`; } @@ -825,22 +812,22 @@ function objectToSentence( } function enrichEnvironmentLabel(obj) { - const raw = String(obj?.label || "").trim(); + const raw = String(obj?.label || '').trim(); const label = raw.toLowerCase(); // Hide vague or unhelpful labels - if (!label) return ""; + if (!label) return ''; if ( - label === "environment" || - label === "scene" || - label === "object" || - label === "mesh" || - label === "ground" || - label === "floor" || - label === "terrain" || - label === "sky" + label === 'environment' || + label === 'scene' || + label === 'object' || + label === 'mesh' || + label === 'ground' || + label === 'floor' || + label === 'terrain' || + label === 'sky' ) { - return ""; + return ''; } // Make some common labels sound nicer @@ -851,13 +838,13 @@ function enrichEnvironmentLabel(obj) { } function buildEnvironmentSummary(objects, anchor = null, scene = null) { - if (!objects?.length) return ""; + if (!objects?.length) return ''; - const normalise = (s) => String(s || "").trim(); + const normalise = (s) => String(s || '').trim(); const lower = (s) => normalise(s).toLowerCase(); // Exclude the main character label from the environment glimpse - const anchorLabel = anchor?.mesh ? lower(getObjectLabel(anchor.mesh)) : ""; + const anchorLabel = anchor?.mesh ? lower(getObjectLabel(anchor.mesh)) : ''; // Group objects by spoken label const counts = new Map(); @@ -871,12 +858,12 @@ function buildEnvironmentSummary(objects, anchor = null, scene = null) { if (labelLower === anchorLabel) continue; - if (obj.isGroundLike || ["ground", "floor", "terrain", "grass"].includes(labelLower)) { + if (obj.isGroundLike || ['ground', 'floor', 'terrain', 'grass'].includes(labelLower)) { hasGround = true; continue; } - if (obj.isSkyLike || labelLower.includes("sky")) { + if (obj.isSkyLike || labelLower.includes('sky')) { hasSky = true; continue; } @@ -890,11 +877,10 @@ function buildEnvironmentSummary(objects, anchor = null, scene = null) { } // Sort repeated scene elements by count, then label - const grouped = Array.from(counts.entries()) - .sort((a, b) => { - if (b[1] !== a[1]) return b[1] - a[1]; - return a[0].localeCompare(b[0]); - }); + const grouped = Array.from(counts.entries()).sort((a, b) => { + if (b[1] !== a[1]) return b[1] - a[1]; + return a[0].localeCompare(b[0]); + }); const parts = []; @@ -903,20 +889,20 @@ function buildEnvironmentSummary(objects, anchor = null, scene = null) { if (count === 1) { parts.push(`${label}`); } else { - parts.push(`${count} ${label}${count === 1 ? "" : "s"}`); + parts.push(`${count} ${label}${count === 1 ? '' : 's'}`); } } if (hasGround) { - parts.push("ground"); + parts.push('ground'); } if (hasSky) { - parts.push("sky"); + parts.push('sky'); } const chosen = parts.slice(0, 6); - if (!chosen.length) return ""; + if (!chosen.length) return ''; if (chosen.length === 1) { return `The scene includes ${chosen[0]}.`; @@ -926,18 +912,22 @@ function buildEnvironmentSummary(objects, anchor = null, scene = null) { return `The scene includes ${chosen[0]} and ${chosen[1]}.`; } - return `The scene includes ${chosen.slice(0, -1).join(", ")}, and ${chosen[chosen.length - 1]}.`; + return `The scene includes ${chosen.slice(0, -1).join(', ')}, and ${chosen[chosen.length - 1]}.`; } export function recordObjectSayText(targetName, text) { - const key = String(targetName || "").trim().toLowerCase(); + const key = String(targetName || '') + .trim() + .toLowerCase(); const spoken = cleanSpokenAnnouncement(resolveSpokenText(text)); if (!key || !spoken) return; objectSayTextCache.set(key, spoken); } export function setTransientSayText(targetName, text, duration) { - const key = String(targetName || "").trim().toLowerCase(); + const key = String(targetName || '') + .trim() + .toLowerCase(); if (!key) return; if (objectTransientSayTimers.has(key)) { @@ -969,21 +959,25 @@ function getSayMeshCandidates(mesh) { getObjectLabel(mesh), getObjectLabel(getEntityRoot(mesh)), ] - .map((v) => String(v || "").trim().toLowerCase()) + .map((v) => + String(v || '') + .trim() + .toLowerCase() + ) .filter(Boolean); } function getTransientSayTextForMesh(mesh) { - if (!mesh) return ""; + if (!mesh) return ''; for (const key of getSayMeshCandidates(mesh)) { const hit = objectTransientSayCache.get(key); if (hit) return hit; } - return ""; + return ''; } function getCachedSayTextForMesh(mesh) { - if (!mesh) return ""; + if (!mesh) return ''; const candidates = getSayMeshCandidates(mesh); for (const key of candidates) { @@ -996,23 +990,21 @@ function getCachedSayTextForMesh(mesh) { if (hit) return hit; } - return ""; + return ''; } function describeCharacterIntro(scene) { const anchor = getReferenceAnchor(scene); - if (!anchor?.mesh || anchor.kind !== "character") { - return ""; + if (!anchor?.mesh || anchor.kind !== 'character') { + return ''; } const label = getObjectLabel(anchor.mesh); const promptText = getCachedPromptTextForMesh(anchor.mesh); - const hint = cleanSpokenAnnouncement( - resolveSpokenText(getInteractionHint(anchor.mesh)) - ); + const hint = cleanSpokenAnnouncement(resolveSpokenText(getInteractionHint(anchor.mesh))); - let msg = ""; + let msg = ''; if (promptText) { msg += ` You can interact with ${label}. ${label} says: ${promptText}.`; @@ -1024,15 +1016,17 @@ function describeCharacterIntro(scene) { } export function describeScene(scene) { - if (!scene) return "No scene loaded."; - if (!scene.activeCamera) return "No active camera is available."; + if (!scene) return 'No scene loaded.'; + if (!scene.activeCamera) return 'No active camera is available.'; const anchor = getReferenceAnchor(scene); const objects = getSceneObjects(scene, { anchor }); const labelCounts = new Map(); for (const o of objects) { - const key = String(o.label || "").trim().toLowerCase(); + const key = String(o.label || '') + .trim() + .toLowerCase(); if (!key) continue; labelCounts.set(key, (labelCounts.get(key) || 0) + 1); } @@ -1045,15 +1039,17 @@ export function describeScene(scene) { } if (objects.length === 0) { - if (parts.length) return parts.join(" "); - return "I cannot detect any objects around you yet."; + if (parts.length) return parts.join(' '); + return 'I cannot detect any objects around you yet.'; } const environmentSummary = buildEnvironmentSummary(objects, anchor, scene); const mainObjects = objects .filter((o) => { - const key = String(o.label || "").trim().toLowerCase(); + const key = String(o.label || '') + .trim() + .toLowerCase(); const repeatedScenery = (labelCounts.get(key) || 0) >= 4; return !o.isEnvironment && !repeatedScenery; }) @@ -1066,23 +1062,21 @@ export function describeScene(scene) { } if (mainObjects.length > 0) { - parts.push( - top.map((o) => objectToSentence(o, { includeText: true })).join(" "), - ); + parts.push(top.map((o) => objectToSentence(o, { includeText: true })).join(' ')); } else { - parts.push("I can detect the environment, but no nearby main objects."); + parts.push('I can detect the environment, but no nearby main objects.'); } if (worldInstructionTexts.length) { - parts.push(`Instructions: ${worldInstructionTexts.join(". ")}.`); + parts.push(`Instructions: ${worldInstructionTexts.join('. ')}.`); } - return parts.join(" "); + return parts.join(' '); } export function describeNearestObject(scene) { - if (!scene) return "No scene loaded."; - if (!scene.activeCamera) return "No active camera is available."; + if (!scene) return 'No scene loaded.'; + if (!scene.activeCamera) return 'No active camera is available.'; const anchor = getReferenceAnchor(scene); const objects = getSceneObjects(scene, { anchor }) @@ -1090,7 +1084,7 @@ export function describeNearestObject(scene) { .sort((a, b) => a.distFromAnchor - b.distFromAnchor); if (objects.length === 0) { - return "I cannot detect any nearby objects."; + return 'I cannot detect any nearby objects.'; } const nearest = objects[0]; @@ -1101,15 +1095,17 @@ export function describeNearestObject(scene) { } export function describeFacingObject(scene) { - if (!scene) return "No scene loaded."; + if (!scene) return 'No scene loaded.'; const camera = scene?.activeCamera; - if (!camera) return "No active camera is available."; + if (!camera) return 'No active camera is available.'; const cameraPos = camera.globalPosition || camera.position; - if (!cameraPos) return "No active camera is available."; + if (!cameraPos) return 'No active camera is available.'; // Full 3D forward direction from the camera (where the player is looking). - let fwdX = 0, fwdY = 0, fwdZ = 1; + let fwdX = 0, + fwdY = 0, + fwdZ = 1; try { const dir = camera.getForwardRay?.(1)?.direction; if (dir && Number.isFinite(dir.x) && Number.isFinite(dir.y) && Number.isFinite(dir.z)) { @@ -1118,13 +1114,15 @@ export function describeFacingObject(scene) { fwdY = dir.y / len; fwdZ = dir.z / len; } - } catch (_) { /* fall through to default forward */ } + } catch (_) { + /* fall through to default forward */ + } const anchor = getReferenceAnchor(scene); // Cast the ray from the player's position when available, not the camera's. // The camera may be offset behind/above in third-person; the player is what moves forward. - const rayOrigin = (anchor?.kind === "character" && anchor.position) ? anchor.position : cameraPos; + const rayOrigin = anchor?.kind === 'character' && anchor.position ? anchor.position : cameraPos; // Tube test is XZ-only: the player moves on the ground plane, so vertical offset // (e.g. a tall tree whose bounding-box centre is several metres up) is irrelevant. @@ -1212,15 +1210,13 @@ export function describeFacingObject(scene) { } } - if (!bestLabel) return "Nothing ahead."; + if (!bestLabel) return 'Nothing ahead.'; const reportDist = anchor?.position ? (closestBoundingBoxDistance(bestRoot, anchor.position) ?? bestRawDist) : bestRawDist; - const dir = Math.abs(bestSigned) < 1.0 ? "Forward" - : bestSigned > 0 ? "Left" - : "Right"; + const dir = Math.abs(bestSigned) < 1.0 ? 'Forward' : bestSigned > 0 ? 'Left' : 'Right'; return `${dir}: ${bestLabel}, ${getDistanceLabel(reportDist)}.`; } @@ -1229,19 +1225,18 @@ function describeInitialWorld(scene) { const sceneIntro = describeScene(scene); if (charIntro && sceneIntro) return `${charIntro} ${sceneIntro}`; - return charIntro || sceneIntro || "World loaded."; + return charIntro || sceneIntro || 'World loaded.'; } export function getHelpText(scene) { - const custom = - scene?.metadata?.a11yInstructions || scene?.metadata?.instructions; + const custom = scene?.metadata?.a11yInstructions || scene?.metadata?.instructions; const baseInstructions = custom ? String(custom).trim() - : "Use keyboard controls to navigate and interact with objects. Canvas keyboard controls: Arrow keys or WASD to move camera, Mouse to look around, Space for actions, Tab to navigate to other interface elements."; + : 'Use keyboard controls to navigate and interact with objects. Canvas keyboard controls: Arrow keys or WASD to move camera, Mouse to look around, Space for actions, Tab to navigate to other interface elements.'; const ctrlInstructions = - " Press Control plus I to hear a scene summary. Press Control plus J to hear the nearest object. Press Control plus K to hear the object directly ahead. Press Control plus H to repeat these instructions."; + ' Press Control plus I to hear a scene summary. Press Control plus J to hear the nearest object. Press Control plus K to hear the object directly ahead. Press Control plus H to repeat these instructions.'; return `${baseInstructions}${ctrlInstructions}`; } @@ -1249,7 +1244,7 @@ export function announceHelp(scene) { announce(getHelpText(scene)); } -function announceInteraction(mesh, actionWord = "interacted with", options = {}) { +function announceInteraction(mesh, actionWord = 'interacted with', options = {}) { if (!mesh) return; const { noDedup = false } = options; @@ -1257,17 +1252,11 @@ function announceInteraction(mesh, actionWord = "interacted with", options = {}) const label = getObjectLabel(root); const hint = getInteractionHint(root); const pos = getRepresentativePosition(root, mesh); - const textLabels = currentScene - ? collectNearbyTextForObject(currentScene, pos, root) - : []; + const textLabels = currentScene ? collectNearbyTextForObject(currentScene, pos, root) : []; const now = Date.now(); - const interactionKey = `${actionWord}:${label}:${hint}:${(textLabels || []).join("|")}`; - if ( - !noDedup && - interactionKey === lastInteractionKey && - now - lastInteractionTime < 400 - ) { + const interactionKey = `${actionWord}:${label}:${hint}:${(textLabels || []).join('|')}`; + if (!noDedup && interactionKey === lastInteractionKey && now - lastInteractionTime < 400) { return; } lastInteractionKey = interactionKey; @@ -1275,7 +1264,7 @@ function announceInteraction(mesh, actionWord = "interacted with", options = {}) let msg = `You ${actionWord} ${label}.`; if (textLabels.length) { - msg += ` Text: ${textLabels.join(". ")}.`; + msg += ` Text: ${textLabels.join('. ')}.`; } if (hint) { msg += ` ${hint}`; @@ -1292,9 +1281,7 @@ function announceMeshAction(mesh, actionWord, options = {}) { // is for Ctrl+J discovery; the say block announces its own text when it // fires, so repeating the old text on interaction is confusing. const transientSay = getTransientSayTextForMesh(root); - const textLabels = currentScene - ? collectNearbyTextForObject(currentScene, pos, root) - : []; + const textLabels = currentScene ? collectNearbyTextForObject(currentScene, pos, root) : []; if (transientSay) { announce(`${label} says: ${transientSay}`, options); @@ -1302,7 +1289,7 @@ function announceMeshAction(mesh, actionWord, options = {}) { } if (textLabels.length) { - announce(`${label}. ${textLabels.join(". ")}`, options); + announce(`${label}. ${textLabels.join('. ')}`, options); return; } @@ -1315,14 +1302,14 @@ function attachPointerAnnouncements(scene) { if (pointerObserverScene && pointerObserverRef) { try { pointerObserverScene.onPointerObservable.remove(pointerObserverRef); - } catch { /* observer already removed */ } + } catch { + /* observer already removed */ + } pointerObserverRef = null; pointerObserverScene = null; } - const PointerTypes = - window.BABYLON?.PointerEventTypes || - globalThis.BABYLON?.PointerEventTypes; + const PointerTypes = window.BABYLON?.PointerEventTypes || globalThis.BABYLON?.PointerEventTypes; pointerObserverScene = scene; @@ -1337,23 +1324,21 @@ function attachPointerAnnouncements(scene) { if (!pickedMesh) return; - const isBabylonPick = PointerTypes - ? type === PointerTypes.POINTERPICK - : true; + const isBabylonPick = PointerTypes ? type === PointerTypes.POINTERPICK : true; // Require a real primary click / tap, not hover or passive movement const isPrimaryMouseClick = !evt || - evt.pointerType === "touch" || - evt.type === "click" || - evt.type === "pointerup" || - evt.type === "mouseup"; + evt.pointerType === 'touch' || + evt.type === 'click' || + evt.type === 'pointerup' || + evt.type === 'mouseup'; if (!isBabylonPick || !isPrimaryMouseClick) return; if (introInProgress || Date.now() < suppressPointerUntil) return; - announceMeshAction(pickedMesh, "selected"); + announceMeshAction(pickedMesh, 'selected'); } catch { // fail silently } @@ -1362,11 +1347,13 @@ function attachPointerAnnouncements(scene) { onInteractObservable.add((mesh) => { if (introInProgress || Date.now() < suppressPointerUntil) return; - announceMeshAction(mesh, "interacted with", { noDedup: true }); + announceMeshAction(mesh, 'interacted with', { noDedup: true }); }); export function recordObjectPromptText(targetName, text) { - const key = String(targetName || "").trim().toLowerCase(); + const key = String(targetName || '') + .trim() + .toLowerCase(); const spoken = cleanSpokenAnnouncement(resolveSpokenText(text)); if (!key || !spoken) return; @@ -1378,15 +1365,19 @@ export function recordObjectPromptText(targetName, text) { } function getCachedPromptTextForMesh(mesh) { - if (!mesh) return ""; + if (!mesh) return ''; const candidates = [ mesh?.name, getEntityRoot(mesh)?.name, getObjectLabel(mesh), - getObjectLabel(getEntityRoot(mesh)) + getObjectLabel(getEntityRoot(mesh)), ] - .map((v) => String(v || "").trim().toLowerCase()) + .map((v) => + String(v || '') + .trim() + .toLowerCase() + ) .filter(Boolean); for (const key of candidates) { @@ -1394,7 +1385,7 @@ function getCachedPromptTextForMesh(mesh) { if (hit) return hit; } - return ""; + return ''; } export function announceSayText(text, options = {}) { @@ -1405,8 +1396,8 @@ export function announceSayText(text, options = {}) { const lower = spoken.toLowerCase(); if ( - lower.includes("flock xr loaded successfully") || - lower.includes("flock world successfully loaded") + lower.includes('flock xr loaded successfully') || + lower.includes('flock world successfully loaded') ) { return; } @@ -1464,10 +1455,10 @@ function scheduleInitialIntro(scene) { const waitUntilReady = (tries = 0) => { if (scene !== currentScene) return; - const loadingScreen = document.getElementById("loadingScreen"); + const loadingScreen = document.getElementById('loadingScreen'); const stillLoading = - document.body.classList.contains("loading") || - (loadingScreen && !loadingScreen.classList.contains("fade-out")); + document.body.classList.contains('loading') || + (loadingScreen && !loadingScreen.classList.contains('fade-out')); if (stillLoading && tries < 40) { setTimeout(() => waitUntilReady(tries + 1), 250); @@ -1484,10 +1475,10 @@ function scheduleInitialIntro(scene) { }); }; - if (document.readyState === "complete") { + if (document.readyState === 'complete') { waitUntilReady(); } else { - window.addEventListener("load", () => waitUntilReady(), { once: true }); + window.addEventListener('load', () => waitUntilReady(), { once: true }); } } @@ -1498,9 +1489,9 @@ export function enableSceneDescription(scene, inputManager) { createLiveRegion(); // Reset per-world state - lastInteractionKey = ""; + lastInteractionKey = ''; lastInteractionTime = 0; - lastAnnouncedText = ""; + lastAnnouncedText = ''; lastAnnouncedAt = 0; announceSeq += 1; @@ -1516,43 +1507,42 @@ export function enableSceneDescription(scene, inputManager) { attachPointerAnnouncements(scene); scheduleInitialIntro(scene); - if (keyListenerAttached) return; keyListenerAttached = true; document.addEventListener( - "keydown", + 'keydown', (e) => { - const tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : ""; - if (tag === "input" || tag === "textarea" || e.target?.isContentEditable) return; + const tag = e.target && e.target.tagName ? e.target.tagName.toLowerCase() : ''; + if (tag === 'input' || tag === 'textarea' || e.target?.isContentEditable) return; if (!e.ctrlKey || e.altKey || e.metaKey) return; if (!e.key) return; const key = e.key.toLowerCase(); - if (key === "i") { + if (key === 'i') { e.preventDefault(); e.stopPropagation(); announce(describeScene(currentScene)); return; } - if (key === "j") { + if (key === 'j') { e.preventDefault(); e.stopPropagation(); announce(describeNearestObject(currentScene)); return; } - if (key === "h") { + if (key === 'h') { e.preventDefault(); e.stopPropagation(); announceHelp(currentScene); return; } - if (key === "k") { + if (key === 'k') { e.preventDefault(); e.stopPropagation(); announce(describeFacingObject(currentScene), { noDedup: true }); @@ -1563,9 +1553,9 @@ export function enableSceneDescription(scene, inputManager) { if (inputManager) { inputManager.onActionDownObservable.add((action) => { - if (action === "A11Y_I") announce(describeScene(currentScene)); - else if (action === "A11Y_J") announce(describeNearestObject(currentScene)); - else if (action === "A11Y_K") announce(describeFacingObject(currentScene), { noDedup: true }); + if (action === 'A11Y_I') announce(describeScene(currentScene)); + else if (action === 'A11Y_J') announce(describeNearestObject(currentScene)); + else if (action === 'A11Y_K') announce(describeFacingObject(currentScene), { noDedup: true }); }); } } @@ -1574,6 +1564,6 @@ export function enableSceneDescription(scene, inputManager) { * Helper for Flock code to call directly on custom events * (e.g., collisions, scripted triggers, button clicks). */ -export function announceSceneEvent(mesh, verb = "interacted with") { +export function announceSceneEvent(mesh, verb = 'interacted with') { announceInteraction(mesh, verb); } diff --git a/accessibility/keyboardui.js b/accessibility/keyboardui.js index 0a012c41..10e157e2 100644 --- a/accessibility/keyboardui.js +++ b/accessibility/keyboardui.js @@ -1,8 +1,8 @@ -import { KeyboardDispatcher } from "../main/keyboardDispatcher.js"; -import { ContextManager } from "../main/context.js"; -import { translate } from "../main/translation.js"; -import { SHORTCUTS_HELP_URL } from "../config.js"; -import { stopCanvasKeyboardMode } from "../ui/canvas-utils.js"; +import { KeyboardDispatcher } from '../main/keyboardDispatcher.js'; +import { ContextManager } from '../main/context.js'; +import { translate } from '../main/translation.js'; +import { SHORTCUTS_HELP_URL } from '../config.js'; +import { stopCanvasKeyboardMode } from '../ui/canvas-utils.js'; // Area menu accessed with Ctrl + B to quickly skip to // different areas on the interface @@ -10,29 +10,32 @@ import { stopCanvasKeyboardMode } from "../ui/canvas-utils.js"; const AreaManager = { overlay: null, areas: [ - { selector: "#menuleft", label: "1", name: "Top left menu" }, - { selector: "#menuright", label: "2", name: "Top right menu" }, - { selector: "#renderCanvas", label: "3", name: "Canvas" }, - { selector: "#gizmoButtons", label: "4", name: "Gizmos" }, - { selector: "#info-panel-tabs", label: "5", name: "Info panel tabs", focusSelector: "#info-tab-btn-shortcuts" }, - { selector: "#resizer", label: "6", pad: -3, name: "Resizer" }, - { selector: ".blocklyToolbox", label: "7", name: "Toolbox" }, - { selector: "svg.blocklySvg", label: "8", name: "Code editor" }, - { - selector: "#blocklyZoomControls", - label: "9", - name: "Workspace controls", + { selector: '#menuleft', label: '1', name: 'Top left menu' }, + { selector: '#menuright', label: '2', name: 'Top right menu' }, + { selector: '#renderCanvas', label: '3', name: 'Canvas' }, + { selector: '#gizmoButtons', label: '4', name: 'Gizmos' }, + { + selector: '#info-panel-tabs', + label: '5', + name: 'Info panel tabs', + focusSelector: '#info-tab-btn-shortcuts', + }, + { selector: '#resizer', label: '6', pad: -3, name: 'Resizer' }, + { selector: '.blocklyToolbox', label: '7', name: 'Toolbox' }, + { selector: 'svg.blocklySvg', label: '8', name: 'Code editor' }, + { + selector: '#blocklyZoomControls', + label: '9', + name: 'Workspace controls', extend: { top: -8 }, }, ], get effectiveAreas() { - const reloadBtn = document.getElementById("reload-btn"); + const reloadBtn = document.getElementById('reload-btn'); if (reloadBtn?.isConnected) { return this.areas.map((a) => - a.label === "9" - ? { selector: "#reload-btn", label: "9", name: "Reload" } - : a, + a.label === '9' ? { selector: '#reload-btn', label: '9', name: 'Reload' } : a ); } return this.areas; @@ -45,12 +48,12 @@ const AreaManager = { createOverlay() { // Create the element dynamically so you don't have to edit index.html - const div = document.createElement("div"); - div.id = "area-menu-overlay"; - div.classList.add("hidden"); - div.setAttribute("role", "dialog"); - div.setAttribute("aria-modal", "true"); - div.setAttribute("aria-label", "Area navigation menu"); + const div = document.createElement('div'); + div.id = 'area-menu-overlay'; + div.classList.add('hidden'); + div.setAttribute('role', 'dialog'); + div.setAttribute('aria-modal', 'true'); + div.setAttribute('aria-label', 'Area navigation menu'); div.tabIndex = -1; div.innerHTML = `
`; document.body.appendChild(div); @@ -64,36 +67,32 @@ const AreaManager = { GizmoMenuManager.toggle(false); // Close gizmo menu if open this.renderHighlights(); this._previousInertStates = new Map(); - document - .querySelectorAll("body > *:not(#area-menu-overlay)") - .forEach((el) => { - this._previousInertStates.set(el, el.inert); - el.inert = true; - }); + document.querySelectorAll('body > *:not(#area-menu-overlay)').forEach((el) => { + this._previousInertStates.set(el, el.inert); + el.inert = true; + }); this.previousFocus = document.activeElement; setTimeout(() => this.overlay.focus(), 0); } else { - this._previousInertStates?.forEach( - (wasInert, el) => (el.inert = wasInert), - ); + this._previousInertStates?.forEach((wasInert, el) => (el.inert = wasInert)); this._previousInertStates = null; this.previousFocus?.focus(); } - this.overlay.classList.toggle("hidden", !show); + this.overlay.classList.toggle('hidden', !show); } }, setupListeners() { - KeyboardDispatcher.on("*", "Mod+KeyB", (e) => { + KeyboardDispatcher.on('*', 'Mod+KeyB', (e) => { e.preventDefault(); e.stopPropagation(); - this.toggle(this.overlay.classList.contains("hidden")); + this.toggle(this.overlay.classList.contains('hidden')); }); - KeyboardDispatcher.on("OVERLAY", "Escape", () => this.toggle(false)); + KeyboardDispatcher.on('OVERLAY', 'Escape', () => this.toggle(false)); for (let i = 1; i <= 9; i++) { - KeyboardDispatcher.on("OVERLAY", `Digit${i}`, (e) => { + KeyboardDispatcher.on('OVERLAY', `Digit${i}`, (e) => { e.preventDefault(); const area = this.effectiveAreas.find((a) => a.label === String(i)); if (area) this.activateArea(area); @@ -101,7 +100,7 @@ const AreaManager = { } const cycleBadges = (reverse) => { - const badges = [...this.overlay.querySelectorAll(".area-number-badge")]; + const badges = [...this.overlay.querySelectorAll('.area-number-badge')]; if (badges.length === 0) return; const currentIndex = badges.indexOf(document.activeElement); const nextIndex = reverse @@ -112,26 +111,26 @@ const AreaManager = { badges[nextIndex].focus(); }; - KeyboardDispatcher.on("OVERLAY", "Tab", (e) => { + KeyboardDispatcher.on('OVERLAY', 'Tab', (e) => { e.preventDefault(); cycleBadges(false); }); - KeyboardDispatcher.on("OVERLAY", "Shift+Tab", (e) => { + KeyboardDispatcher.on('OVERLAY', 'Shift+Tab', (e) => { e.preventDefault(); cycleBadges(true); }); - KeyboardDispatcher.on("OVERLAY", "Enter", (e) => { + KeyboardDispatcher.on('OVERLAY', 'Enter', (e) => { const focused = document.activeElement; - if (!focused?.classList.contains("area-number-badge")) return; + if (!focused?.classList.contains('area-number-badge')) return; e.preventDefault(); const area = this.effectiveAreas.find((a) => a.label === focused.innerText); if (area) this.activateArea(area); }); // Re-render if the browser window gets resized - window.addEventListener("resize", () => { - if (!this.overlay.classList.contains("hidden")) { + window.addEventListener('resize', () => { + if (!this.overlay.classList.contains('hidden')) { requestAnimationFrame(() => this.renderHighlights()); } }); @@ -143,29 +142,28 @@ const AreaManager = { const el = document.querySelector(area.selector); const childFocusable = el?.querySelector( - 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ) ?? el; const focusable = - (area.focusSelector ? document.querySelector(area.focusSelector) : null) ?? - childFocusable; + (area.focusSelector ? document.querySelector(area.focusSelector) : null) ?? childFocusable; focusable?.focus(); - if (area.selector === "#gizmoButtons") GizmoMenuManager.toggle(true); + if (area.selector === '#gizmoButtons') GizmoMenuManager.toggle(true); }, renderHighlights() { - const container = document.getElementById("area-menu-content"); - container.innerHTML = ""; // Clear old numbers + const container = document.getElementById('area-menu-content'); + container.innerHTML = ''; // Clear old numbers this.effectiveAreas.forEach((area) => { const el = document.querySelector(area.selector); if (el && (el.offsetWidth > 0 || el.getBoundingClientRect().width > 0)) { const rect = el.getBoundingClientRect(); - const badge = document.createElement("div"); - badge.className = "area-number-badge"; - badge.setAttribute("role", "button"); - badge.setAttribute("aria-label", `${area.label}: ${area.name}`); + const badge = document.createElement('div'); + badge.className = 'area-number-badge'; + badge.setAttribute('role', 'button'); + badge.setAttribute('aria-label', `${area.label}: ${area.name}`); badge.tabIndex = 0; // Make badges focusable badge.innerText = area.label; @@ -175,14 +173,14 @@ const AreaManager = { container.appendChild(badge); - const highlight = document.createElement("div"); + const highlight = document.createElement('div'); const pad = area.pad ?? 1; const ext = area.extend ?? {}; const eTop = ext.top ?? 0; const eBottom = ext.bottom ?? 0; const eLeft = ext.left ?? 0; const eRight = ext.right ?? 0; - highlight.className = "area-outline"; + highlight.className = 'area-outline'; highlight.style.top = `${rect.top - pad - eTop}px`; highlight.style.left = `${rect.left - pad - eLeft}px`; highlight.style.width = `${rect.width + pad * 2 + eLeft + eRight}px`; @@ -197,15 +195,15 @@ const AreaManager = { const GizmoMenuManager = { overlay: null, buttons: [ - { id: "showShapesButton", label: "1" }, - { id: "colorPickerButton", label: "2" }, - { id: "positionButton", label: "3" }, - { id: "rotationButton", label: "4" }, - { id: "scaleButton", label: "5" }, - { id: "selectButton", label: "6" }, - { id: "duplicateButton", label: "7" }, - { id: "deleteButton", label: "8" }, - { id: "cameraButton", label: "9" }, + { id: 'showShapesButton', label: '1' }, + { id: 'colorPickerButton', label: '2' }, + { id: 'positionButton', label: '3' }, + { id: 'rotationButton', label: '4' }, + { id: 'scaleButton', label: '5' }, + { id: 'selectButton', label: '6' }, + { id: 'duplicateButton', label: '7' }, + { id: 'deleteButton', label: '8' }, + { id: 'cameraButton', label: '9' }, ], init() { @@ -214,16 +212,16 @@ const GizmoMenuManager = { }, createOverlay() { - const div = document.createElement("div"); - div.id = "gizmo-menu-overlay"; - div.className = "hidden"; + const div = document.createElement('div'); + div.id = 'gizmo-menu-overlay'; + div.className = 'hidden'; div.innerHTML = `
`; document.body.appendChild(div); this.overlay = div; }, isOpen() { - return !this.overlay.classList.contains("hidden"); + return !this.overlay.classList.contains('hidden'); }, toggle(show) { @@ -232,49 +230,49 @@ const GizmoMenuManager = { this.renderBadges(); if (this._watchFocus) { - document.removeEventListener("focusin", this._watchFocus); + document.removeEventListener('focusin', this._watchFocus); } if (this._watchPointer) { - document.removeEventListener("pointerdown", this._watchPointer, { + document.removeEventListener('pointerdown', this._watchPointer, { capture: true, }); } this._watchFocus = () => { const ctx = ContextManager.getCurrentContext(); - if (ctx !== "GIZMO" && ctx !== "NAVIGATION") this.toggle(false); + if (ctx !== 'GIZMO' && ctx !== 'NAVIGATION') this.toggle(false); }; this._watchPointer = () => this.toggle(false); - document.addEventListener("focusin", this._watchFocus); - document.addEventListener("pointerdown", this._watchPointer, { + document.addEventListener('focusin', this._watchFocus); + document.addEventListener('pointerdown', this._watchPointer, { capture: true, }); // Focus 1st button if nothing in gizmos is already focused, // but if another gizmo is active, leave focus there - const alreadyFocused = document.activeElement?.closest("#gizmoButtons"); + const alreadyFocused = document.activeElement?.closest('#gizmoButtons'); if (!alreadyFocused) { const btn = - document.querySelector(".gizmo-button.active") || - document.getElementById("showShapesButton"); + document.querySelector('.gizmo-button.active') || + document.getElementById('showShapesButton'); if (btn && !btn.disabled && btn.offsetParent !== null) btn.focus(); } } else { - document.removeEventListener("focusin", this._watchFocus); - document.removeEventListener("pointerdown", this._watchPointer, { + document.removeEventListener('focusin', this._watchFocus); + document.removeEventListener('pointerdown', this._watchPointer, { capture: true, }); this._watchFocus = null; this._watchPointer = null; } - this.overlay.classList.toggle("hidden", !show); + this.overlay.classList.toggle('hidden', !show); }, setupListeners() { // Toggle gizmo menu with Ctrl + G - KeyboardDispatcher.on("*", "Mod+KeyG", (e) => { + KeyboardDispatcher.on('*', 'Mod+KeyG', (e) => { const ctx = ContextManager.getCurrentContext(); - if (ctx === "TYPING" || ctx === "OVERLAY") return; + if (ctx === 'TYPING' || ctx === 'OVERLAY') return; e.preventDefault(); e.stopPropagation(); this.toggle(true); @@ -282,7 +280,7 @@ const GizmoMenuManager = { // Activate gizmo buttons with number keys for (let i = 1; i <= 9; i++) { - KeyboardDispatcher.on("*", `Digit${i}`, () => { + KeyboardDispatcher.on('*', `Digit${i}`, () => { if (!this.isOpen()) return; const entry = this.buttons.find((b) => b.label === String(i)); if (entry) this.activateButton(entry); @@ -290,8 +288,8 @@ const GizmoMenuManager = { } // Move the gizmo buttons if the window is resized - const gizmoButtons = document.getElementById("gizmoButtons"); - const resizer = document.getElementById("resizer"); + const gizmoButtons = document.getElementById('gizmoButtons'); + const resizer = document.getElementById('resizer'); if (gizmoButtons) { new ResizeObserver(() => { if (this.isOpen()) this.renderBadges(); @@ -299,10 +297,10 @@ const GizmoMenuManager = { } if (resizer) { new MutationObserver(() => { - if (!resizer.classList.contains("resizing") && this.isOpen()) { + if (!resizer.classList.contains('resizing') && this.isOpen()) { this.renderBadges(); } - }).observe(resizer, { attributes: true, attributeFilter: ["class"] }); + }).observe(resizer, { attributes: true, attributeFilter: ['class'] }); } }, @@ -314,14 +312,14 @@ const GizmoMenuManager = { }, renderBadges() { - const container = document.getElementById("gizmo-menu-content"); - container.innerHTML = ""; + const container = document.getElementById('gizmo-menu-content'); + container.innerHTML = ''; this.buttons.forEach((entry) => { const el = document.getElementById(entry.id); if (!el || el.offsetParent === null) return; const rect = el.getBoundingClientRect(); - const badge = document.createElement("div"); - badge.className = "gizmo-key-badge"; + const badge = document.createElement('div'); + badge.className = 'gizmo-key-badge'; badge.innerText = entry.label; badge.style.top = `${rect.top + rect.height + 8}px`; badge.style.left = `${rect.left + rect.width / 2}px`; @@ -334,245 +332,243 @@ const GizmoMenuManager = { // Check their platform (Mac or not Mac) to show the correct modifier key function isMac() { - return (navigator.userAgentData?.platform ?? navigator.platform) - .toUpperCase() - .includes("MAC"); + return (navigator.userAgentData?.platform ?? navigator.platform).toUpperCase().includes('MAC'); } // List of shortcuts to show in the panel, with categories for grouping function getShortcuts() { - const mod = isMac() ? "⌘" : "Ctrl"; + const mod = isMac() ? '⌘' : 'Ctrl'; return [ { - label: translate("shortcut_show_hide_help"), + label: translate('shortcut_show_hide_help'), keys: `${mod} + /`, - category: translate("shortcut_category_main"), + category: translate('shortcut_category_main'), }, { - label: translate("shortcut_move_between_areas"), + label: translate('shortcut_move_between_areas'), keys: `Tab / Shift + Tab`, - category: translate("shortcut_category_main"), + category: translate('shortcut_category_main'), }, { - label: translate("shortcut_confirm"), + label: translate('shortcut_confirm'), keys: `Enter`, - category: translate("shortcut_category_main"), + category: translate('shortcut_category_main'), }, { - label: translate("shortcut_exit"), + label: translate('shortcut_exit'), keys: `Esc`, - category: translate("shortcut_category_main"), + category: translate('shortcut_category_main'), }, { - label: translate("shortcut_play"), + label: translate('shortcut_play'), keys: `${mod} + P`, - category: translate("shortcut_category_main"), + category: translate('shortcut_category_main'), }, { - label: translate("shortcut_undo"), + label: translate('shortcut_undo'), keys: `${mod} + Z`, - category: translate("shortcut_category_main"), + category: translate('shortcut_category_main'), }, { - label: translate("shortcut_redo"), + label: translate('shortcut_redo'), keys: `${mod} + Shift + Z`, - category: translate("shortcut_category_main"), + category: translate('shortcut_category_main'), }, { - label: translate("shortcut_browser_nav"), + label: translate('shortcut_browser_nav'), keys: `${mod} + L`, - category: translate("shortcut_category_main"), + category: translate('shortcut_category_main'), }, { - label: translate("shortcut_main_menu"), + label: translate('shortcut_main_menu'), keys: `${mod} + M`, - category: translate("shortcut_category_menu"), + category: translate('shortcut_category_menu'), }, { - label: translate("shortcut_open_file"), + label: translate('shortcut_open_file'), keys: `${mod} + O`, - category: translate("shortcut_category_menu"), + category: translate('shortcut_category_menu'), }, { - label: translate("shortcut_save_export"), + label: translate('shortcut_save_export'), keys: `${mod} + S`, - category: translate("shortcut_category_menu"), + category: translate('shortcut_category_menu'), }, { - label: translate("shortcut_open_close_area_menu"), + label: translate('shortcut_open_close_area_menu'), keys: `${mod} + B`, - category: translate("shortcut_category_area_menu"), + category: translate('shortcut_category_area_menu'), }, { - label: translate("shortcut_toggle_area"), + label: translate('shortcut_toggle_area'), keys: `Tab`, - category: translate("shortcut_category_area_menu"), + category: translate('shortcut_category_area_menu'), }, { - label: translate("shortcut_select_area"), + label: translate('shortcut_select_area'), keys: `1-9 / Enter`, - category: translate("shortcut_category_area_menu"), + category: translate('shortcut_category_area_menu'), }, { - label: translate("shortcut_toolbox"), + label: translate('shortcut_toolbox'), keys: `T`, - category: translate("shortcut_category_toolbox"), + category: translate('shortcut_category_toolbox'), }, { - label: translate("shortcut_toolbox_typing"), - keys: `"${translate("shortcut_toolbox_typing_hint")}"`, - category: translate("shortcut_category_toolbox"), + label: translate('shortcut_toolbox_typing'), + keys: `"${translate('shortcut_toolbox_typing_hint')}"`, + category: translate('shortcut_category_toolbox'), }, { - label: translate("shortcut_nav_toolbox_blocks"), + label: translate('shortcut_nav_toolbox_blocks'), keys: `↑ ↓ ← →`, - category: translate("shortcut_category_toolbox"), + category: translate('shortcut_category_toolbox'), }, { - label: translate("shortcut_add_block"), + label: translate('shortcut_add_block'), keys: `Enter`, - category: translate("shortcut_category_toolbox"), + category: translate('shortcut_category_toolbox'), }, { - label: translate("shortcut_code_editor"), + label: translate('shortcut_code_editor'), keys: `${mod} + E`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_select_workspace"), + label: translate('shortcut_select_workspace'), keys: `W`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_move_through_blocks"), + label: translate('shortcut_move_through_blocks'), keys: `↑ ↓`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_move_in_out_blocks"), + label: translate('shortcut_move_in_out_blocks'), keys: `← →`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_next_block_stack"), + label: translate('shortcut_next_block_stack'), keys: `N`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_prev_block_stack"), + label: translate('shortcut_prev_block_stack'), keys: `B`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_add_block_by_name"), + label: translate('shortcut_add_block_by_name'), keys: `${mod} + ]`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_context_menu"), + label: translate('shortcut_context_menu'), keys: `${mod} + Enter`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_duplicate_block"), + label: translate('shortcut_duplicate_block'), keys: `D`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_detach_block"), + label: translate('shortcut_detach_block'), keys: `X`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_start_move_block"), + label: translate('shortcut_start_move_block'), keys: `M`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_move_arrows"), + label: translate('shortcut_move_arrows'), keys: `↑ ↓`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_move_anywhere"), + label: translate('shortcut_move_anywhere'), keys: `${mod} + ↑ ↓ ← →`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_search_block"), + label: translate('shortcut_search_block'), keys: `${mod} + F`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_select_next_result"), + label: translate('shortcut_select_next_result'), keys: `Enter`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_select_previous_result"), + label: translate('shortcut_select_previous_result'), keys: `Shift + Enter`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_focus_result"), + label: translate('shortcut_focus_result'), keys: `Esc`, - category: translate("shortcut_category_editor"), + category: translate('shortcut_category_editor'), }, { - label: translate("shortcut_open_gizmos"), + label: translate('shortcut_open_gizmos'), keys: `${mod} + G`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_select_gizmo"), + label: translate('shortcut_select_gizmo'), keys: `1-9`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_keyboard_cursor_gizmos"), + label: translate('shortcut_keyboard_cursor_gizmos'), keys: `↑ ↓ ← →`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_slow_cursor_gizmos"), + label: translate('shortcut_slow_cursor_gizmos'), keys: `Shift + ↑ ↓ ← →`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_lock_transform"), + label: translate('shortcut_lock_transform'), keys: `X Y Z`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_uniform_scale"), + label: translate('shortcut_uniform_scale'), keys: `U`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_transform_3d"), + label: translate('shortcut_transform_3d'), keys: `↑ ↓ ← → PgUp PgDn`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_focus_camera"), + label: translate('shortcut_focus_camera'), keys: `F`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_quick_colour"), + label: translate('shortcut_quick_colour'), keys: `C`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, { - label: translate("shortcut_delete_object"), + label: translate('shortcut_delete_object'), keys: `Del`, - category: translate("shortcut_category_gizmos"), + category: translate('shortcut_category_gizmos'), }, ]; } @@ -586,16 +582,16 @@ function formatKeys(keys) { return keys .split(/( \+ | \/ )/) .map((part) => - part === " + " + part === ' + ' ? part - : part === " / " + : part === ' / ' ? ` / ` : part - .split(" ") + .split(' ') .map((k) => `${k}`) - .join(" "), + .join(' ') ) - .join(""); + .join(''); } const InfoPanel = { @@ -603,31 +599,31 @@ const InfoPanel = { _activeId: null, init() { - this._el = document.getElementById("info-panel"); - this._tablist = document.getElementById("info-panel-tabs"); - this._body = document.getElementById("info-panel-body"); + this._el = document.getElementById('info-panel'); + this._tablist = document.getElementById('info-panel-tabs'); + this._body = document.getElementById('info-panel-body'); }, register(id, label) { - const btn = document.createElement("button"); + const btn = document.createElement('button'); btn.id = `info-tab-btn-${id}`; - btn.className = "info-tab-btn bigbutton"; - btn.setAttribute("role", "tab"); - btn.setAttribute("aria-selected", "false"); - btn.setAttribute("aria-controls", `info-tab-panel-${id}`); + btn.className = 'info-tab-btn bigbutton'; + btn.setAttribute('role', 'tab'); + btn.setAttribute('aria-selected', 'false'); + btn.setAttribute('aria-controls', `info-tab-panel-${id}`); btn.textContent = label; - btn.addEventListener("click", () => this.toggle(id)); + btn.addEventListener('click', () => this.toggle(id)); this._tablist.appendChild(btn); - const divider = document.createElement("div"); - divider.className = "toolbar-divider"; - divider.setAttribute("aria-hidden", "true"); + const divider = document.createElement('div'); + divider.className = 'toolbar-divider'; + divider.setAttribute('aria-hidden', 'true'); this._tablist.appendChild(divider); - const panel = document.createElement("div"); + const panel = document.createElement('div'); panel.id = `info-tab-panel-${id}`; - panel.className = "info-tab-panel hidden"; - panel.setAttribute("role", "tabpanel"); - panel.setAttribute("aria-labelledby", `info-tab-btn-${id}`); + panel.className = 'info-tab-panel hidden'; + panel.setAttribute('role', 'tabpanel'); + panel.setAttribute('aria-labelledby', `info-tab-btn-${id}`); panel.tabIndex = 0; this._body.appendChild(panel); @@ -638,47 +634,44 @@ const InfoPanel = { activate(id) { if (this._activeId && this._activeId !== id) { const cur = this._tabs.get(this._activeId); - cur.btn.setAttribute("aria-selected", "false"); - cur.btn.classList.remove("active"); - cur.panel.classList.add("hidden"); + cur.btn.setAttribute('aria-selected', 'false'); + cur.btn.classList.remove('active'); + cur.panel.classList.add('hidden'); } const tab = this._tabs.get(id); if (!tab) return; this._activeId = id; - tab.btn.setAttribute("aria-selected", "true"); - tab.btn.classList.add("active"); - tab.panel.classList.remove("hidden"); + tab.btn.setAttribute('aria-selected', 'true'); + tab.btn.classList.add('active'); + tab.panel.classList.remove('hidden'); tab.panel.focus(); }, deactivate(id) { const tab = this._tabs.get(id); if (!tab) return; - tab.btn.setAttribute("aria-selected", "false"); - tab.btn.classList.remove("active"); - tab.panel.classList.add("hidden"); + tab.btn.setAttribute('aria-selected', 'false'); + tab.btn.classList.remove('active'); + tab.panel.classList.add('hidden'); if (this._activeId === id) this._activeId = null; }, toggle(id) { const tab = this._tabs.get(id); if (!tab) return; - tab.panel.classList.contains("hidden") - ? this.activate(id) - : this.deactivate(id); + tab.panel.classList.contains('hidden') ? this.activate(id) : this.deactivate(id); }, }; const SHORTCUTS_FONT_SIZES = [0.8, 1.0, 1.2, 1.4, 1.6, 1.8]; -const SHORTCUTS_FONT_SIZE_KEY = "flock-shortcuts-font-size"; +const SHORTCUTS_FONT_SIZE_KEY = 'flock-shortcuts-font-size'; const SHORTCUTS_FONT_SIZE_DEFAULT = 1.2; const ShortcutsPanel = { panel: null, previousFocus: null, fontSize: - parseFloat(localStorage.getItem(SHORTCUTS_FONT_SIZE_KEY)) || - SHORTCUTS_FONT_SIZE_DEFAULT, + parseFloat(localStorage.getItem(SHORTCUTS_FONT_SIZE_KEY)) || SHORTCUTS_FONT_SIZE_DEFAULT, init() { this.createPanel(); @@ -693,21 +686,16 @@ const ShortcutsPanel = { if (next === this.fontSize) return; this.fontSize = next; localStorage.setItem(SHORTCUTS_FONT_SIZE_KEY, next); - this.panel.querySelector("#shortcuts-list").style.fontSize = next + "em"; - this.panel.querySelector(".shortcuts-decrease-btn").disabled = - next === sizes[0]; - this.panel.querySelector(".shortcuts-increase-btn").disabled = - next === sizes[sizes.length - 1]; + this.panel.querySelector('#shortcuts-list').style.fontSize = next + 'em'; + this.panel.querySelector('.shortcuts-decrease-btn').disabled = next === sizes[0]; + this.panel.querySelector('.shortcuts-increase-btn').disabled = next === sizes[sizes.length - 1]; }, createPanel() { - const panel = InfoPanel.register( - "shortcuts", - translate("shortcut_panel_title"), - ); - const btn = document.getElementById("info-tab-btn-shortcuts"); - btn.setAttribute("aria-label", translate("shortcut_panel_title")); - btn.setAttribute("title", translate("shortcut_panel_title")); + const panel = InfoPanel.register('shortcuts', translate('shortcut_panel_title')); + const btn = document.getElementById('info-tab-btn-shortcuts'); + btn.setAttribute('aria-label', translate('shortcut_panel_title')); + btn.setAttribute('title', translate('shortcut_panel_title')); btn.innerHTML = ``; panel.innerHTML = `
@@ -715,35 +703,33 @@ const ShortcutsPanel = {
- +
`; this.panel = panel; const sizes = SHORTCUTS_FONT_SIZES; - const decreaseBtn = panel.querySelector(".shortcuts-decrease-btn"); - const increaseBtn = panel.querySelector(".shortcuts-increase-btn"); + const decreaseBtn = panel.querySelector('.shortcuts-decrease-btn'); + const increaseBtn = panel.querySelector('.shortcuts-increase-btn'); decreaseBtn.disabled = this.fontSize === sizes[0]; increaseBtn.disabled = this.fontSize === sizes[sizes.length - 1]; - decreaseBtn.addEventListener("click", () => this.adjustFontSize(-1)); - increaseBtn.addEventListener("click", () => this.adjustFontSize(1)); - panel.querySelector("#shortcuts-list").style.fontSize = - this.fontSize + "em"; + decreaseBtn.addEventListener('click', () => this.adjustFontSize(-1)); + increaseBtn.addEventListener('click', () => this.adjustFontSize(1)); + panel.querySelector('#shortcuts-list').style.fontSize = this.fontSize + 'em'; this.renderContent(); }, renderContent() { document - .getElementById("info-tab-btn-shortcuts") - .setAttribute("aria-label", translate("shortcut_panel_title")); - this.panel.querySelector("#shortcuts-panel-title").textContent = translate( - "shortcut_panel_title", - ); + .getElementById('info-tab-btn-shortcuts') + .setAttribute('aria-label', translate('shortcut_panel_title')); + this.panel.querySelector('#shortcuts-panel-title').textContent = + translate('shortcut_panel_title'); this.panel - .querySelector(".help-link-button") - .setAttribute("aria-label", translate("shortcut_panel_help_link")); - const container = this.panel.querySelector("#shortcuts-list"); + .querySelector('.help-link-button') + .setAttribute('aria-label', translate('shortcut_panel_help_link')); + const container = this.panel.querySelector('#shortcuts-list'); const groups = getShortcuts().reduce((acc, s) => { (acc[s.category] ??= []).push(s); return acc; @@ -753,18 +739,18 @@ const ShortcutsPanel = { ([cat, items]) => `

${cat}

- ${items.map(({ label, keys }) => `
${label}
${formatKeys(keys)}
`).join("")} + ${items.map(({ label, keys }) => `
${label}
${formatKeys(keys)}
`).join('')}
- `, + ` ) - .join(""); + .join(''); }, show() { this.renderContent(); this.previousFocus = document.activeElement; - InfoPanel.activate("shortcuts"); - document.getElementById("shortcutsBtn")?.classList.add("active"); + InfoPanel.activate('shortcuts'); + document.getElementById('shortcutsBtn')?.classList.add('active'); }, refreshTranslations() { @@ -774,30 +760,30 @@ const ShortcutsPanel = { hide() { this.previousFocus?.focus(); this.previousFocus = null; - InfoPanel.deactivate("shortcuts"); - document.getElementById("shortcutsBtn")?.classList.remove("active"); + InfoPanel.deactivate('shortcuts'); + document.getElementById('shortcutsBtn')?.classList.remove('active'); }, toggle() { - this.panel.classList.contains("hidden") ? this.show() : this.hide(); + this.panel.classList.contains('hidden') ? this.show() : this.hide(); }, setupListeners() { - this.panel.addEventListener("keydown", (e) => { - const scroller = document.getElementById("info-panel-body"); - if (e.key === "ArrowUp") { + this.panel.addEventListener('keydown', (e) => { + const scroller = document.getElementById('info-panel-body'); + if (e.key === 'ArrowUp') { e.preventDefault(); - scroller?.scrollBy({ top: -100, behavior: "instant" }); + scroller?.scrollBy({ top: -100, behavior: 'instant' }); } - if (e.key === "ArrowDown") { + if (e.key === 'ArrowDown') { e.preventDefault(); - scroller?.scrollBy({ top: 100, behavior: "instant" }); + scroller?.scrollBy({ top: 100, behavior: 'instant' }); } - if (e.key === "Escape") { + if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); this.hide(); - document.getElementById("info-tab-btn-shortcuts")?.focus(); + document.getElementById('info-tab-btn-shortcuts')?.focus(); } }); }, @@ -806,7 +792,7 @@ const ShortcutsPanel = { // Start it up AreaManager.init(); GizmoMenuManager.init(); -if (document.getElementById("info-panel-tabs")) { +if (document.getElementById('info-panel-tabs')) { InfoPanel.init(); ShortcutsPanel.init(); } diff --git a/accessibility/uiA11y.js b/accessibility/uiA11y.js index bcbdacaa..0b1da122 100644 --- a/accessibility/uiA11y.js +++ b/accessibility/uiA11y.js @@ -30,8 +30,7 @@ function _createSrRoot() { root.setAttribute('role', 'group'); root.setAttribute('aria-label', 'UI controls'); // Visually hidden but accessible; individual elements are moved into view on focus - root.style.cssText = - 'position:absolute;left:-9999px;top:0;width:1px;height:1px;overflow:hidden;'; + root.style.cssText = 'position:absolute;left:-9999px;top:0;width:1px;height:1px;overflow:hidden;'; document.body.appendChild(root); return root; } @@ -50,9 +49,13 @@ function _installCanvasTabHandler(canvas) { function _getFocusableElements() { if (!_srRoot) return []; - return Array.from( - _srRoot.querySelectorAll('[data-flock-twin]') - ).filter((el) => el.tagName === 'BUTTON' || el.tagName === 'INPUT' || el.tagName === 'P' || el.tagName === 'SPAN'); + return Array.from(_srRoot.querySelectorAll('[data-flock-twin]')).filter( + (el) => + el.tagName === 'BUTTON' || + el.tagName === 'INPUT' || + el.tagName === 'P' || + el.tagName === 'SPAN' + ); } // Cycle focus within shadow controls; return to canvas at either end @@ -87,12 +90,12 @@ function _computeFixedRect({ x, y, w, h }) { // Negative coordinates mean right/bottom alignment (same logic as ui.js) const cssLeft = x < 0 ? cw - Math.abs(px) - pw : px; - const cssTop = y < 0 ? ch - Math.abs(py) - ph : py; + const cssTop = y < 0 ? ch - Math.abs(py) - ph : py; return { - left: cr.left + cssLeft, - top: cr.top + cssTop, - width: pw, + left: cr.left + cssLeft, + top: cr.top + cssTop, + width: pw, height: ph, }; } @@ -119,9 +122,9 @@ function _attachFocusBehavior(el, posParams) { el.addEventListener('focus', () => { const rect = _computeFixedRect(posParams); if (rect) { - indicator.style.left = `${rect.left}px`; - indicator.style.top = `${rect.top}px`; - indicator.style.width = `${rect.width}px`; + indicator.style.left = `${rect.left}px`; + indicator.style.top = `${rect.top}px`; + indicator.style.width = `${rect.width}px`; indicator.style.height = `${rect.height}px`; indicator.style.display = 'block'; } @@ -166,7 +169,11 @@ export function registerUIButton(id, text, babylonButton, posParams) { btn.removeEventListener('click', onClick); btn.removeEventListener('keydown', _onTwinKeydown); indicator.remove(); - try { babylonButton.onDisposeObservable.remove(disposeObs); } catch { /* already gone */ } + try { + babylonButton.onDisposeObservable.remove(disposeObs); + } catch { + /* already gone */ + } }, }); } @@ -206,15 +213,30 @@ export function registerUISlider(id, babylonSlider, posParams) { range.removeEventListener('input', onInput); range.removeEventListener('keydown', _onTwinKeydown); indicator.remove(); - try { babylonSlider.onValueChangedObservable.remove(valueObs); } catch { /* already gone */ } - try { babylonSlider.onDisposeObservable.remove(disposeObs); } catch { /* already gone */ } + try { + babylonSlider.onValueChangedObservable.remove(valueObs); + } catch { + /* already gone */ + } + try { + babylonSlider.onDisposeObservable.remove(disposeObs); + } catch { + /* already gone */ + } }, }); } // ─── UIInput twin ───────────────────────────────────────────────────────────── -export function registerUIInput(inputId, submitId, babylonInput, babylonSubmit, inputPosParams, submitPosParams) { +export function registerUIInput( + inputId, + submitId, + babylonInput, + babylonSubmit, + inputPosParams, + submitPosParams +) { if (!_srRoot) return; unregisterUIControl(inputId); @@ -231,9 +253,7 @@ export function registerUIInput(inputId, submitId, babylonInput, babylonSubmit, // Capture next focus target before the twins are removed by dispose const focusable = _getFocusableElements(); const idx = focusable.indexOf(submitBtn); - const nextTarget = idx >= 0 && idx < focusable.length - 1 - ? focusable[idx + 1] - : _canvas; + const nextTarget = idx >= 0 && idx < focusable.length - 1 ? focusable[idx + 1] : _canvas; babylonInput.text = htmlInput.value; babylonSubmit.onPointerUpObservable.notifyObservers({}); @@ -249,12 +269,11 @@ export function registerUIInput(inputId, submitId, babylonInput, babylonSubmit, const _applyStyle = (focused) => { const rect = _computeFixedRect(inputPosParams); if (!rect) return; - const bg = babylonInput.background || '#ffffff'; - const fg = babylonInput.color || '#000000'; - const fs = babylonInput.fontSize || '16px'; + const bg = babylonInput.background || '#ffffff'; + const fg = babylonInput.color || '#000000'; + const fs = babylonInput.fontSize || '16px'; const border = focused ? '2px solid #005fcc' : '1px solid rgba(128,128,128,0.6)'; - _phStyle.textContent = - `#${_styleId}-el::placeholder { color: ${fg}; opacity: 0.5; }`; + _phStyle.textContent = `#${_styleId}-el::placeholder { color: ${fg}; opacity: 0.5; }`; htmlInput.id = `${_styleId}-el`; htmlInput.style.cssText = `position:fixed;left:${rect.left}px;top:${rect.top}px;` + @@ -286,8 +305,12 @@ export function registerUIInput(inputId, submitId, babylonInput, babylonSubmit, const idx = focusable.indexOf(htmlInput); e.preventDefault(); (e.shiftKey - ? (idx > 0 ? focusable[idx - 1] : _canvas) - : (idx < focusable.length - 1 ? focusable[idx + 1] : _canvas) + ? idx > 0 + ? focusable[idx - 1] + : _canvas + : idx < focusable.length - 1 + ? focusable[idx + 1] + : _canvas )?.focus(); } }; @@ -317,7 +340,11 @@ export function registerUIInput(inputId, submitId, babylonInput, babylonSubmit, submitBtn.removeEventListener('keydown', _onTwinKeydown); submitIndicator.remove(); _phStyle.remove(); - try { babylonInput.onDisposeObservable.remove(disposeObs); } catch { /* already gone */ } + try { + babylonInput.onDisposeObservable.remove(disposeObs); + } catch { + /* already gone */ + } }, }); } @@ -360,9 +387,9 @@ export function registerUIText(id, text, posParams) { p.addEventListener('focus', () => { const rect = _computeFixedRect(posParams); if (rect) { - indicator.style.left = `${rect.left}px`; - indicator.style.top = `${rect.top}px`; - indicator.style.width = `${rect.width}px`; + indicator.style.left = `${rect.left}px`; + indicator.style.top = `${rect.top}px`; + indicator.style.width = `${rect.width}px`; indicator.style.height = `${rect.height}px`; indicator.style.display = 'block'; } diff --git a/api/animate.js b/api/animate.js index 95ad0959..8cd7d16a 100644 --- a/api/animate.js +++ b/api/animate.js @@ -388,7 +388,13 @@ export const flockAnimate = { mesh.metadata._glideObserver = null; } if (!reverse && !loop) mesh.position = endPosition.clone(); - if (isPhysicsActive && originalMotionType !== null && !loop && !reverse && isBodyAlive(mesh.physics)) { + if ( + isPhysicsActive && + originalMotionType !== null && + !loop && + !reverse && + isBodyAlive(mesh.physics) + ) { mesh.physics.setMotionType(originalMotionType); } resolve(); diff --git a/api/camera.js b/api/camera.js index f5b5989d..fc0940a2 100644 --- a/api/camera.js +++ b/api/camera.js @@ -14,7 +14,7 @@ export const flockCamera = { return new Promise((resolve) => { flock.whenModelReady(meshName, async function (mesh) { if (!mesh) { - console.log("Model not loaded:", meshName); + console.log('Model not loaded:', meshName); resolve(); return; } @@ -42,17 +42,17 @@ export const flockCamera = { const forward = new flock.BABYLON.Vector3( Math.sin(mesh.rotation.y), 0, - Math.cos(mesh.rotation.y), + Math.cos(mesh.rotation.y) ); // STEP 1: Place camera IN FRONT of character (facing them) camera = new flock.BABYLON.ArcRotateCamera( - "camera", + 'camera', Math.PI / 2, Math.PI, radius, mesh.position.add(forward.scale(3)), // in front - flock.scene, + flock.scene ); camera.lockedTarget = mesh; @@ -67,7 +67,7 @@ export const flockCamera = { camera.angularSensibilityX = 2000; camera.angularSensibilityY = 2000; camera.panningSensibility = 0; - camera.inputs.removeByType("ArcRotateCameraMouseWheelInput"); + camera.inputs.removeByType('ArcRotateCameraMouseWheelInput'); camera.inputs.attached.pointers.multiTouchPanAndZoom = false; camera.inputs.attached.pointers.multiTouchPanning = false; @@ -90,12 +90,12 @@ export const flockCamera = { savedCamera?.detachControl(); camera = new flock.BABYLON.ArcRotateCamera( - "camera", + 'camera', Math.PI / 2, Math.PI, radius, mesh.position, - flock.scene, + flock.scene ); camera.checkCollisions = true; @@ -106,7 +106,7 @@ export const flockCamera = { camera.angularSensibilityX = 2000; camera.angularSensibilityY = 2000; camera.panningSensibility = 0; - camera.inputs.removeByType("ArcRotateCameraMouseWheelInput"); + camera.inputs.removeByType('ArcRotateCameraMouseWheelInput'); camera.inputs.attached.pointers.multiTouchPanAndZoom = false; camera.inputs.attached.pointers.multiTouchPanning = false; @@ -136,43 +136,37 @@ export const flockCamera = { // --- find or create a reusable constraint box (anchor) --- let constraintBox = - (flock._constraintBox && - !flock._constraintBox.isDisposed() && - flock._constraintBox) || + (flock._constraintBox && !flock._constraintBox.isDisposed() && flock._constraintBox) || (scene.meshes || []).find( (m) => - m && - typeof m.name === "string" && - m.name.startsWith("Constraint_") && - !m.isDisposed(), + m && typeof m.name === 'string' && m.name.startsWith('Constraint_') && !m.isDisposed() ); if (!constraintBox) { // create a new hidden static box constraintBox = flock.BABYLON.MeshBuilder.CreateBox( - "Constraint", + 'Constraint', { height: 1, width: 1, depth: 1 }, - scene, + scene ); constraintBox.metadata = constraintBox.metadata || {}; constraintBox.metadata.blockKey = constraintBox.name; - constraintBox.name = constraintBox.name + "_" + constraintBox.uniqueId; + constraintBox.name = constraintBox.name + '_' + constraintBox.uniqueId; constraintBox.isVisible = false; constraintBox.material = - constraintBox.material || - new flock.BABYLON.StandardMaterial("staticMaterial", scene); + constraintBox.material || new flock.BABYLON.StandardMaterial('staticMaterial', scene); const body = new flock.BABYLON.PhysicsBody( constraintBox, flock.BABYLON.PhysicsMotionType.STATIC, false, - scene, + scene ); const shape = new flock.BABYLON.PhysicsShapeBox( flock.BABYLON.Vector3.Zero(), new flock.BABYLON.Quaternion(0, 0, 0, 1), flock.BABYLON.Vector3.One(), - scene, + scene ); body.shape = shape; body.setMassProperties({ mass: 1, restitution: 0.5 }); @@ -187,13 +181,13 @@ export const flockCamera = { constraintBox, flock.BABYLON.PhysicsMotionType.STATIC, false, - scene, + scene ); const shape = new flock.BABYLON.PhysicsShapeBox( flock.BABYLON.Vector3.Zero(), new flock.BABYLON.Quaternion(0, 0, 0, 1), flock.BABYLON.Vector3.One(), - scene, + scene ); body.shape = shape; body.setMassProperties({ mass: 1, restitution: 0.5 }); @@ -203,7 +197,7 @@ export const flockCamera = { flock.BABYLON.Vector3.Zero(), new flock.BABYLON.Quaternion(0, 0, 0, 1), flock.BABYLON.Vector3.One(), - scene, + scene ); } } @@ -223,7 +217,7 @@ export const flockCamera = { constraintBox.physics.disablePreStep = false; constraintBox.physics.setTargetTransform( constraintBox.position, - constraintBox.rotationQuaternion, + constraintBox.rotationQuaternion ); } @@ -247,7 +241,7 @@ export const flockCamera = { maxLimit: 0, }, ], - scene, + scene ); try { @@ -255,37 +249,27 @@ export const flockCamera = { mesh.metadata.constraint = true; mesh.metadata.uprightConstraint = constraint; } catch (e) { - console.warn("[ensureVerticalConstraint] addConstraint failed:", e); + console.warn('[ensureVerticalConstraint] addConstraint failed:', e); } // --- stabiliser: add only once per mesh to avoid stacking effects after swaps --- if (!mesh.metadata._uprightStabiliser) { - mesh.metadata._uprightStabiliser = scene.onAfterPhysicsObservable.add( - () => { - if ( - !mesh || - mesh.isDisposed() || - !mesh.physics || - !mesh.physics._pluginData - ) - return; - try { - // preserve Y motion; zero X/Z linear velocity - const v = mesh.physics.getLinearVelocity(); - mesh.physics.setLinearVelocity( - new flock.BABYLON.Vector3(0, v.y, 0), - ); - - mesh.physics.setAngularVelocity(new flock.BABYLON.Vector3(0, 0, 0)); - } catch (err) { - console.warn("Physics body became invalid:", err); - } - }, - ); + mesh.metadata._uprightStabiliser = scene.onAfterPhysicsObservable.add(() => { + if (!mesh || mesh.isDisposed() || !mesh.physics || !mesh.physics._pluginData) return; + try { + // preserve Y motion; zero X/Z linear velocity + const v = mesh.physics.getLinearVelocity(); + mesh.physics.setLinearVelocity(new flock.BABYLON.Vector3(0, v.y, 0)); + + mesh.physics.setAngularVelocity(new flock.BABYLON.Vector3(0, 0, 0)); + } catch (err) { + console.warn('Physics body became invalid:', err); + } + }); } }, getCamera() { - return "__active_camera__"; + return '__active_camera__'; }, _normalizeKeyCode(inputKey) { const keyMap = { @@ -293,17 +277,17 @@ export const flockCamera = { ArrowUp: 38, ArrowRight: 39, ArrowDown: 40, - " ": 32, - ",": 188, - ".": 190, - "/": 191, + ' ': 32, + ',': 188, + '.': 190, + '/': 191, }; - if (typeof inputKey === "number") { + if (typeof inputKey === 'number') { return inputKey; } - if (typeof inputKey !== "string") { + if (typeof inputKey !== 'string') { return null; } @@ -328,47 +312,47 @@ export const flockCamera = { _applyCameraBinding(camera, normalizedKey, action) { if (camera.keysRotateLeft) { switch (action) { - case "moveUp": + case 'moveUp': camera.keysUp = [normalizedKey]; break; - case "moveDown": + case 'moveDown': camera.keysDown = [normalizedKey]; break; - case "moveLeft": + case 'moveLeft': camera.keysLeft = [normalizedKey]; break; - case "moveRight": + case 'moveRight': camera.keysRight = [normalizedKey]; break; - case "rotateUp": + case 'rotateUp': camera.keysRotateUp = [normalizedKey]; break; - case "rotateDown": + case 'rotateDown': camera.keysRotateDown = [normalizedKey]; break; - case "rotateLeft": + case 'rotateLeft': camera.keysRotateLeft = [normalizedKey]; break; - case "rotateRight": + case 'rotateRight': camera.keysRotateRight = [normalizedKey]; break; } } else { switch (action) { - case "rotateLeft": - case "moveLeft": + case 'rotateLeft': + case 'moveLeft': camera.keysLeft = [normalizedKey]; break; - case "rotateRight": - case "moveRight": + case 'rotateRight': + case 'moveRight': camera.keysRight = [normalizedKey]; break; - case "moveUp": - case "rotateUp": + case 'moveUp': + case 'rotateUp': camera.keysUp = [normalizedKey]; break; - case "moveDown": - case "rotateDown": + case 'moveDown': + case 'rotateDown': camera.keysDown = [normalizedKey]; break; } @@ -383,22 +367,20 @@ export const flockCamera = { cameraControl(key, action) { const normalizedKey = this._normalizeKeyCode(key); if (normalizedKey == null) { - console.warn("Unsupported camera control key:", key); + console.warn('Unsupported camera control key:', key); return; } if (!flock._cameraControlBindings) { flock._cameraControlBindings = []; } - flock._cameraControlBindings = flock._cameraControlBindings.filter( - (b) => b.action !== action, - ); + flock._cameraControlBindings = flock._cameraControlBindings.filter((b) => b.action !== action); flock._cameraControlBindings.push({ normalizedKey, action }); if (flock.scene.activeCamera) { this._applyCameraBinding(flock.scene.activeCamera, normalizedKey, action); } else { - console.error("No active camera found in the scene."); + console.error('No active camera found in the scene.'); } }, }; diff --git a/api/control.js b/api/control.js index e15d4cda..ddd0df5a 100644 --- a/api/control.js +++ b/api/control.js @@ -17,7 +17,7 @@ export const flockControl = { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { if (flock.abortController?.signal) { - flock.abortController.signal.removeEventListener("abort", onAbort); + flock.abortController.signal.removeEventListener('abort', onAbort); } resolve(); }, ms); @@ -25,18 +25,18 @@ export const flockControl = { const onAbort = () => { clearTimeout(timeoutId); // Clear the timeout if aborted if (flock.abortController?.signal) { - flock.abortController.signal.removeEventListener("abort", onAbort); + flock.abortController.signal.removeEventListener('abort', onAbort); } // Instead of throwing an error, resolve gracefully here - reject(new Error("Wait aborted")); + reject(new Error('Wait aborted')); }; if (flock.abortController?.signal) { - flock.abortController.signal.addEventListener("abort", onAbort); + flock.abortController.signal.addEventListener('abort', onAbort); } }).catch((error) => { // Check if the error is the expected "Wait aborted" error and handle it - if (error.message === "Wait aborted") { + if (error.message === 'Wait aborted') { return; } // If it's another error, rethrow it @@ -48,7 +48,7 @@ export const flockControl = { loopBody, chunkSize = 100, timing = { lastFrameTime: performance.now() }, - state = {}, + state = {} ) { if (state.stopExecution) return; // Check if we should stop further iterations if (flock.abortController?.signal?.aborted) return; @@ -67,8 +67,8 @@ export const flockControl = { } }, waitUntil(conditionFunc) { - if (typeof conditionFunc !== "function") { - console.warn("waitUntil: conditionFunc must be a function"); + if (typeof conditionFunc !== 'function') { + console.warn('waitUntil: conditionFunc must be a function'); return Promise.resolve(); } const signal = flock.abortController?.signal; @@ -97,12 +97,12 @@ export const flockControl = { const observer = flock.scene.onBeforeRenderObservable.add(checkCondition); signal?.addEventListener( - "abort", + 'abort', () => { flock.scene?.onBeforeRenderObservable?.remove(observer); resolve(); }, - { once: true }, + { once: true } ); }); }, diff --git a/api/csg.js b/api/csg.js index 5f5ce287..cb433d44 100644 --- a/api/csg.js +++ b/api/csg.js @@ -16,8 +16,7 @@ function prepareMeshForCSG(mesh) { // Check if mesh itself has valid geometry using multiple methods const hasPositions = - mesh.getVerticesData && - mesh.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind); + mesh.getVerticesData && mesh.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind); const hasIndices = mesh.getIndices && mesh.getIndices(); // If mesh has positions but no indices, it might need conversion @@ -27,10 +26,7 @@ function prepareMeshForCSG(mesh) { mesh.convertToUnIndexedMesh(); mesh.forceSharedVertices(); } catch (error) { - console.warn( - "[prepareMeshForCSG] Failed to convert mesh geometry:", - error, - ); + console.warn('[prepareMeshForCSG] Failed to convert mesh geometry:', error); } } @@ -49,10 +45,8 @@ function prepareMeshForCSG(mesh) { // Mesh has no geometry, check for children with geometry const children = mesh.getChildMeshes ? mesh.getChildMeshes(false) : []; const meshesWithGeometry = children.filter((child) => { - const childHasVertices = - child.getTotalVertices && child.getTotalVertices() > 0; - const childHasIndices = - child.getIndices && child.getIndices() && child.getIndices().length > 0; + const childHasVertices = child.getTotalVertices && child.getTotalVertices() > 0; + const childHasIndices = child.getIndices && child.getIndices() && child.getIndices().length > 0; return childHasVertices && childHasIndices; }); @@ -60,8 +54,7 @@ function prepareMeshForCSG(mesh) { if (meshesWithGeometry.length === 0) { const childrenWithPositions = children.filter((child) => { const positions = - child.getVerticesData && - child.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind); + child.getVerticesData && child.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind); return positions && positions.length > 0; }); @@ -72,27 +65,18 @@ function prepareMeshForCSG(mesh) { if (!child.getIndices() || child.getIndices().length === 0) { child.forceSharedVertices(); } - if ( - child.getTotalVertices() > 0 && - child.getIndices() && - child.getIndices().length > 0 - ) { + if (child.getTotalVertices() > 0 && child.getIndices() && child.getIndices().length > 0) { meshesWithGeometry.push(child); } } catch (error) { - console.warn( - "[prepareMeshForCSG] Failed to process child mesh geometry:", - error, - ); + console.warn('[prepareMeshForCSG] Failed to process child mesh geometry:', error); } }); } } if (meshesWithGeometry.length === 0) { - console.warn( - `[prepareMeshForCSG] No valid geometry found for mesh: ${mesh.name}`, - ); + console.warn(`[prepareMeshForCSG] No valid geometry found for mesh: ${mesh.name}`); return null; } @@ -121,7 +105,7 @@ function prepareMeshForCSG(mesh) { true, // allow32BitsIndices undefined, false, - true, + true ); if (merged) { @@ -162,11 +146,7 @@ function recenterMeshLocalOrigin(mesh) { if (!worldCenter) return; mesh.bakeTransformIntoVertices( - flock.BABYLON.Matrix.Translation( - -worldCenter.x, - -worldCenter.y, - -worldCenter.z, - ), + flock.BABYLON.Matrix.Translation(-worldCenter.x, -worldCenter.y, -worldCenter.z) ); mesh.position.copyFrom(worldCenter); mesh.computeWorldMatrix?.(true); @@ -174,8 +154,7 @@ function recenterMeshLocalOrigin(mesh) { } function applyBoxProjectionUV(mesh, uvScale = 1) { - if (!mesh?.getVerticesData || typeof flock.setSizeBasedBoxUVs !== "function") - return; + if (!mesh?.getVerticesData || typeof flock.setSizeBasedBoxUVs !== 'function') return; // Ensure per-face UV projection behaves predictably: // indexed meshes share vertices across hard edges, which can blend normals @@ -186,17 +165,13 @@ function applyBoxProjectionUV(mesh, uvScale = 1) { currentIndices && currentIndices.length > 0 && currentIndices.length !== - (mesh.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind)?.length || - 0) / - 3 && - typeof mesh.convertToUnIndexedMesh === "function" + (mesh.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind)?.length || 0) / 3 && + typeof mesh.convertToUnIndexedMesh === 'function' ) { mesh.convertToUnIndexedMesh(); } - const positions = mesh.getVerticesData( - flock.BABYLON.VertexBuffer.PositionKind, - ); + const positions = mesh.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind); if (!positions || positions.length === 0) return; const indices = mesh.getIndices ? mesh.getIndices() : null; @@ -265,15 +240,15 @@ function hasUsableUVs(mesh) { } function shouldApplyBoxProjection(resultMesh, options = {}) { - if (options.uvProjection === "box") return true; - if (options.uvProjection !== "auto") return false; + if (options.uvProjection === 'box') return true; + if (options.uvProjection !== 'auto') return false; const hasRenderableTexture = (texture) => { if (!texture) return false; - const textureName = String(texture.name || "").toLowerCase(); + const textureName = String(texture.name || '').toLowerCase(); if (!textureName) return false; - if (textureName.endsWith("undefined")) return false; - if (textureName.includes("none.png")) return false; + if (textureName.endsWith('undefined')) return false; + if (textureName.includes('none.png')) return false; return true; }; @@ -315,13 +290,10 @@ function normalizeMeshAttributesForMerge(meshes, { logWarning = true } = {}) { if (!normalizationRequired) return; if (logWarning) { - console.warn( - "[mergeMeshes] Normalizing vertex attributes before fallback merge", - { - meshes: getMeshKindsSummary(meshes), - requiredKinds: Array.from(kindUnion), - }, - ); + console.warn('[mergeMeshes] Normalizing vertex attributes before fallback merge', { + meshes: getMeshKindsSummary(meshes), + requiredKinds: Array.from(kindUnion), + }); } const vertexBuffer = flock.BABYLON.VertexBuffer; @@ -380,9 +352,7 @@ function normalizeMeshAttributesForMerge(meshes, { logWarning = true } = {}) { function hasNonFinitePositions(mesh) { if (!mesh?.getVerticesData) return true; - const positions = mesh.getVerticesData( - flock.BABYLON.VertexBuffer.PositionKind, - ); + const positions = mesh.getVerticesData(flock.BABYLON.VertexBuffer.PositionKind); if (!positions || positions.length === 0) return true; for (let i = 0; i < positions.length; i++) { if (!Number.isFinite(positions[i])) return true; @@ -404,20 +374,13 @@ function sanitizeMeshVertexDataForCSG(mesh) { const positionKind = flock.BABYLON.VertexBuffer.PositionKind; const normalKind = flock.BABYLON.VertexBuffer.NormalKind; const positions = mesh.getVerticesData(positionKind); - if ( - !positions || - positions.length === 0 || - arrayHasNonFiniteValues(positions) - ) { + if (!positions || positions.length === 0 || arrayHasNonFiniteValues(positions)) { return false; } const kinds = mesh.getVerticesDataKinds() || []; let indices = mesh.getIndices ? mesh.getIndices() : null; - if ( - (!indices || indices.length === 0) && - typeof mesh.setIndices === "function" - ) { + if ((!indices || indices.length === 0) && typeof mesh.setIndices === 'function') { indices = Array.from({ length: positions.length / 3 }, (_, i) => i); mesh.setIndices(indices); } @@ -447,8 +410,8 @@ function resolveCsgModelIdentity(requestedModelId) { let resolvedModelId = requestedModelId; let blockKey = requestedModelId; - if (typeof requestedModelId === "string" && requestedModelId.includes("__")) { - const separatorIndex = requestedModelId.lastIndexOf("__"); + if (typeof requestedModelId === 'string' && requestedModelId.includes('__')) { + const separatorIndex = requestedModelId.lastIndexOf('__'); resolvedModelId = requestedModelId.slice(0, separatorIndex); blockKey = requestedModelId.slice(separatorIndex + 2); } @@ -463,29 +426,25 @@ export const flockCSG = { if (!Array.isArray(meshes) || meshes.length === 0) return false; return meshes.some((meshOrName) => { const raw = - typeof meshOrName === "string" + typeof meshOrName === 'string' ? meshOrName - : meshOrName?.name || meshOrName?.metadata?.modelName || ""; + : meshOrName?.name || meshOrName?.metadata?.modelName || ''; const name = raw.toLowerCase(); const modelName = - typeof meshOrName === "string" - ? "" - : meshOrName?.metadata?.modelName?.toLowerCase?.() || ""; - return ( - name.includes("3dtext") || - modelName.includes("3dtext") || - modelName.includes("text") - ); + typeof meshOrName === 'string' + ? '' + : meshOrName?.metadata?.modelName?.toLowerCase?.() || ''; + return name.includes('3dtext') || modelName.includes('3dtext') || modelName.includes('text'); }); }, toolMeshesUseTextures(meshes) { if (!Array.isArray(meshes) || meshes.length === 0) return false; const hasRenderableTexture = (texture) => { if (!texture) return false; - const textureName = String(texture.name || "").toLowerCase(); + const textureName = String(texture.name || '').toLowerCase(); if (!textureName) return false; - if (textureName.endsWith("undefined")) return false; - if (textureName.includes("none.png")) return false; + if (textureName.endsWith('undefined')) return false; + if (textureName.includes('none.png')) return false; return true; }; const materialHasTexture = (material) => { @@ -503,9 +462,7 @@ export const flockCSG = { return meshes.some((mesh) => { if (materialHasTexture(mesh?.material)) return true; if (!mesh?.getChildMeshes) return false; - return mesh - .getChildMeshes() - .some((child) => materialHasTexture(child?.material)); + return mesh.getChildMeshes().some((child) => materialHasTexture(child?.material)); }); }, mergeCompositeMesh(meshes) { @@ -518,228 +475,200 @@ export const flockCSG = { true, // allow32BitsIndices null, // meshSubclass false, // subdivideWithSubMeshes (Set to false for CSG) - true, // multiMultiMaterials + true // multiMultiMaterials ); return merged; }, mergeMeshes(modelId, meshList) { - const { modelId: resolvedModelId, blockKey } = - resolveCsgModelIdentity(modelId); + const { modelId: resolvedModelId, blockKey } = resolveCsgModelIdentity(modelId); modelId = resolvedModelId; - return flock - .prepareMeshes(modelId, meshList, blockKey) - .then((validMeshes) => { - if (validMeshes.length) { - const meshesToMerge = []; - let referenceMesh = validMeshes[0]; + return flock.prepareMeshes(modelId, meshList, blockKey).then((validMeshes) => { + if (validMeshes.length) { + const meshesToMerge = []; + let referenceMesh = validMeshes[0]; - validMeshes.forEach((mesh) => { - let targetMesh = mesh; - - if (mesh.metadata?.modelName) { - const meshWithMaterial = - flock._findFirstDescendantWithMaterial(mesh); - if (meshWithMaterial) { - targetMesh = meshWithMaterial; - targetMesh.refreshBoundingInfo(); - } + validMeshes.forEach((mesh) => { + let targetMesh = mesh; + + if (mesh.metadata?.modelName) { + const meshWithMaterial = flock._findFirstDescendantWithMaterial(mesh); + if (meshWithMaterial) { + targetMesh = meshWithMaterial; + targetMesh.refreshBoundingInfo(); } + } - targetMesh = prepareMeshForCSG(targetMesh); - if (!targetMesh) return; + targetMesh = prepareMeshForCSG(targetMesh); + if (!targetMesh) return; - targetMesh.computeWorldMatrix(true); + targetMesh.computeWorldMatrix(true); - if (targetMesh.getTotalVertices() > 0) { - meshesToMerge.push(targetMesh); - } - }); + if (targetMesh.getTotalVertices() > 0) { + meshesToMerge.push(targetMesh); + } + }); - if (meshesToMerge.length === 0) return null; + if (meshesToMerge.length === 0) return null; - if (meshesToMerge.length === 1) { - const singleMesh = meshesToMerge[0]; - let mergedMesh = singleMesh.clone(modelId); - if (!mergedMesh) mergedMesh = singleMesh; + if (meshesToMerge.length === 1) { + const singleMesh = meshesToMerge[0]; + let mergedMesh = singleMesh.clone(modelId); + if (!mergedMesh) mergedMesh = singleMesh; - mergedMesh.name = modelId; - mergedMesh.metadata = mergedMesh.metadata || {}; - mergedMesh.metadata.blockKey = blockKey; - mergedMesh.metadata.sharedMaterial = false; + mergedMesh.name = modelId; + mergedMesh.metadata = mergedMesh.metadata || {}; + mergedMesh.metadata.blockKey = blockKey; + mergedMesh.metadata.sharedMaterial = false; - return modelId; - } + return modelId; + } - const originalMaterial = referenceMesh.material; - let mergedMesh = null; - let csgSucceeded = false; + const originalMaterial = referenceMesh.material; + let mergedMesh = null; + let csgSucceeded = false; - normalizeMeshAttributesForMerge(meshesToMerge, { logWarning: false }); + normalizeMeshAttributesForMerge(meshesToMerge, { logWarning: false }); - const csgUnsafe = meshesToMerge.some((mesh) => { - const positionsFinite = !hasNonFinitePositions(mesh); - if (!positionsFinite) return true; - return !sanitizeMeshVertexDataForCSG(mesh); - }); + const csgUnsafe = meshesToMerge.some((mesh) => { + const positionsFinite = !hasNonFinitePositions(mesh); + if (!positionsFinite) return true; + return !sanitizeMeshVertexDataForCSG(mesh); + }); - if (!csgUnsafe) { - try { - let currentMesh = meshesToMerge[0]; - - for (let i = 1; i < meshesToMerge.length; i++) { - const nextMesh = meshesToMerge[i]; - - // Perform pairwise normalization to prevent property mismatch - const pair = [currentMesh, nextMesh]; - normalizeMeshAttributesForMerge(pair, { logWarning: false }); - - const leftCSG = flock.BABYLON.CSG2.FromMesh(currentMesh, false); - const rightCSG = flock.BABYLON.CSG2.FromMesh(nextMesh, false); - const combinedCSG = leftCSG.add(rightCSG); - - const oldIntermediate = currentMesh; - - // Convert back to mesh to "bake" geometry before the next addition - currentMesh = combinedCSG.toMesh( - "temp_step_" + i, - referenceMesh.getScene(), - { - centerMesh: false, - rebuildNormals: true, - }, - ); - - // Dispose the intermediate mesh if it's not part of the original source list - if (i > 1 || oldIntermediate !== meshesToMerge[0]) { - oldIntermediate.dispose(); - } - } + if (!csgUnsafe) { + try { + let currentMesh = meshesToMerge[0]; - mergedMesh = currentMesh; + for (let i = 1; i < meshesToMerge.length; i++) { + const nextMesh = meshesToMerge[i]; - if (mergedMesh && mergedMesh.getTotalVertices() > 0) { - csgSucceeded = true; - } else { - if (mergedMesh) mergedMesh.dispose(); - mergedMesh = null; - } - } catch (error) { - const emptyMeshes = flock.scene.meshes.filter( - (m) => m.name === modelId && m.getTotalVertices() === 0, - ); - emptyMeshes.forEach((m) => m.dispose()); - - if ( - !String(error?.message || "").includes( - "same number of properties", - ) || - flock?.materialsDebug - ) { - console.warn( - "[mergeMeshes] Pairwise CSG attempt failed:", - error, - ); + // Perform pairwise normalization to prevent property mismatch + const pair = [currentMesh, nextMesh]; + normalizeMeshAttributesForMerge(pair, { logWarning: false }); + + const leftCSG = flock.BABYLON.CSG2.FromMesh(currentMesh, false); + const rightCSG = flock.BABYLON.CSG2.FromMesh(nextMesh, false); + const combinedCSG = leftCSG.add(rightCSG); + + const oldIntermediate = currentMesh; + + // Convert back to mesh to "bake" geometry before the next addition + currentMesh = combinedCSG.toMesh('temp_step_' + i, referenceMesh.getScene(), { + centerMesh: false, + rebuildNormals: true, + }); + + // Dispose the intermediate mesh if it's not part of the original source list + if (i > 1 || oldIntermediate !== meshesToMerge[0]) { + oldIntermediate.dispose(); } - csgSucceeded = false; } - } - if (!csgSucceeded) { - try { - normalizeMeshAttributesForMerge(meshesToMerge, { - logWarning: false, - }); - mergedMesh = flock.BABYLON.Mesh.MergeMeshes( - meshesToMerge, - false, - true, - undefined, - true, - true, - ); - } catch (mergeError) { - console.warn( - "[mergeMeshes] Mesh.MergeMeshes fallback failed:", - mergeError, - ); - return null; + mergedMesh = currentMesh; + + if (mergedMesh && mergedMesh.getTotalVertices() > 0) { + csgSucceeded = true; + } else { + if (mergedMesh) mergedMesh.dispose(); + mergedMesh = null; + } + } catch (error) { + const emptyMeshes = flock.scene.meshes.filter( + (m) => m.name === modelId && m.getTotalVertices() === 0 + ); + emptyMeshes.forEach((m) => m.dispose()); + + if ( + !String(error?.message || '').includes('same number of properties') || + flock?.materialsDebug + ) { + console.warn('[mergeMeshes] Pairwise CSG attempt failed:', error); } + csgSucceeded = false; } + } - if (!mergedMesh) return null; + if (!csgSucceeded) { + try { + normalizeMeshAttributesForMerge(meshesToMerge, { + logWarning: false, + }); + mergedMesh = flock.BABYLON.Mesh.MergeMeshes( + meshesToMerge, + false, + true, + undefined, + true, + true + ); + } catch (mergeError) { + console.warn('[mergeMeshes] Mesh.MergeMeshes fallback failed:', mergeError); + return null; + } + } - recenterMeshLocalOrigin(mergedMesh); + if (!mergedMesh) return null; - mergedMesh.name = modelId; - mergedMesh.metadata = mergedMesh.metadata || {}; - mergedMesh.metadata.blockKey = blockKey; - mergedMesh.metadata.sharedMaterial = false; + recenterMeshLocalOrigin(mergedMesh); + + mergedMesh.name = modelId; + mergedMesh.metadata = mergedMesh.metadata || {}; + mergedMesh.metadata.blockKey = blockKey; + mergedMesh.metadata.sharedMaterial = false; - const isDefaultMaterial = (material) => { - return ( - material instanceof flock.BABYLON.StandardMaterial && - material.name === "default material" + const isDefaultMaterial = (material) => { + return ( + material instanceof flock.BABYLON.StandardMaterial && + material.name === 'default material' + ); + }; + + if (mergedMesh.material) { + if (mergedMesh.material instanceof flock.BABYLON.MultiMaterial) { + mergedMesh.material.subMaterials = mergedMesh.material.subMaterials.map( + (subMaterial) => { + if (subMaterial && isDefaultMaterial(subMaterial) && originalMaterial) { + const replacement = originalMaterial.clone(modelId + '_material'); + replacement.backFaceCulling = false; + return replacement; + } + return subMaterial; + } ); - }; - - if (mergedMesh.material) { - if (mergedMesh.material instanceof flock.BABYLON.MultiMaterial) { - mergedMesh.material.subMaterials = - mergedMesh.material.subMaterials.map((subMaterial) => { - if ( - subMaterial && - isDefaultMaterial(subMaterial) && - originalMaterial - ) { - const replacement = originalMaterial.clone( - modelId + "_material", - ); - replacement.backFaceCulling = false; - return replacement; - } - return subMaterial; - }); - } else if ( - isDefaultMaterial(mergedMesh.material) && - originalMaterial - ) { - const newMat = originalMaterial.clone(modelId + "_material"); - newMat.backFaceCulling = false; - mergedMesh.material = newMat; - } - } else if (originalMaterial) { - const newMat = originalMaterial.clone(modelId + "_material"); + } else if (isDefaultMaterial(mergedMesh.material) && originalMaterial) { + const newMat = originalMaterial.clone(modelId + '_material'); newMat.backFaceCulling = false; mergedMesh.material = newMat; } + } else if (originalMaterial) { + const newMat = originalMaterial.clone(modelId + '_material'); + newMat.backFaceCulling = false; + mergedMesh.material = newMat; + } - mergedMesh.createNormals(true); + mergedMesh.createNormals(true); - try { - const physicsShape = new flock.BABYLON.PhysicsShapeMesh( - mergedMesh, - flock.scene, - ); - flock.applyPhysics(mergedMesh, physicsShape); - } catch (e) { - console.warn("Suppressed non-critical error:", e); - } + try { + const physicsShape = new flock.BABYLON.PhysicsShapeMesh(mergedMesh, flock.scene); + flock.applyPhysics(mergedMesh, physicsShape); + } catch (e) { + console.warn('Suppressed non-critical error:', e); + } - validMeshes.forEach((mesh) => { - if (mesh !== mergedMesh) mesh.dispose(); - }); + validMeshes.forEach((mesh) => { + if (mesh !== mergedMesh) mesh.dispose(); + }); - return modelId; - } else { - return null; - } - }); + return modelId; + } else { + return null; + } + }); }, subtractMeshesMerge(modelId, baseMeshName, meshNames, options = {}) { - const { modelId: resolvedModelId, blockKey } = - resolveCsgModelIdentity(modelId); + const { modelId: resolvedModelId, blockKey } = resolveCsgModelIdentity(modelId); modelId = resolvedModelId; const collectMaterialMeshesDeep = (root) => { @@ -748,12 +677,7 @@ export const flockCSG = { while (stack.length) { const node = stack.pop(); if (!node) continue; - if ( - node.getTotalVertices && - node.getTotalVertices() > 0 && - node.material - ) - out.push(node); + if (node.getTotalVertices && node.getTotalVertices() > 0 && node.material) out.push(node); const kids = node.getChildren ? node.getChildren() : []; for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]); } @@ -793,149 +717,115 @@ export const flockCSG = { // Ensure base mesh has valid geometry for CSG actualBase = prepareMeshForCSG(actualBase); if (!actualBase) { - console.warn( - "[subtractMeshesMerge] Base mesh has no valid geometry for CSG.", - ); + console.warn('[subtractMeshesMerge] Base mesh has no valid geometry for CSG.'); return resolve(null); } - flock - .prepareMeshes(modelId, meshNames, blockKey) - .then((validMeshes) => { - const inferredUvProjection = - options.uvProjection === undefined && - flock.toolMeshesUseTextures(validMeshes) - ? "auto" - : options.uvProjection; - const scene = baseMesh.getScene(); - const baseDuplicate = cloneForCSG(actualBase, "baseDuplicate"); - let outerCSG = flock.BABYLON.CSG2.FromMesh(baseDuplicate, false); - const subtractDuplicates = []; - - validMeshes.forEach((mesh, meshIndex) => { - const parts = collectMaterialMeshesDeep(mesh); - - // Check if mesh itself has valid geometry (e.g., manifold text meshes) - const meshHasGeometry = - mesh.getTotalVertices && mesh.getTotalVertices() > 0; - - if (parts.length > 0) { - const partClones = parts.map((p, i) => - cloneForCSG(p, `temp_${meshIndex}_${i}`), - ); - const isDonut = - mesh.name.toLowerCase().includes("donut") || - mesh.metadata?.modelName?.toLowerCase().includes("donut"); - - if (isDonut) { - partClones.forEach((pc) => subtractDuplicates.push(pc)); - } else { - let unified = - partClones.length > 1 - ? flock.BABYLON.Mesh.MergeMeshes( - partClones, - true, - true, - undefined, - false, - true, - ) - : partClones[0]; - if (unified) { - unified.forceSharedVertices(); - if ( - mesh.metadata?.modelName && - typeof unified.flipFaces === "function" - ) - unified.flipFaces(); - subtractDuplicates.push(unified); - } + flock.prepareMeshes(modelId, meshNames, blockKey).then((validMeshes) => { + const inferredUvProjection = + options.uvProjection === undefined && flock.toolMeshesUseTextures(validMeshes) + ? 'auto' + : options.uvProjection; + const scene = baseMesh.getScene(); + const baseDuplicate = cloneForCSG(actualBase, 'baseDuplicate'); + let outerCSG = flock.BABYLON.CSG2.FromMesh(baseDuplicate, false); + const subtractDuplicates = []; + + validMeshes.forEach((mesh, meshIndex) => { + const parts = collectMaterialMeshesDeep(mesh); + + // Check if mesh itself has valid geometry (e.g., manifold text meshes) + const meshHasGeometry = mesh.getTotalVertices && mesh.getTotalVertices() > 0; + + if (parts.length > 0) { + const partClones = parts.map((p, i) => cloneForCSG(p, `temp_${meshIndex}_${i}`)); + const isDonut = + mesh.name.toLowerCase().includes('donut') || + mesh.metadata?.modelName?.toLowerCase().includes('donut'); + + if (isDonut) { + partClones.forEach((pc) => subtractDuplicates.push(pc)); + } else { + let unified = + partClones.length > 1 + ? flock.BABYLON.Mesh.MergeMeshes(partClones, true, true, undefined, false, true) + : partClones[0]; + if (unified) { + unified.forceSharedVertices(); + if (mesh.metadata?.modelName && typeof unified.flipFaces === 'function') + unified.flipFaces(); + subtractDuplicates.push(unified); } - } else if (meshHasGeometry) { - // Direct mesh without children (e.g., manifold text mesh) - const clone = cloneForCSG(mesh, `direct_tool_${meshIndex}`); - subtractDuplicates.push(clone); - } - }); - - subtractDuplicates.forEach((m, idx) => { - try { - const meshCSG = flock.BABYLON.CSG2.FromMesh(m, false); - outerCSG = outerCSG.subtract(meshCSG); - } catch (e) { - console.warn( - `[subtractMeshesMerge] Subtraction ${idx} failed:`, - e.message, - ); } - }); + } else if (meshHasGeometry) { + // Direct mesh without children (e.g., manifold text mesh) + const clone = cloneForCSG(mesh, `direct_tool_${meshIndex}`); + subtractDuplicates.push(clone); + } + }); - let resultMesh; + subtractDuplicates.forEach((m, idx) => { try { - resultMesh = outerCSG.toMesh("resultMesh", scene, { - centerMesh: false, - }); - - if (!resultMesh || resultMesh.getTotalVertices() === 0) { - throw new Error("CSG produced empty mesh"); - } + const meshCSG = flock.BABYLON.CSG2.FromMesh(m, false); + outerCSG = outerCSG.subtract(meshCSG); } catch (e) { - console.warn( - "[subtractMeshesMerge] CSG subtract failed:", - e.message, - ); - console.warn( - "[subtractMeshesMerge] Note: CSG operations require watertight (manifold) geometry. 3D text and merged meshes are typically non-manifold.", - ); - - // Clean up any empty meshes - flock.scene.meshes - .filter( - (m) => m.name === "resultMesh" && m.getTotalVertices() === 0, - ) - .forEach((m) => m.dispose()); - - baseDuplicate.dispose(); - subtractDuplicates.forEach((m) => m.dispose()); - return resolve(null); + console.warn(`[subtractMeshesMerge] Subtraction ${idx} failed:`, e.message); } + }); - resultMesh.position.set(0, 0, 0); - resultMesh.rotation.set(0, 0, 0); - resultMesh.scaling.set(1, 1, 1); - resultMesh.computeWorldMatrix(true); - flock.applyResultMeshProperties( - resultMesh, - actualBase, - modelId, - blockKey, - { - forceReferenceMaterial: options.forceReferenceMaterial === true, - flattenNonReferenceSubMaterials: - options.flattenNonReferenceSubMaterials === true, - }, - ); - if ( - shouldApplyBoxProjection(resultMesh, { - ...options, - uvProjection: inferredUvProjection, - }) - ) { - applyBoxProjectionUV(resultMesh, options.uvScale); + let resultMesh; + try { + resultMesh = outerCSG.toMesh('resultMesh', scene, { + centerMesh: false, + }); + + if (!resultMesh || resultMesh.getTotalVertices() === 0) { + throw new Error('CSG produced empty mesh'); } + } catch (e) { + console.warn('[subtractMeshesMerge] CSG subtract failed:', e.message); + console.warn( + '[subtractMeshesMerge] Note: CSG operations require watertight (manifold) geometry. 3D text and merged meshes are typically non-manifold.' + ); + + // Clean up any empty meshes + flock.scene.meshes + .filter((m) => m.name === 'resultMesh' && m.getTotalVertices() === 0) + .forEach((m) => m.dispose()); baseDuplicate.dispose(); subtractDuplicates.forEach((m) => m.dispose()); - baseMesh.dispose(); - validMeshes.forEach((m) => m.dispose()); - resolve(modelId); + return resolve(null); + } + + resultMesh.position.set(0, 0, 0); + resultMesh.rotation.set(0, 0, 0); + resultMesh.scaling.set(1, 1, 1); + resultMesh.computeWorldMatrix(true); + flock.applyResultMeshProperties(resultMesh, actualBase, modelId, blockKey, { + forceReferenceMaterial: options.forceReferenceMaterial === true, + flattenNonReferenceSubMaterials: options.flattenNonReferenceSubMaterials === true, }); + if ( + shouldApplyBoxProjection(resultMesh, { + ...options, + uvProjection: inferredUvProjection, + }) + ) { + applyBoxProjectionUV(resultMesh, options.uvScale); + } + + baseDuplicate.dispose(); + subtractDuplicates.forEach((m) => m.dispose()); + baseMesh.dispose(); + validMeshes.forEach((m) => m.dispose()); + resolve(modelId); + }); }); }); }, subtractMeshesIndividual(modelId, baseMeshName, meshNames, options = {}) { - const { modelId: resolvedModelId, blockKey } = - resolveCsgModelIdentity(modelId); + const { modelId: resolvedModelId, blockKey } = resolveCsgModelIdentity(modelId); modelId = resolvedModelId; const collectMaterialMeshesDeep = (root) => { @@ -944,12 +834,7 @@ export const flockCSG = { while (stack.length) { const node = stack.pop(); if (!node) continue; - if ( - node.getTotalVertices && - node.getTotalVertices() > 0 && - node.material - ) - out.push(node); + if (node.getTotalVertices && node.getTotalVertices() > 0 && node.material) out.push(node); const kids = node.getChildren ? node.getChildren() : []; for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]); } @@ -961,403 +846,315 @@ export const flockCSG = { if (!baseMesh) return resolve(null); let actualBase = baseMesh; if (baseMesh.metadata?.modelName) { - const meshWithMaterial = - flock._findFirstDescendantWithMaterial(baseMesh); + const meshWithMaterial = flock._findFirstDescendantWithMaterial(baseMesh); if (meshWithMaterial) actualBase = meshWithMaterial; } // Ensure base mesh has valid geometry for CSG actualBase = prepareMeshForCSG(actualBase); if (!actualBase) { - console.warn( - "[subtractMeshesIndividual] Base mesh has no valid geometry for CSG.", - ); + console.warn('[subtractMeshesIndividual] Base mesh has no valid geometry for CSG.'); return resolve(null); } - flock - .prepareMeshes(modelId, meshNames, blockKey) - .then((validMeshes) => { - const inferredUvProjection = - options.uvProjection === undefined && - flock.toolMeshesUseTextures(validMeshes) - ? "auto" - : options.uvProjection; - const scene = baseMesh.getScene(); - const baseDuplicate = actualBase.clone("baseDuplicate"); - baseDuplicate.setParent(null); - baseDuplicate.position = actualBase.getAbsolutePosition().clone(); - baseDuplicate.rotationQuaternion = null; - baseDuplicate.rotation = actualBase.absoluteRotationQuaternion - ? actualBase.absoluteRotationQuaternion.toEulerAngles() - : actualBase.rotation.clone(); - baseDuplicate.computeWorldMatrix(true); - - let outerCSG = flock.BABYLON.CSG2.FromMesh(baseDuplicate, false); - const allToolParts = []; - validMeshes.forEach((mesh) => { - const parts = collectMaterialMeshesDeep(mesh); - parts.forEach((p) => { - const dup = p.clone("partDup", null, true); - dup.computeWorldMatrix(true); - if (typeof dup.flipFaces === "function") dup.flipFaces(); - allToolParts.push(dup); - }); - }); - - allToolParts.forEach((part) => { - try { - const partCSG = flock.BABYLON.CSG2.FromMesh(part, false); - outerCSG = outerCSG.subtract(partCSG); - } catch (e) { - console.warn(e); - } + flock.prepareMeshes(modelId, meshNames, blockKey).then((validMeshes) => { + const inferredUvProjection = + options.uvProjection === undefined && flock.toolMeshesUseTextures(validMeshes) + ? 'auto' + : options.uvProjection; + const scene = baseMesh.getScene(); + const baseDuplicate = actualBase.clone('baseDuplicate'); + baseDuplicate.setParent(null); + baseDuplicate.position = actualBase.getAbsolutePosition().clone(); + baseDuplicate.rotationQuaternion = null; + baseDuplicate.rotation = actualBase.absoluteRotationQuaternion + ? actualBase.absoluteRotationQuaternion.toEulerAngles() + : actualBase.rotation.clone(); + baseDuplicate.computeWorldMatrix(true); + + let outerCSG = flock.BABYLON.CSG2.FromMesh(baseDuplicate, false); + const allToolParts = []; + validMeshes.forEach((mesh) => { + const parts = collectMaterialMeshesDeep(mesh); + parts.forEach((p) => { + const dup = p.clone('partDup', null, true); + dup.computeWorldMatrix(true); + if (typeof dup.flipFaces === 'function') dup.flipFaces(); + allToolParts.push(dup); }); + }); - let resultMesh; + allToolParts.forEach((part) => { try { - resultMesh = outerCSG.toMesh("resultMesh", scene, { - centerMesh: false, - }); - - if (!resultMesh || resultMesh.getTotalVertices() === 0) { - throw new Error("CSG produced empty mesh"); - } + const partCSG = flock.BABYLON.CSG2.FromMesh(part, false); + outerCSG = outerCSG.subtract(partCSG); } catch (e) { - console.warn( - "[subtractMeshesIndividual] CSG subtract failed:", - e.message, - ); - console.warn( - "[subtractMeshesIndividual] Note: CSG operations require watertight (manifold) geometry. 3D text and merged meshes are typically non-manifold.", - ); - - // Clean up any empty meshes - flock.scene.meshes - .filter( - (m) => m.name === "resultMesh" && m.getTotalVertices() === 0, - ) - .forEach((m) => m.dispose()); - - baseDuplicate.dispose(); - allToolParts.forEach((t) => t.dispose()); - return resolve(null); + console.warn(e); } + }); - const localCenter = resultMesh - .getBoundingInfo() - .boundingBox.center.clone(); - resultMesh.setPivotMatrix( - flock.BABYLON.Matrix.Translation( - localCenter.x, - localCenter.y, - localCenter.z, - ), - false, - ); - resultMesh.position.subtractInPlace(localCenter); - resultMesh.computeWorldMatrix(true); - flock.applyResultMeshProperties( - resultMesh, - actualBase, - modelId, - blockKey, - { - forceReferenceMaterial: options.forceReferenceMaterial === true, - flattenNonReferenceSubMaterials: - options.flattenNonReferenceSubMaterials === true, - }, - ); - if ( - shouldApplyBoxProjection(resultMesh, { - ...options, - uvProjection: inferredUvProjection, - }) - ) { - applyBoxProjectionUV(resultMesh, options.uvScale); + let resultMesh; + try { + resultMesh = outerCSG.toMesh('resultMesh', scene, { + centerMesh: false, + }); + + if (!resultMesh || resultMesh.getTotalVertices() === 0) { + throw new Error('CSG produced empty mesh'); } + } catch (e) { + console.warn('[subtractMeshesIndividual] CSG subtract failed:', e.message); + console.warn( + '[subtractMeshesIndividual] Note: CSG operations require watertight (manifold) geometry. 3D text and merged meshes are typically non-manifold.' + ); + + // Clean up any empty meshes + flock.scene.meshes + .filter((m) => m.name === 'resultMesh' && m.getTotalVertices() === 0) + .forEach((m) => m.dispose()); baseDuplicate.dispose(); allToolParts.forEach((t) => t.dispose()); - baseMesh.dispose(); - validMeshes.forEach((m) => m.dispose()); - resolve(modelId); + return resolve(null); + } + + const localCenter = resultMesh.getBoundingInfo().boundingBox.center.clone(); + resultMesh.setPivotMatrix( + flock.BABYLON.Matrix.Translation(localCenter.x, localCenter.y, localCenter.z), + false + ); + resultMesh.position.subtractInPlace(localCenter); + resultMesh.computeWorldMatrix(true); + flock.applyResultMeshProperties(resultMesh, actualBase, modelId, blockKey, { + forceReferenceMaterial: options.forceReferenceMaterial === true, + flattenNonReferenceSubMaterials: options.flattenNonReferenceSubMaterials === true, }); + if ( + shouldApplyBoxProjection(resultMesh, { + ...options, + uvProjection: inferredUvProjection, + }) + ) { + applyBoxProjectionUV(resultMesh, options.uvScale); + } + + baseDuplicate.dispose(); + allToolParts.forEach((t) => t.dispose()); + baseMesh.dispose(); + validMeshes.forEach((m) => m.dispose()); + resolve(modelId); + }); }); }); }, - subtractMeshes( - modelId, - baseMeshName, - meshNames, - optionsOrApproach = "merge", - ) { + subtractMeshes(modelId, baseMeshName, meshNames, optionsOrApproach = 'merge') { const options = - optionsOrApproach && typeof optionsOrApproach === "object" - ? optionsOrApproach - : {}; + optionsOrApproach && typeof optionsOrApproach === 'object' ? optionsOrApproach : {}; const approach = - typeof optionsOrApproach === "string" - ? optionsOrApproach - : options.approach || "merge"; - - if (approach === "individual") { - return this.subtractMeshesIndividual( - modelId, - baseMeshName, - meshNames, - options, - ); + typeof optionsOrApproach === 'string' ? optionsOrApproach : options.approach || 'merge'; + + if (approach === 'individual') { + return this.subtractMeshesIndividual(modelId, baseMeshName, meshNames, options); } else { - return this.subtractMeshesMerge( - modelId, - baseMeshName, - meshNames, - options, - ); + return this.subtractMeshesMerge(modelId, baseMeshName, meshNames, options); } }, intersectMeshes(modelId, meshList) { - const { modelId: resolvedModelId, blockKey } = - resolveCsgModelIdentity(modelId); + const { modelId: resolvedModelId, blockKey } = resolveCsgModelIdentity(modelId); modelId = resolvedModelId; - return flock - .prepareMeshes(modelId, meshList, blockKey) - .then((validMeshes) => { - if (validMeshes.length) { - let firstMesh = validMeshes[0]; - // If metadata exists, use the mesh with material. - if (firstMesh.metadata?.modelName) { - const meshWithMaterial = - flock._findFirstDescendantWithMaterial(firstMesh); + return flock.prepareMeshes(modelId, meshList, blockKey).then((validMeshes) => { + if (validMeshes.length) { + let firstMesh = validMeshes[0]; + // If metadata exists, use the mesh with material. + if (firstMesh.metadata?.modelName) { + const meshWithMaterial = flock._findFirstDescendantWithMaterial(firstMesh); + if (meshWithMaterial) { + firstMesh = meshWithMaterial; + firstMesh.refreshBoundingInfo(); + firstMesh.flipFaces(); + } + } + + // Ensure mesh has valid geometry for CSG + firstMesh = prepareMeshForCSG(firstMesh); + if (!firstMesh) { + console.warn('First mesh has no valid geometry for CSG intersect.'); + return null; + } + + // Create the base CSG + let baseCSG; + try { + baseCSG = flock.BABYLON.CSG2.FromMesh(firstMesh, false); + } catch (e) { + console.warn('[intersectMeshes] CSG2.FromMesh failed on first mesh:', e.message); + console.warn( + '[intersectMeshes] Note: CSG operations require watertight (manifold) geometry. 3D text and merged meshes are typically non-manifold.' + ); + return null; + } + + // Intersect each subsequent mesh + let csgFailed = false; + validMeshes.slice(1).forEach((mesh) => { + if (csgFailed) return; + + if (mesh.metadata?.modelName) { + const meshWithMaterial = flock._findFirstDescendantWithMaterial(mesh); if (meshWithMaterial) { - firstMesh = meshWithMaterial; - firstMesh.refreshBoundingInfo(); - firstMesh.flipFaces(); + mesh = meshWithMaterial; + mesh.refreshBoundingInfo(); + mesh.flipFaces(); } } // Ensure mesh has valid geometry for CSG - firstMesh = prepareMeshForCSG(firstMesh); - if (!firstMesh) { - console.warn("First mesh has no valid geometry for CSG intersect."); - return null; + mesh = prepareMeshForCSG(mesh); + if (!mesh) { + console.warn('Skipping mesh with no valid geometry for CSG intersect.'); + return; } - // Create the base CSG - let baseCSG; try { - baseCSG = flock.BABYLON.CSG2.FromMesh(firstMesh, false); + const meshCSG = flock.BABYLON.CSG2.FromMesh(mesh, false); + baseCSG = baseCSG.intersect(meshCSG); } catch (e) { - console.warn( - "[intersectMeshes] CSG2.FromMesh failed on first mesh:", - e.message, - ); - console.warn( - "[intersectMeshes] Note: CSG operations require watertight (manifold) geometry. 3D text and merged meshes are typically non-manifold.", - ); - return null; + console.warn('[intersectMeshes] CSG intersect failed:', e.message); + csgFailed = true; } + }); - // Intersect each subsequent mesh - let csgFailed = false; - validMeshes.slice(1).forEach((mesh) => { - if (csgFailed) return; - - if (mesh.metadata?.modelName) { - const meshWithMaterial = - flock._findFirstDescendantWithMaterial(mesh); - if (meshWithMaterial) { - mesh = meshWithMaterial; - mesh.refreshBoundingInfo(); - mesh.flipFaces(); - } - } - - // Ensure mesh has valid geometry for CSG - mesh = prepareMeshForCSG(mesh); - if (!mesh) { - console.warn( - "Skipping mesh with no valid geometry for CSG intersect.", - ); - return; - } + if (csgFailed) { + console.warn( + '[intersectMeshes] Note: CSG operations require watertight (manifold) geometry.' + ); + return null; + } - try { - const meshCSG = flock.BABYLON.CSG2.FromMesh(mesh, false); - baseCSG = baseCSG.intersect(meshCSG); - } catch (e) { - console.warn( - "[intersectMeshes] CSG intersect failed:", - e.message, - ); - csgFailed = true; - } + // Generate the resulting intersected mesh + let intersectedMesh; + try { + intersectedMesh = baseCSG.toMesh('intersectedMesh', validMeshes[0].getScene(), { + centerMesh: false, + rebuildNormals: true, }); - if (csgFailed) { - console.warn( - "[intersectMeshes] Note: CSG operations require watertight (manifold) geometry.", - ); - return null; + if (!intersectedMesh || intersectedMesh.getTotalVertices() === 0) { + throw new Error('CSG produced empty mesh'); } + } catch (e) { + console.warn('[intersectMeshes] CSG toMesh failed:', e.message); + console.warn( + '[intersectMeshes] Note: CSG operations require watertight (manifold) geometry.' + ); - // Generate the resulting intersected mesh - let intersectedMesh; - try { - intersectedMesh = baseCSG.toMesh( - "intersectedMesh", - validMeshes[0].getScene(), - { - centerMesh: false, - rebuildNormals: true, - }, - ); - - if (!intersectedMesh || intersectedMesh.getTotalVertices() === 0) { - throw new Error("CSG produced empty mesh"); - } - } catch (e) { - console.warn("[intersectMeshes] CSG toMesh failed:", e.message); - console.warn( - "[intersectMeshes] Note: CSG operations require watertight (manifold) geometry.", - ); - - // Clean up any empty meshes - flock.scene.meshes - .filter( - (m) => - m.name === "intersectedMesh" && m.getTotalVertices() === 0, - ) - .forEach((m) => m.dispose()); + // Clean up any empty meshes + flock.scene.meshes + .filter((m) => m.name === 'intersectedMesh' && m.getTotalVertices() === 0) + .forEach((m) => m.dispose()); - return null; - } + return null; + } - // Keep local origin aligned with mesh bounds while preserving world placement. - recenterMeshLocalOrigin(intersectedMesh); + // Keep local origin aligned with mesh bounds while preserving world placement. + recenterMeshLocalOrigin(intersectedMesh); - // Apply properties to the resulting mesh - flock.applyResultMeshProperties( - intersectedMesh, - firstMesh, - modelId, - blockKey, - ); + // Apply properties to the resulting mesh + flock.applyResultMeshProperties(intersectedMesh, firstMesh, modelId, blockKey); - validMeshes.forEach((mesh) => mesh.dispose()); + validMeshes.forEach((mesh) => mesh.dispose()); - return modelId; // Return the modelId as per original functionality - } else { - console.warn("No valid meshes to intersect."); - return null; - } - }); + return modelId; // Return the modelId as per original functionality + } else { + console.warn('No valid meshes to intersect.'); + return null; + } + }); }, createHull(modelId, meshList) { - const { modelId: resolvedModelId, blockKey } = - resolveCsgModelIdentity(modelId); + const { modelId: resolvedModelId, blockKey } = resolveCsgModelIdentity(modelId); modelId = resolvedModelId; - return flock - .prepareMeshes(modelId, meshList, blockKey) - .then((validMeshes) => { - if (validMeshes.length) { - // Calculate the combined bounding box centre - let min = validMeshes[0] - .getBoundingInfo() - .boundingBox.minimumWorld.clone(); - let max = validMeshes[0] - .getBoundingInfo() - .boundingBox.maximumWorld.clone(); + return flock.prepareMeshes(modelId, meshList, blockKey).then((validMeshes) => { + if (validMeshes.length) { + // Calculate the combined bounding box centre + let min = validMeshes[0].getBoundingInfo().boundingBox.minimumWorld.clone(); + let max = validMeshes[0].getBoundingInfo().boundingBox.maximumWorld.clone(); - validMeshes.forEach((mesh) => { - const boundingInfo = mesh.getBoundingInfo(); - const meshMin = boundingInfo.boundingBox.minimumWorld; - const meshMax = boundingInfo.boundingBox.maximumWorld; + validMeshes.forEach((mesh) => { + const boundingInfo = mesh.getBoundingInfo(); + const meshMin = boundingInfo.boundingBox.minimumWorld; + const meshMax = boundingInfo.boundingBox.maximumWorld; - min = flock.BABYLON.Vector3.Minimize(min, meshMin); - max = flock.BABYLON.Vector3.Maximize(max, meshMax); - }); + min = flock.BABYLON.Vector3.Minimize(min, meshMin); + max = flock.BABYLON.Vector3.Maximize(max, meshMax); + }); - const combinedCentre = min.add(max).scale(0.5); - - // Merge the valid meshes into a single mesh - const updatedValidMeshes = validMeshes.map((mesh) => { - if (mesh.metadata?.modelName) { - const meshWithMaterial = - flock._findFirstDescendantWithMaterial(mesh); - if (meshWithMaterial) { - meshWithMaterial.refreshBoundingInfo(); - meshWithMaterial.flipFaces(); - return meshWithMaterial; - } + const combinedCentre = min.add(max).scale(0.5); + + // Merge the valid meshes into a single mesh + const updatedValidMeshes = validMeshes.map((mesh) => { + if (mesh.metadata?.modelName) { + const meshWithMaterial = flock._findFirstDescendantWithMaterial(mesh); + if (meshWithMaterial) { + meshWithMaterial.refreshBoundingInfo(); + meshWithMaterial.flipFaces(); + return meshWithMaterial; } - return mesh; - }); + } + return mesh; + }); - const mergedMesh = flock.BABYLON.Mesh.MergeMeshes( - updatedValidMeshes, - true, - ); + const mergedMesh = flock.BABYLON.Mesh.MergeMeshes(updatedValidMeshes, true); - if (!mergedMesh) { - console.warn("Failed to merge meshes for hull creation."); - return null; - } + if (!mergedMesh) { + console.warn('Failed to merge meshes for hull creation.'); + return null; + } - // Offset the merged mesh to be locally centred - mergedMesh.bakeTransformIntoVertices( - flock.BABYLON.Matrix.Translation( - -combinedCentre.x, - -combinedCentre.y, - -combinedCentre.z, - ), - ); + // Offset the merged mesh to be locally centred + mergedMesh.bakeTransformIntoVertices( + flock.BABYLON.Matrix.Translation(-combinedCentre.x, -combinedCentre.y, -combinedCentre.z) + ); - // Apply the material of the first mesh to the merged mesh - mergedMesh.material = updatedValidMeshes[0].material; + // Apply the material of the first mesh to the merged mesh + mergedMesh.material = updatedValidMeshes[0].material; - // Create the convex hull physics aggregate - const hullAggregate = new flock.BABYLON.PhysicsAggregate( - mergedMesh, - flock.BABYLON.PhysicsShapeType.CONVEX_HULL, - { mass: 0 }, // Adjust mass based on use case - flock.scene, - ); + // Create the convex hull physics aggregate + const hullAggregate = new flock.BABYLON.PhysicsAggregate( + mergedMesh, + flock.BABYLON.PhysicsShapeType.CONVEX_HULL, + { mass: 0 }, // Adjust mass based on use case + flock.scene + ); - // Create a debug mesh to visualize the convex hull - const hullMesh = flock.hullMeshFromBody(hullAggregate.body); + // Create a debug mesh to visualize the convex hull + const hullMesh = flock.hullMeshFromBody(hullAggregate.body); - // Offset the debug mesh to the original world position - hullMesh.position = combinedCentre; + // Offset the debug mesh to the original world position + hullMesh.position = combinedCentre; - hullMesh.material = updatedValidMeshes[0].material; + hullMesh.material = updatedValidMeshes[0].material; - // Apply properties to the resulting mesh - flock.applyResultMeshProperties( - hullMesh, - updatedValidMeshes[0], - modelId, - blockKey, - ); - // Dispose of original meshes after creating the hull - validMeshes.forEach((mesh) => mesh.dispose()); - mergedMesh.dispose(); + // Apply properties to the resulting mesh + flock.applyResultMeshProperties(hullMesh, updatedValidMeshes[0], modelId, blockKey); + // Dispose of original meshes after creating the hull + validMeshes.forEach((mesh) => mesh.dispose()); + mergedMesh.dispose(); - return modelId; // Return the debug mesh for further use - } else { - console.warn("No valid meshes to create a hull."); - return null; - } - }); + return modelId; // Return the debug mesh for further use + } else { + console.warn('No valid meshes to create a hull.'); + return null; + } + }); }, hullMeshFromBody(body) { const bodyInfoGeom = flock.hk.getBodyGeometry(body); const { positions, indices } = bodyInfoGeom; - const hullMesh = new flock.BABYLON.Mesh("custom", flock.scene); + const hullMesh = new flock.BABYLON.Mesh('custom', flock.scene); indices.reverse(); const vertexData = new flock.BABYLON.VertexData(); @@ -1384,7 +1181,7 @@ export const flockCSG = { } }); }); - }), + }) ).then((meshes) => meshes.filter((mesh) => mesh !== null)); }, applyResultMeshProperties( @@ -1392,10 +1189,7 @@ export const flockCSG = { referenceMesh, modelId, blockId, - { - forceReferenceMaterial = false, - flattenNonReferenceSubMaterials = false, - } = {}, + { forceReferenceMaterial = false, flattenNonReferenceSubMaterials = false } = {} ) { // Copy transformation properties referenceMesh.material.backFaceCulling = false; @@ -1407,35 +1201,27 @@ export const flockCSG = { resultMesh.metadata.blockKey = blockId; // Apply physics - flock.applyPhysics( - resultMesh, - new flock.BABYLON.PhysicsShapeMesh(resultMesh, flock.scene), - ); + flock.applyPhysics(resultMesh, new flock.BABYLON.PhysicsShapeMesh(resultMesh, flock.scene)); // Log and replace default materials const isDefaultMaterial = (material) => { return ( - material instanceof flock.BABYLON.StandardMaterial && - material.name === "default material" + material instanceof flock.BABYLON.StandardMaterial && material.name === 'default material' ); }; const replaceMaterial = () => { - return referenceMesh.material.clone("clonedMaterial"); + return referenceMesh.material.clone('clonedMaterial'); }; if (forceReferenceMaterial) { - resultMesh.material = referenceMesh.material.clone("csgResultMaterial"); + resultMesh.material = referenceMesh.material.clone('csgResultMaterial'); resultMesh.material.backFaceCulling = false; const textureName = String( - resultMesh.material.diffuseTexture?.name || - resultMesh.material.albedoTexture?.name || - "", + resultMesh.material.diffuseTexture?.name || resultMesh.material.albedoTexture?.name || '' ).toLowerCase(); const hasRenderableTexture = - textureName && - !textureName.endsWith("undefined") && - !textureName.includes("none.png"); + textureName && !textureName.endsWith('undefined') && !textureName.includes('none.png'); if (!hasRenderableTexture && resultMesh.convertToFlatShadedMesh) { try { resultMesh.convertToFlatShadedMesh(); @@ -1450,21 +1236,19 @@ export const flockCSG = { if (resultMesh.material) { if (resultMesh.material instanceof flock.BABYLON.MultiMaterial) { - resultMesh.material.subMaterials = resultMesh.material.subMaterials.map( - (subMaterial) => { - if (subMaterial && isDefaultMaterial(subMaterial)) { - return replaceMaterial(); - } - return subMaterial; - }, - ); + resultMesh.material.subMaterials = resultMesh.material.subMaterials.map((subMaterial) => { + if (subMaterial && isDefaultMaterial(subMaterial)) { + return replaceMaterial(); + } + return subMaterial; + }); } else if (isDefaultMaterial(resultMesh.material)) { resultMesh.material = replaceMaterial(); resultMesh.material.backFaceCulling = false; } } else { // No material assigned by CSG - copy from reference mesh - resultMesh.material = referenceMesh.material.clone("csgResultMaterial"); + resultMesh.material = referenceMesh.material.clone('csgResultMaterial'); resultMesh.material.backFaceCulling = false; } @@ -1473,26 +1257,24 @@ export const flockCSG = { resultMesh.material instanceof flock.BABYLON.MultiMaterial ) { const baseName = referenceMesh.material?.name; - resultMesh.material.subMaterials = resultMesh.material.subMaterials.map( - (subMaterial) => { - if (!subMaterial) return subMaterial; - if (baseName && subMaterial.name === baseName) return subMaterial; + resultMesh.material.subMaterials = resultMesh.material.subMaterials.map((subMaterial) => { + if (!subMaterial) return subMaterial; + if (baseName && subMaterial.name === baseName) return subMaterial; - if (typeof subMaterial.clone === "function") { - subMaterial = subMaterial.clone(`${subMaterial.name}_csg`); - } + if (typeof subMaterial.clone === 'function') { + subMaterial = subMaterial.clone(`${subMaterial.name}_csg`); + } - if (subMaterial.diffuseColor) { - subMaterial.emissiveColor = subMaterial.diffuseColor.scale(0.2); - } - subMaterial.disableLighting = false; - if (subMaterial.specularColor) { - subMaterial.specularColor = flock.BABYLON.Color3.Black(); - } - subMaterial.backFaceCulling = false; - return subMaterial; - }, - ); + if (subMaterial.diffuseColor) { + subMaterial.emissiveColor = subMaterial.diffuseColor.scale(0.2); + } + subMaterial.disableLighting = false; + if (subMaterial.specularColor) { + subMaterial.specularColor = flock.BABYLON.Color3.Black(); + } + subMaterial.backFaceCulling = false; + return subMaterial; + }); } }, }; diff --git a/api/effects.js b/api/effects.js index bafd62ef..fdbf5a56 100644 --- a/api/effects.js +++ b/api/effects.js @@ -10,24 +10,19 @@ export const flockEffects = { if (flock.mainLight) { flock.mainLight.intensity = intensity; } else { - console.warn( - "Main light is not defined. Please ensure flock.mainLight exists.", - ); + console.warn('Main light is not defined. Please ensure flock.mainLight exists.'); } }, lightColor(diffuse, groundColor) { if (flock.mainLight) { flock.mainLight.diffuse = flock.BABYLON.Color3.FromHexString(diffuse); - flock.mainLight.groundColor = - flock.BABYLON.Color3.FromHexString(groundColor); + flock.mainLight.groundColor = flock.BABYLON.Color3.FromHexString(groundColor); } else { - console.warn( - "Main light is not defined. Please ensure flock.mainLight exists.", - ); + console.warn('Main light is not defined. Please ensure flock.mainLight exists.'); } }, getMainLight() { - return "__main_light__"; + return '__main_light__'; }, createParticleEffect( name, @@ -42,7 +37,7 @@ export const flockEffects = { gravity, direction, rotation, - } = {}, + } = {} ) { const resultName = flock._reserveName(name); @@ -61,21 +56,12 @@ export const flockEffects = { }); } - const particleSystem = new flock.BABYLON.ParticleSystem( - resultName, - 500, - flock.scene, - ); + const particleSystem = new flock.BABYLON.ParticleSystem(resultName, 500, flock.scene); const texturePath = flock.texturePath + shape; - particleSystem.particleTexture = new flock.BABYLON.Texture( - texturePath, - flock.scene, - ); + particleSystem.particleTexture = new flock.BABYLON.Texture(texturePath, flock.scene); particleSystem.emitter = meshInstance; - const meshEmitter = new flock.BABYLON.MeshParticleEmitter( - meshInstance, - ); + const meshEmitter = new flock.BABYLON.MeshParticleEmitter(meshInstance); particleSystem.particleEmitterType = meshEmitter; particleSystem.blendMode = 4; @@ -86,19 +72,19 @@ export const flockEffects = { startColor.r, startColor.g, startColor.b, - alphas.start, + alphas.start ); particleSystem.color2 = new flock.BABYLON.Color4( endColor.r, endColor.g, endColor.b, - alphas.end, + alphas.end ); particleSystem.colorDead = new flock.BABYLON.Color4( endColor.r, endColor.g, endColor.b, - 0, + 0 ); particleSystem.minSize = sizes.start; @@ -114,38 +100,31 @@ export const flockEffects = { ? new flock.BABYLON.Vector3(0, -9.81, 0) : flock.BABYLON.Vector3.Zero(); - if ( - direction && - (direction.x !== 0 || direction.y !== 0 || direction.z !== 0) - ) { + if (direction && (direction.x !== 0 || direction.y !== 0 || direction.z !== 0)) { particleSystem.minEmitPower = 1; particleSystem.maxEmitPower = 3; meshEmitter.useMeshNormalsForDirection = false; meshEmitter.direction1 = new flock.BABYLON.Vector3( direction.x, direction.y, - direction.z, + direction.z ); meshEmitter.direction2 = new flock.BABYLON.Vector3( direction.x, direction.y, - direction.z, + direction.z ); } if (rotation) { const toRad = Math.PI / 180; if (rotation.angularSpeed) { - particleSystem.minAngularSpeed = - rotation.angularSpeed.min * toRad; - particleSystem.maxAngularSpeed = - rotation.angularSpeed.max * toRad; + particleSystem.minAngularSpeed = rotation.angularSpeed.min * toRad; + particleSystem.maxAngularSpeed = rotation.angularSpeed.max * toRad; } if (rotation.initialRotation) { - particleSystem.minInitialRotation = - rotation.initialRotation.min * toRad; - particleSystem.maxInitialRotation = - rotation.initialRotation.max * toRad; + particleSystem.minInitialRotation = rotation.initialRotation.min * toRad; + particleSystem.maxInitialRotation = rotation.initialRotation.max * toRad; } } @@ -161,9 +140,7 @@ export const flockEffects = { return resultName; }, startParticleSystem(systemName) { - const particleSystem = flock.scene.particleSystems.find( - (system) => system.name === systemName, - ); + const particleSystem = flock.scene.particleSystems.find((system) => system.name === systemName); if (particleSystem) { particleSystem.start(); } else { @@ -171,9 +148,7 @@ export const flockEffects = { } }, stopParticleSystem(systemName) { - const particleSystem = flock.scene.particleSystems.find( - (system) => system.name === systemName, - ); + const particleSystem = flock.scene.particleSystems.find((system) => system.name === systemName); if (particleSystem) { particleSystem.stop(); @@ -182,37 +157,27 @@ export const flockEffects = { } }, resetParticleSystem(systemName) { - const particleSystem = flock.scene.particleSystems.find( - (system) => system.name === systemName, - ); + const particleSystem = flock.scene.particleSystems.find((system) => system.name === systemName); if (particleSystem) { particleSystem.reset(); } else { console.warn(`Particle system '${systemName}' not found.`); } }, - setFog({ - fogColorHex, - fogMode, - fogDensity = 0.1, - fogStart = 50, - fogEnd = 100, - } = {}) { - const fogColorRgb = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(fogColorHex), - ); + setFog({ fogColorHex, fogMode, fogDensity = 0.1, fogStart = 50, fogEnd = 100 } = {}) { + const fogColorRgb = flock.BABYLON.Color3.FromHexString(flock.getColorFromString(fogColorHex)); switch (fogMode) { - case "NONE": + case 'NONE': flock.scene.fogMode = flock.BABYLON.Scene.FOGMODE_NONE; break; - case "EXP": + case 'EXP': flock.scene.fogMode = flock.BABYLON.Scene.FOGMODE_EXP; break; - case "EXP2": + case 'EXP2': flock.scene.fogMode = flock.BABYLON.Scene.FOGMODE_EXP2; break; - case "LINEAR": + case 'LINEAR': flock.scene.fogMode = flock.BABYLON.Scene.FOGMODE_LINEAR; break; } diff --git a/api/events.js b/api/events.js index 0ee3f2ea..387314df 100644 --- a/api/events.js +++ b/api/events.js @@ -1,4 +1,3 @@ - let flock; export function setFlockReference(ref) { @@ -11,15 +10,13 @@ export const flockEvents = { */ onEvent(eventName, handler, once = false) { - if (typeof handler !== "function") { - console.warn("onEvent: handler must be a function"); + if (typeof handler !== 'function') { + console.warn('onEvent: handler must be a function'); return; } eventName = flock.sanitizeEventName(eventName); if (!flock.isAllowedEventName(eventName)) { - console.warn( - `Event name ${eventName} is reserved and cannot be broadcasted.`, - ); + console.warn(`Event name ${eventName} is reserved and cannot be broadcasted.`); return; } const signal = flock.abortController?.signal; @@ -40,19 +37,17 @@ export const flockEvents = { } signal?.addEventListener( - "abort", + 'abort', () => { flock.events[eventName]?.remove(observer); }, - { once: true }, + { once: true } ); }, broadcastEvent(eventName, data) { eventName = flock.sanitizeEventName(eventName); if (!flock.isAllowedEventName(eventName)) { - console.warn( - `Event name ${eventName} is reserved and cannot be broadcasted.`, - ); + console.warn(`Event name ${eventName} is reserved and cannot be broadcasted.`); return; } if (flock.events && flock.events[eventName]) { @@ -60,8 +55,8 @@ export const flockEvents = { } }, whenActionEvent(action, callback, isReleased = false) { - if (typeof callback !== "function") { - console.warn("whenActionEvent: callback must be a function"); + if (typeof callback !== 'function') { + console.warn('whenActionEvent: callback must be a function'); return; } const signal = flock.abortController?.signal; @@ -74,7 +69,7 @@ export const flockEvents = { if (isReleased) { const upObs = flock.inputManager.onActionUpObservable; const observer = upObs.add(handler); - signal?.addEventListener("abort", () => upObs.remove(observer), { + signal?.addEventListener('abort', () => upObs.remove(observer), { once: true, }); } else { @@ -85,29 +80,31 @@ export const flockEvents = { const downObserver = downObs.add(handler); const repeatObserver = repeatObs.add(handler); signal?.addEventListener( - "abort", + 'abort', () => { downObs.remove(downObserver); repeatObs.remove(repeatObserver); }, - { once: true }, + { once: true } ); } }, whenKeyEvent(key, callback, isReleased = false) { - if (typeof callback !== "function") { - console.warn("whenKeyEvent: callback must be a function"); + if (typeof callback !== 'function') { + console.warn('whenKeyEvent: callback must be a function'); return; } const signal = flock.abortController?.signal; if (signal?.aborted) return; - const handler = (k) => { if (k === key) callback(); }; + const handler = (k) => { + if (k === key) callback(); + }; if (isReleased) { const upObs = flock.inputManager.onKeyUpObservable; const observer = upObs.add(handler); - signal?.addEventListener("abort", () => upObs.remove(observer), { + signal?.addEventListener('abort', () => upObs.remove(observer), { once: true, }); } else { @@ -118,12 +115,12 @@ export const flockEvents = { const downObserver = downObs.add(handler); const repeatObserver = repeatObs.add(handler); signal?.addEventListener( - "abort", + 'abort', () => { downObs.remove(downObserver); repeatObs.remove(repeatObserver); }, - { once: true }, + { once: true } ); } }, @@ -137,7 +134,7 @@ export const flockEvents = { // Function to run the action const runAction = async () => { if (isDisposed) { - console.log("Scene is disposed. Exiting action."); + console.log('Scene is disposed. Exiting action.'); return; // Exit if the scene is disposed } @@ -153,7 +150,7 @@ export const flockEvents = { } await action(); } catch (error) { - console.log("Error while running action:", error); + console.log('Error while running action:', error); } finally { isActionRunning = false; if (!isDisposed) { @@ -175,7 +172,7 @@ export const flockEvents = { flock.scene.onDisposeObservable.addOnce(disposeHandler); }, isAllowedEventName(eventName) { - if (!eventName || typeof eventName !== "string") { + if (!eventName || typeof eventName !== 'string') { return false; } @@ -184,14 +181,7 @@ export const flockEvents = { } const lower = eventName.toLowerCase(); - const reservedPrefixes = [ - "_", - "on", - "system", - "internal", - "babylon", - "flock", - ]; + const reservedPrefixes = ['_', 'on', 'system', 'internal', 'babylon', 'flock']; if (reservedPrefixes.some((prefix) => lower.startsWith(prefix))) { return false; } @@ -204,15 +194,12 @@ export const flockEvents = { return true; }, sanitizeEventName(eventName) { - if (typeof eventName !== "string") { - return ""; + if (typeof eventName !== 'string') { + return ''; } // Remove disallowed characters (symbols, control chars), allow emoji, spaces, letters, numbers // This allows everything except common punctuation and control characters - const clean = eventName.replace( - /[!@#$%^&*()+=[\]{};:'"\\|,<>?/\n\r\t]/g, - "", - ); + const clean = eventName.replace(/[!@#$%^&*()+=[\]{};:'"\\|,<>?/\n\r\t]/g, ''); return clean.substring(0, 50); }, }; diff --git a/api/material.js b/api/material.js index 157e0c11..fb160ff7 100644 --- a/api/material.js +++ b/api/material.js @@ -13,50 +13,46 @@ export const flockMaterial = { const targets = [mesh, ...(mesh.getDescendants?.() || [])]; targets.forEach((m) => { const mat = - m.material || - (m.getClassName?.() === "InstancedMesh" - ? m.sourceMesh?.material - : null); + m.material || (m.getClassName?.() === 'InstancedMesh' ? m.sourceMesh?.material : null); flock.adjustMaterialTilingToMesh(m, mat, unitsPerTile); }); }, randomColour() { - const letters = "0123456789ABCDEF"; - let colour = "#"; + const letters = '0123456789ABCDEF'; + let colour = '#'; for (let i = 0; i < 6; i++) { colour += letters[Math.floor(Math.random() * 16)]; } - if (flock.materialsDebug) - console.log(` Generated the random colour ${colour}`); + if (flock.materialsDebug) console.log(` Generated the random colour ${colour}`); return colour.toLowerCase(); }, rgbToHex(rgb) { const matches = rgb.match(/\d+/g); if (!matches || matches.length < 3) { - return "#000000"; // fallback to black for invalid input + return '#000000'; // fallback to black for invalid input } const result = matches.slice(0, 3).map(function (x) { const num = parseInt(x); - if (isNaN(num)) return "00"; + if (isNaN(num)) return '00'; const hex = Math.max(0, Math.min(255, num)).toString(16); - return hex.length === 1 ? "0" + hex : hex; + return hex.length === 1 ? '0' + hex : hex; }); - return "#" + result.join(""); + return '#' + result.join(''); }, hexToRgba(hex, alpha) { - hex = hex.replace(/^#/, ""); + hex = hex.replace(/^#/, ''); let r = parseInt(hex.substring(0, 2), 16); let g = parseInt(hex.substring(2, 4), 16); let b = parseInt(hex.substring(4, 6), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; }, getColorFromString(colourString) { - if (typeof colourString !== "string") { - return "#000000"; + if (typeof colourString !== 'string') { + return '#000000'; } if (!colourString) { - return "#000000"; + return '#000000'; } if (/^([0-9A-F]{3}|[0-9A-F]{6})$/i.test(colourString)) { @@ -68,7 +64,7 @@ export const flockMaterial = { } try { - const colorDiv = flock.document.createElement("div"); + const colorDiv = flock.document.createElement('div'); colorDiv.style.color = colourString; flock.document.body.appendChild(colorDiv); const computedColor = getComputedStyle(colorDiv).color; @@ -77,7 +73,7 @@ export const flockMaterial = { // Parse the rgb(r, g, b) string and convert to individual numbers const matches = computedColor.match(/\d+/g); if (!matches || matches.length < 3) { - return "#000000"; + return '#000000'; } const r = parseInt(matches[0]); const g = parseInt(matches[1]); @@ -85,22 +81,19 @@ export const flockMaterial = { const result = flock.rgbToHex(`rgb(${r}, ${g}, ${b})`); return result.toLowerCase(); } catch (error) { - console.warn("Failed to parse color to hex; using default black:", error); - return "#000000"; + console.warn('Failed to parse color to hex; using default black:', error); + return '#000000'; } }, tint(meshName, { color } = {}) { - if (flock.materialsDebug) - console.log(`Changing tint of ${meshName} by ${color}`); + if (flock.materialsDebug) console.log(`Changing tint of ${meshName} by ${color}`); return new Promise((resolve) => { flock.whenModelReady(meshName, (mesh) => { if (mesh.material) { mesh.renderOverlay = true; mesh.overlayAlpha = 0.5; - mesh.overlayColor = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(color), - ); + mesh.overlayColor = flock.BABYLON.Color3.FromHexString(flock.getColorFromString(color)); } mesh.getChildMeshes().forEach(function (childMesh) { @@ -108,25 +101,22 @@ export const flockMaterial = { childMesh.renderOverlay = true; childMesh.overlayAlpha = 0.5; childMesh.overlayColor = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(flock.getColorFromString(color)), + flock.getColorFromString(flock.getColorFromString(color)) ); } }); - mesh.metadata?.clones?.forEach((cloneName) => - flock.tint(cloneName, { color: color }), - ); + mesh.metadata?.clones?.forEach((cloneName) => flock.tint(cloneName, { color: color })); resolve(); }); }); }, highlight(meshName, { color } = {}) { - if (flock.materialsDebug) - console.log(`Highlighting ${meshName} with ${color}`); + if (flock.materialsDebug) console.log(`Highlighting ${meshName} with ${color}`); const applyHighlight = (mesh) => { if (mesh.material) { flock.highlighter.addMesh( mesh, - flock.BABYLON.Color3.FromHexString(flock.getColorFromString(color)), + flock.BABYLON.Color3.FromHexString(flock.getColorFromString(color)) ); } }; @@ -135,9 +125,7 @@ export const flockMaterial = { flock.whenModelReady(meshName, (mesh) => { applyHighlight(mesh); mesh.getChildMeshes().forEach(applyHighlight); - mesh.metadata?.clones?.forEach((cloneName) => - flock.highlight(cloneName, { color: color }), - ); + mesh.metadata?.clones?.forEach((cloneName) => flock.highlight(cloneName, { color: color })); resolve(); }); }); @@ -145,17 +133,12 @@ export const flockMaterial = { glow(meshName, { color } = {}) { if (flock.materialsDebug) console.log(`Making ${meshName} glow`); if (!flock.glowLayer) { - flock.glowLayer = new flock.BABYLON.GlowLayer("glowLayer", flock.scene); + flock.glowLayer = new flock.BABYLON.GlowLayer('glowLayer', flock.scene); flock.glowLayer.intensity = 0.5; if (flock.sky) { flock.glowLayer.addExcludedMesh(flock.sky); } - flock.glowLayer.customEmissiveColorSelector = ( - mesh, - _subMesh, - _material, - result, - ) => { + flock.glowLayer.customEmissiveColorSelector = (mesh, _subMesh, _material, result) => { const glowColor = mesh.metadata?.glowColor; if (glowColor) { const c = flock.BABYLON.Color3.FromHexString(glowColor); @@ -175,9 +158,7 @@ export const flockMaterial = { flock.whenModelReady(meshName, (mesh) => { flock.glowMesh(mesh, color); mesh.metadata?.clones?.forEach((_cloneName) => - flock.whenModelReady((cloneMesh) => - flock.glowMesh(cloneMesh, { color: color }), - ), + flock.whenModelReady((cloneMesh) => flock.glowMesh(cloneMesh, { color: color })) ); resolve(); }); @@ -188,32 +169,30 @@ export const flockMaterial = { if (!mat) return null; if (mat.metadata?.cacheKey) { - const parts = mat.metadata.cacheKey.split("_"); + const parts = mat.metadata.cacheKey.split('_'); const lastPart = parts[parts.length - 1]; - const hasGlowPart = lastPart === "glow" || lastPart === "noglow"; + const hasGlowPart = lastPart === 'glow' || lastPart === 'noglow'; const colorPart = parts[1]; - const color = colorPart.includes("-") - ? colorPart.split("-") - : colorPart; + const color = colorPart.includes('-') ? colorPart.split('-') : colorPart; const parsedAlpha = parseFloat(parts[2]); return { color, materialName: mat.metadata.texName || - parts.slice(3, hasGlowPart ? -1 : parts.length).join("_") || - "none.png", + parts.slice(3, hasGlowPart ? -1 : parts.length).join('_') || + 'none.png', alpha: Number.isFinite(parsedAlpha) ? parsedAlpha : (mat.alpha ?? 1), - glow: hasGlowPart ? lastPart === "glow" : (mesh.metadata?.glow ?? false), + glow: hasGlowPart ? lastPart === 'glow' : (mesh.metadata?.glow ?? false), }; } const matColor = mat.diffuseColor || mat.albedoColor; const textureName = - mat.diffuseTexture?.name?.split("/").pop() || - mat.albedoTexture?.name?.split("/").pop() || - "none.png"; + mat.diffuseTexture?.name?.split('/').pop() || + mat.albedoTexture?.name?.split('/').pop() || + 'none.png'; return { - color: matColor ? "#" + matColor.toHexString().slice(1) : "#ffffff", + color: matColor ? '#' + matColor.toHexString().slice(1) : '#ffffff', materialName: textureName, alpha: mat.alpha ?? 1, glow: mesh.metadata?.glow ?? false, @@ -232,7 +211,7 @@ export const flockMaterial = { ? flock.getColorFromString(baseColor[0]) : baseColor ? flock.getColorFromString(baseColor) - : "#ffffff"; + : '#ffffff'; if (params) { const materialParams = { @@ -256,7 +235,7 @@ export const flockMaterial = { return new Promise((resolve) => { flock.whenModelReady(meshName, (mesh) => { const allMeshes = [mesh, ...mesh.getDescendants()].filter( - (m) => m instanceof flock.BABYLON.Mesh && m.getTotalVertices() > 0, + (m) => m instanceof flock.BABYLON.Mesh && m.getTotalVertices() > 0 ); allMeshes.forEach((nextMesh) => { @@ -280,8 +259,7 @@ export const flockMaterial = { clearEffects(meshName) { return new Promise((resolve) => { flock.whenModelReady(meshName, (mesh) => { - if (flock.materialsDebug) - console.log(`Clear effects from ${meshName}:`); + if (flock.materialsDebug) console.log(`Clear effects from ${meshName}:`); const removeEffects = (targetMesh) => { if (targetMesh.material) { const params = flock.getMaterialParamsFromMesh(targetMesh); @@ -300,9 +278,7 @@ export const flockMaterial = { delete targetMesh.metadata.glowColor; if (flock.glowLayer) { - const anyGlowing = flock.scene.meshes.some( - (m) => m !== targetMesh && m.metadata?.glow, - ); + const anyGlowing = flock.scene.meshes.some((m) => m !== targetMesh && m.metadata?.glow); if (!anyGlowing) { flock.glowLayer.isEnabled = false; } @@ -314,9 +290,7 @@ export const flockMaterial = { removeEffects(mesh); mesh.getChildMeshes().forEach(removeEffects); - mesh?.metadata?.clones?.forEach((cloneName) => - flock.clearEffects(cloneName), - ); + mesh?.metadata?.clones?.forEach((cloneName) => flock.clearEffects(cloneName)); resolve(); }); }); @@ -351,9 +325,7 @@ export const flockMaterial = { if (!materialMapping.has(currentMesh.material)) { // Clone the material and store it in the mapping if (flock.materialsDebug) - console.log( - ` Cloning material, ${currentMesh.material}, of ${currentMesh.name}`, - ); + console.log(` Cloning material, ${currentMesh.material}, of ${currentMesh.name}`); const clonedMaterial = cloneMaterial(currentMesh.material); materialMapping.set(currentMesh.material, clonedMaterial); } @@ -371,7 +343,7 @@ export const flockMaterial = { // Default material to use as the replacement base const defaultMaterial = flock.scene.defaultMaterial || - new flock.BABYLON.StandardMaterial("defaultMaterial", flock.scene); + new flock.BABYLON.StandardMaterial('defaultMaterial', flock.scene); defaultMaterial.backFaceCulling = false; // Helper function to copy color properties from PBR to Standard material @@ -412,7 +384,7 @@ export const flockMaterial = { const replaceIfPBRMaterial = (targetMesh) => { const material = targetMesh.material; - if (material && material.getClassName() === "PBRMaterial") { + if (material && material.getClassName() === 'PBRMaterial') { if (!replacedMaterialsMap.has(material)) { // Replace with a cloned default material, preserving the name const originalName = material.name; @@ -420,9 +392,9 @@ export const flockMaterial = { // Check if this is a target material and copy colors const materialNameLower = originalName.toLowerCase(); - const targetMaterials = ["black", "white", "mouth", "nose"]; + const targetMaterials = ['black', 'white', 'mouth', 'nose']; const isTargetMaterial = targetMaterials.some((target) => - materialNameLower.includes(target), + materialNameLower.includes(target) ); if (isTargetMaterial) { @@ -437,17 +409,16 @@ export const flockMaterial = { // Only override alpha if this isn't a target material const materialNameLower = targetMesh.material.name.toLowerCase(); - const targetMaterials = ["black", "white", "mouth", "nose"]; + const targetMaterials = ['black', 'white', 'mouth', 'nose']; const isTargetMaterial = targetMaterials.some((target) => - materialNameLower.includes(target), + materialNameLower.includes(target) ); if (!isTargetMaterial) { targetMesh.material.alpha = 1; } - targetMesh.material.transparencyMode = - flock.BABYLON.Material.MATERIAL_OPAQUE; + targetMesh.material.transparencyMode = flock.BABYLON.Material.MATERIAL_OPAQUE; // targetMesh.material.alphaMode = undefined; //targetMesh.material.reflectionTexture = null; targetMesh.material.needDepthPrePass = false; @@ -462,7 +433,7 @@ export const flockMaterial = { // (the cached template may still hold references to the same material objects) replacedMaterialsMap.forEach((newMaterial, oldMaterial) => { const stillInUse = flock.scene.meshes.some( - (m) => !m.isDisposed() && m.material === oldMaterial, + (m) => !m.isDisposed() && m.material === oldMaterial ); if (!stillInUse) oldMaterial.dispose(); }); @@ -470,11 +441,10 @@ export const flockMaterial = { changeColor(meshName, { color } = {}) { return new Promise((resolve) => { flock.whenModelReady(meshName, (mesh) => { - if (flock.materialsDebug) - console.log(`Change colour of ${meshName} to ${color}:`); + if (flock.materialsDebug) console.log(`Change colour of ${meshName} to ${color}:`); if (!mesh) { flock.scene.clearColor = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(color), + flock.getColorFromString(color) ); resolve(); return; @@ -487,27 +457,25 @@ export const flockMaterial = { }, changeColorMesh(mesh, color) { if (!mesh) { - flock.scene.clearColor = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(color), - ); + flock.scene.clearColor = flock.BABYLON.Color3.FromHexString(flock.getColorFromString(color)); return; } const CHARACTER_PART_ALIASES = { - hair: "hair", - skin: "skin", - eyes: "eyes", - shorts: "shorts", - tshirt: "tshirt", - "t-shirt": "tshirt", - tee: "tshirt", - sleeves: "sleeves", - sleeve: "sleeves", - detail: "sleeves", - shoes: "sleeves", + hair: 'hair', + skin: 'skin', + eyes: 'eyes', + shorts: 'shorts', + tshirt: 'tshirt', + 't-shirt': 'tshirt', + tee: 'tshirt', + sleeves: 'sleeves', + sleeve: 'sleeves', + detail: 'sleeves', + shoes: 'sleeves', }; - const canonicalizePartName = (name = "") => { + const canonicalizePartName = (name = '') => { const s = String(name).toLowerCase(); for (const key of Object.keys(CHARACTER_PART_ALIASES)) { if (s === key || s.includes(key)) return CHARACTER_PART_ALIASES[key]; @@ -547,8 +515,7 @@ export const flockMaterial = { }; const isCharacterLike = isCharacterMesh(mesh) || isCharacterLikeMesh(mesh); - const isTextPlaneMesh = (part) => - part?.name === "textPlane" || part?.metadata?.isTextPlane; + const isTextPlaneMesh = (part) => part?.name === 'textPlane' || part?.metadata?.isTextPlane; if (isCharacterLike) { const root = getRootMesh(mesh); @@ -563,9 +530,9 @@ export const flockMaterial = { const normalizeColorInput = (input) => { if (Array.isArray(input)) return input; - if (typeof input === "string") { + if (typeof input === 'string') { const trimmed = input.trim(); - if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { try { return JSON.parse(trimmed.replace(/'/g, '"')); } catch { @@ -577,13 +544,10 @@ export const flockMaterial = { }; const normalizedColor = normalizeColorInput(color); - const colors = Array.isArray(normalizedColor) - ? normalizedColor - : [normalizedColor]; + const colors = Array.isArray(normalizedColor) ? normalizedColor : [normalizedColor]; let colorIndex = 0; - if (flock.materialsDebug) - console.log(` Changing the colour of ${mesh.name} to ${colors}`); + if (flock.materialsDebug) console.log(` Changing the colour of ${mesh.name} to ${colors}`); // Map to keep track of materials and their assigned colours and indices const materialToColorMap = new Map(); @@ -688,12 +652,12 @@ export const flockMaterial = { } try { - if (mesh.metadata.shapeType === "Cylinder") { + if (mesh.metadata.shapeType === 'Cylinder') { mesh.forceSharedVertices(); mesh.convertToFlatShadedMesh(); } } catch (e) { - console.log("Error converting mesh to flat shaded:", e); + console.log('Error converting mesh to flat shaded:', e); } if (mesh.metadata?.glow) { @@ -702,20 +666,20 @@ export const flockMaterial = { }, applyColorToMaterial(part, materialName, color) { const CHARACTER_PART_ALIASES = { - hair: "hair", - skin: "skin", - eyes: "eyes", - shorts: "shorts", - tshirt: "tshirt", - "t-shirt": "tshirt", - tee: "tshirt", - sleeves: "sleeves", - sleeve: "sleeves", - detail: "sleeves", - shoes: "sleeves", + hair: 'hair', + skin: 'skin', + eyes: 'eyes', + shorts: 'shorts', + tshirt: 'tshirt', + 't-shirt': 'tshirt', + tee: 'tshirt', + sleeves: 'sleeves', + sleeve: 'sleeves', + detail: 'sleeves', + shoes: 'sleeves', }; - const canonicalizePartName = (name = "") => { + const canonicalizePartName = (name = '') => { const s = String(name).toLowerCase(); for (const key of Object.keys(CHARACTER_PART_ALIASES)) { if (s === key || s.includes(key)) return CHARACTER_PART_ALIASES[key]; @@ -737,10 +701,10 @@ export const flockMaterial = { if (part.material && targetPart && partName === targetPart) { part.material.diffuseColor = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(color), + flock.getColorFromString(color) ); part.material.albedoColor = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(color), + flock.getColorFromString(color) ); part.metadata = part.metadata || {}; part.metadata.materialPartName = targetPart; @@ -773,46 +737,35 @@ export const flockMaterial = { const parts = [root, ...root.getChildMeshes()]; parts.forEach((part) => { if (!part) return; - const rawName = - part.metadata?.materialPartName || part.material?.name || part.name; - const lower = String(rawName || "").toLowerCase(); - if (lower.includes("hair")) seedPartMetadata(part, "hair"); - else if (lower.includes("skin")) seedPartMetadata(part, "skin"); - else if (lower.includes("eyes")) seedPartMetadata(part, "eyes"); - else if (lower.includes("shorts")) seedPartMetadata(part, "shorts"); - else if ( - lower.includes("tshirt") || - lower.includes("t-shirt") || - lower.includes("tee") - ) - seedPartMetadata(part, "tshirt"); + const rawName = part.metadata?.materialPartName || part.material?.name || part.name; + const lower = String(rawName || '').toLowerCase(); + if (lower.includes('hair')) seedPartMetadata(part, 'hair'); + else if (lower.includes('skin')) seedPartMetadata(part, 'skin'); + else if (lower.includes('eyes')) seedPartMetadata(part, 'eyes'); + else if (lower.includes('shorts')) seedPartMetadata(part, 'shorts'); + else if (lower.includes('tshirt') || lower.includes('t-shirt') || lower.includes('tee')) + seedPartMetadata(part, 'tshirt'); else if ( - lower.includes("sleeves") || - lower.includes("sleeve") || - lower.includes("detail") || - lower.includes("shoes") + lower.includes('sleeves') || + lower.includes('sleeve') || + lower.includes('detail') || + lower.includes('shoes') ) - seedPartMetadata(part, "sleeves"); + seedPartMetadata(part, 'sleeves'); }); }; ensurePartMetadata(mesh); - if (hairColor != null) flock.applyColorToMaterial(mesh, "Hair", hairColor); - if (skinColor != null) flock.applyColorToMaterial(mesh, "Skin", skinColor); - if (eyesColor != null) flock.applyColorToMaterial(mesh, "Eyes", eyesColor); - if (sleevesColor != null) - flock.applyColorToMaterial(mesh, "Detail", sleevesColor); - if (shortsColor != null) - flock.applyColorToMaterial(mesh, "Shorts", shortsColor); - if (tshirtColor != null) - flock.applyColorToMaterial(mesh, "TShirt", tshirtColor); - if (tshirtColor != null) - flock.applyColorToMaterial(mesh, "Tshirt", tshirtColor); - if (sleevesColor != null) - flock.applyColorToMaterial(mesh, "Sleeves", sleevesColor); - if (sleevesColor != null) - flock.applyColorToMaterial(mesh, "Shoes", sleevesColor); + if (hairColor != null) flock.applyColorToMaterial(mesh, 'Hair', hairColor); + if (skinColor != null) flock.applyColorToMaterial(mesh, 'Skin', skinColor); + if (eyesColor != null) flock.applyColorToMaterial(mesh, 'Eyes', eyesColor); + if (sleevesColor != null) flock.applyColorToMaterial(mesh, 'Detail', sleevesColor); + if (shortsColor != null) flock.applyColorToMaterial(mesh, 'Shorts', shortsColor); + if (tshirtColor != null) flock.applyColorToMaterial(mesh, 'TShirt', tshirtColor); + if (tshirtColor != null) flock.applyColorToMaterial(mesh, 'Tshirt', tshirtColor); + if (sleevesColor != null) flock.applyColorToMaterial(mesh, 'Sleeves', sleevesColor); + if (sleevesColor != null) flock.applyColorToMaterial(mesh, 'Shoes', sleevesColor); }, changeMaterial(meshName, materialName, color) { return new Promise((resolve) => { @@ -836,15 +789,11 @@ export const flockMaterial = { : null; }, changeMaterialMesh(mesh, materialName, texturePath, color, alpha = 1) { - if (flock.materialsDebug) - console.log("Change material", materialName, color); + if (flock.materialsDebug) console.log('Change material', materialName, color); flock.ensureUniqueMaterial(mesh); // Create a new material - const material = new flock.BABYLON.StandardMaterial( - materialName, - flock.scene, - ); + const material = new flock.BABYLON.StandardMaterial(materialName, flock.scene); // Load the texture if provided if (texturePath) { @@ -862,13 +811,10 @@ export const flockMaterial = { material.alpha = alpha; material.backFaceCulling = false; - const isTextPlaneMesh = (part) => - part?.name === "textPlane" || part?.metadata?.isTextPlane; + const isTextPlaneMesh = (part) => part?.name === 'textPlane' || part?.metadata?.isTextPlane; // Assign the material to the mesh and its descendants - const allMeshes = [mesh] - .concat(mesh.getDescendants()) - .filter((part) => !isTextPlaneMesh(part)); + const allMeshes = [mesh].concat(mesh.getDescendants()).filter((part) => !isTextPlaneMesh(part)); allMeshes.forEach((part) => { part.material = material; flock.adjustMaterialTilingToMesh(part, material); @@ -915,12 +861,8 @@ export const flockMaterial = { // Normalize single-element array to plain value if (Array.isArray(color) && color.length === 1) color = color[0]; if (Array.isArray(color) && color.length >= 2) { - const normalizedColors = color.map((c) => - flock.getColorFromString(c).toLowerCase(), - ); - const allColorsIdentical = normalizedColors.every( - (c) => c === normalizedColors[0], - ); + const normalizedColors = color.map((c) => flock.getColorFromString(c).toLowerCase()); + const allColorsIdentical = normalizedColors.every((c) => c === normalizedColors[0]); if (allColorsIdentical) { color = normalizedColors[0]; } @@ -928,14 +870,14 @@ export const flockMaterial = { if (Array.isArray(color) && color.length >= 2) { // Use gradient for Flat material - if (materialName === "none.png") { + if (materialName === 'none.png') { if (color.length === 2) { material = new flock.GradientMaterial(materialName, flock.scene); material.bottomColor = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(color[0]), + flock.getColorFromString(color[0]) ); material.topColor = flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(color[1]), + flock.getColorFromString(color[1]) ); material.offset = 0.5; material.smoothness = 0.5; @@ -947,11 +889,7 @@ export const flockMaterial = { } } else { // Use shader-based color replacement for patterned materials - material = flock.createColorReplaceShaderMaterial( - materialName, - texturePath, - color, - ); + material = flock.createColorReplaceShaderMaterial(materialName, texturePath, color); material.backFaceCulling = false; } } else { @@ -981,29 +919,26 @@ export const flockMaterial = { // Update alpha for shader materials if (material.setFloat && alpha !== undefined) { - material.setFloat("alpha", alpha); + material.setFloat('alpha', alpha); } // Apply glow properties if enabled if (glow && material.emissiveColor !== undefined) { const emissiveColor = color ? flock.BABYLON.Color3.FromHexString( - flock.getColorFromString(Array.isArray(color) ? color[0] : color), + flock.getColorFromString(Array.isArray(color) ? color[0] : color) ) : flock.BABYLON.Color3.White(); material.emissiveColor = emissiveColor; material.emissiveIntensity = 1.0; } - if (flock.materialsDebug) - console.log(`Created the material: ${material.name}`); + if (flock.materialsDebug) console.log(`Created the material: ${material.name}`); return material; }, createMultiColorGradientMaterial(name, colors) { - if ( - !flock.BABYLON.Effect.ShadersStore["multiColorGradientFogVertexShader"] - ) { - flock.BABYLON.Effect.ShadersStore["multiColorGradientFogVertexShader"] = ` + if (!flock.BABYLON.Effect.ShadersStore['multiColorGradientFogVertexShader']) { + flock.BABYLON.Effect.ShadersStore['multiColorGradientFogVertexShader'] = ` precision highp float; attribute vec3 position; uniform mat4 worldViewProjection; @@ -1022,12 +957,8 @@ export const flockMaterial = { } `; } - if ( - !flock.BABYLON.Effect.ShadersStore["multiColorGradientFogFragmentShader"] - ) { - flock.BABYLON.Effect.ShadersStore[ - "multiColorGradientFogFragmentShader" - ] = ` + if (!flock.BABYLON.Effect.ShadersStore['multiColorGradientFogFragmentShader']) { + flock.BABYLON.Effect.ShadersStore['multiColorGradientFogFragmentShader'] = ` precision highp float; varying float vGradient; varying vec3 vFogPosition; @@ -1081,26 +1012,26 @@ export const flockMaterial = { name, flock.scene, { - vertex: "multiColorGradientFog", - fragment: "multiColorGradientFog", + vertex: 'multiColorGradientFog', + fragment: 'multiColorGradientFog', }, { - attributes: ["position"], + attributes: ['position'], uniforms: [ - "worldViewProjection", - "world", - "view", - "colorCount", - "colors", - "alpha", - "minMax", - "fogColor", - "fogDensity", - "fogStart", - "fogEnd", - "fogMode", + 'worldViewProjection', + 'world', + 'view', + 'colorCount', + 'colors', + 'alpha', + 'minMax', + 'fogColor', + 'fogDensity', + 'fogStart', + 'fogEnd', + 'fogMode', ], - }, + } ); // Convert colors to Color3 array (max 16, matching shader array size) @@ -1114,14 +1045,14 @@ export const flockMaterial = { .flat(); if (flock.materialsDebug) { - console.log("Color count:", colors.length); - console.log("Color array:", color3Array); + console.log('Color count:', colors.length); + console.log('Color array:', color3Array); } - shaderMaterial.setInt("colorCount", Math.min(colors.length, 16)); - shaderMaterial.setArray3("colors", color3Array); - shaderMaterial.setFloat("alpha", 1.0); - shaderMaterial.setVector2("minMax", new flock.BABYLON.Vector2(-1, 1)); + shaderMaterial.setInt('colorCount', Math.min(colors.length, 16)); + shaderMaterial.setArray3('colors', color3Array); + shaderMaterial.setFloat('alpha', 1.0); + shaderMaterial.setVector2('minMax', new flock.BABYLON.Vector2(-1, 1)); flock.registerFogAwareShaderMaterial(shaderMaterial); flock.updateFogUniformsForShaderMaterial(shaderMaterial); @@ -1138,23 +1069,16 @@ export const flockMaterial = { }); }, updateFogUniformsForShaderMaterial(material) { - if (!material?.setVector3 || !material?.setFloat || !material?.setInt) - return; + if (!material?.setVector3 || !material?.setFloat || !material?.setInt) return; const scene = flock.scene; const fogColor = scene?.fogColor || flock.BABYLON.Color3.Black(); - material.setVector3( - "fogColor", - new flock.BABYLON.Vector3(fogColor.r, fogColor.g, fogColor.b), - ); - material.setFloat("fogDensity", scene?.fogDensity ?? 0); - material.setFloat("fogStart", scene?.fogStart ?? 0); - material.setFloat("fogEnd", scene?.fogEnd ?? 0); - material.setInt( - "fogMode", - scene?.fogMode ?? flock.BABYLON.Scene.FOGMODE_NONE, - ); + material.setVector3('fogColor', new flock.BABYLON.Vector3(fogColor.r, fogColor.g, fogColor.b)); + material.setFloat('fogDensity', scene?.fogDensity ?? 0); + material.setFloat('fogStart', scene?.fogStart ?? 0); + material.setFloat('fogEnd', scene?.fogEnd ?? 0); + material.setInt('fogMode', scene?.fogMode ?? flock.BABYLON.Scene.FOGMODE_NONE); }, updateFogAwareShaderMaterials() { if (!flock._fogAwareShaderMaterials) return; @@ -1254,52 +1178,50 @@ export const flockMaterial = { // Create shader material const shaderMaterial = new flock.BABYLON.ShaderMaterial( - materialName + "_shader", + materialName + '_shader', flock.scene, { - vertex: "colorReplace", - fragment: "colorReplace", + vertex: 'colorReplace', + fragment: 'colorReplace', }, { - attributes: ["position", "uv"], + attributes: ['position', 'uv'], uniforms: [ - "worldViewProjection", - "world", - "view", - "textureSampler", - "lightColor", - "greyTintColor", - "darkColor", - "colorCount", - "alpha", - "uScale", - "vScale", - "fogColor", - "fogDensity", - "fogStart", - "fogEnd", - "fogMode", + 'worldViewProjection', + 'world', + 'view', + 'textureSampler', + 'lightColor', + 'greyTintColor', + 'darkColor', + 'colorCount', + 'alpha', + 'uScale', + 'vScale', + 'fogColor', + 'fogDensity', + 'fogStart', + 'fogEnd', + 'fogMode', ], needAlphaBlending: false, - }, + } ); // Register shaders - flock.BABYLON.Effect.ShadersStore["colorReplaceVertexShader"] = - vertexShader; - flock.BABYLON.Effect.ShadersStore["colorReplaceFragmentShader"] = - fragmentShader; + flock.BABYLON.Effect.ShadersStore['colorReplaceVertexShader'] = vertexShader; + flock.BABYLON.Effect.ShadersStore['colorReplaceFragmentShader'] = fragmentShader; // Set texture if (texturePath) { const texture = new flock.BABYLON.Texture(texturePath, flock.scene); texture.wrapU = flock.BABYLON.Texture.WRAP_ADDRESSMODE; texture.wrapV = flock.BABYLON.Texture.WRAP_ADDRESSMODE; - shaderMaterial.setTexture("textureSampler", texture); + shaderMaterial.setTexture('textureSampler', texture); // Apply tiling through shader uniforms (shader materials don't automatically use texture matrix) // Use scale of 1 to match single-color material behavior - shaderMaterial.setFloat("uScale", 1); - shaderMaterial.setFloat("vScale", 1); + shaderMaterial.setFloat('uScale', 1); + shaderMaterial.setFloat('vScale', 1); } // Convert colors and set uniforms @@ -1307,21 +1229,13 @@ export const flockMaterial = { const colorGrey = flock.hexToRgb(flock.getColorFromString(colors[1])); // tints greys shaderMaterial.setVector3( - "lightColor", - new flock.BABYLON.Vector3( - colorLight.r / 255.0, - colorLight.g / 255.0, - colorLight.b / 255.0, - ), + 'lightColor', + new flock.BABYLON.Vector3(colorLight.r / 255.0, colorLight.g / 255.0, colorLight.b / 255.0) ); shaderMaterial.setVector3( - "greyTintColor", - new flock.BABYLON.Vector3( - colorGrey.r / 255.0, - colorGrey.g / 255.0, - colorGrey.b / 255.0, - ), + 'greyTintColor', + new flock.BABYLON.Vector3(colorGrey.r / 255.0, colorGrey.g / 255.0, colorGrey.b / 255.0) ); const colorDark = @@ -1329,16 +1243,12 @@ export const flockMaterial = { ? flock.hexToRgb(flock.getColorFromString(colors[2])) : { r: 0, g: 0, b: 0 }; shaderMaterial.setVector3( - "darkColor", - new flock.BABYLON.Vector3( - colorDark.r / 255.0, - colorDark.g / 255.0, - colorDark.b / 255.0, - ), + 'darkColor', + new flock.BABYLON.Vector3(colorDark.r / 255.0, colorDark.g / 255.0, colorDark.b / 255.0) ); - shaderMaterial.setInt("colorCount", colors.length); + shaderMaterial.setInt('colorCount', colors.length); - shaderMaterial.setFloat("alpha", 1.0); + shaderMaterial.setFloat('alpha', 1.0); flock.registerFogAwareShaderMaterial(shaderMaterial); flock.updateFogUniformsForShaderMaterial(shaderMaterial); shaderMaterial.onBindObservable?.add(() => { @@ -1427,28 +1337,26 @@ export const flockMaterial = { `; // Register shaders once - if (!flock.BABYLON.Effect.ShadersStore["multiGradientVertexShader"]) { - flock.BABYLON.Effect.ShadersStore["multiGradientVertexShader"] = - vertexShader; + if (!flock.BABYLON.Effect.ShadersStore['multiGradientVertexShader']) { + flock.BABYLON.Effect.ShadersStore['multiGradientVertexShader'] = vertexShader; } - if (!flock.BABYLON.Effect.ShadersStore["multiGradientFragmentShader"]) { - flock.BABYLON.Effect.ShadersStore["multiGradientFragmentShader"] = - fragmentShader; + if (!flock.BABYLON.Effect.ShadersStore['multiGradientFragmentShader']) { + flock.BABYLON.Effect.ShadersStore['multiGradientFragmentShader'] = fragmentShader; } // Create shader material const shaderMaterial = new flock.BABYLON.ShaderMaterial( - materialName + "_multiGradient", + materialName + '_multiGradient', flock.scene, { - vertex: "multiGradient", - fragment: "multiGradient", + vertex: 'multiGradient', + fragment: 'multiGradient', }, { - attributes: ["position", "uv"], - uniforms: ["worldViewProjection", "color", "colorCount", "alpha"], + attributes: ['position', 'uv'], + uniforms: ['worldViewProjection', 'color', 'colorCount', 'alpha'], needAlphaBlending: false, - }, + } ); // Clamp to max 6 colors @@ -1456,18 +1364,16 @@ export const flockMaterial = { const count = clampedColors.length; for (let i = 0; i < 6; i++) { - const hex = flock.getColorFromString( - clampedColors[i] || clampedColors[count - 1], - ); + const hex = flock.getColorFromString(clampedColors[i] || clampedColors[count - 1]); const rgb = flock.hexToRgb(hex); shaderMaterial.setVector3( `color[${i}]`, - new flock.BABYLON.Vector3(rgb.r / 255.0, rgb.g / 255.0, rgb.b / 255.0), + new flock.BABYLON.Vector3(rgb.r / 255.0, rgb.g / 255.0, rgb.b / 255.0) ); } - shaderMaterial.setInt("colorCount", count); - shaderMaterial.setFloat("alpha", 1.0); + shaderMaterial.setInt('colorCount', count); + shaderMaterial.setFloat('alpha', 1.0); return shaderMaterial; }, @@ -1486,7 +1392,7 @@ export const flockMaterial = { if (oldMat && oldMat.metadata && oldMat.metadata.isManaged) { const cacheKey = oldMat.metadata.cacheKey; const isStillInUse = flock.scene.meshes.some( - (m) => m !== mesh && !m.isDisposed() && m.material === oldMat, + (m) => m !== mesh && !m.isDisposed() && m.material === oldMat ); if (!isStillInUse) { @@ -1499,52 +1405,46 @@ export const flockMaterial = { }, getOrCreateMaterial(colorInput, alpha = 1) { const isObject = - typeof colorInput === "object" && - colorInput !== null && - !Array.isArray(colorInput); + typeof colorInput === 'object' && colorInput !== null && !Array.isArray(colorInput); - let rawColor = "#ffffff"; - let texName = "none.png"; + let rawColor = '#ffffff'; + let texName = 'none.png'; let finalAlpha = alpha; let finalGlow = false; if (isObject) { const inner = colorInput.color || colorInput.baseColor; - const isInnerObject = - typeof inner === "object" && inner !== null && !Array.isArray(inner); + const isInnerObject = typeof inner === 'object' && inner !== null && !Array.isArray(inner); if (isInnerObject) { - rawColor = inner.color || inner.baseColor || "#ffffff"; + rawColor = inner.color || inner.baseColor || '#ffffff'; texName = inner.materialName || inner.textureSet || colorInput.materialName || colorInput.textureSet || - "none.png"; + 'none.png'; finalAlpha = inner.alpha !== undefined ? inner.alpha : colorInput.alpha !== undefined ? colorInput.alpha : alpha; - finalGlow = - inner.glow !== undefined ? inner.glow : (colorInput.glow ?? false); + finalGlow = inner.glow !== undefined ? inner.glow : (colorInput.glow ?? false); } else { - rawColor = inner || "#ffffff"; - texName = - colorInput.materialName || colorInput.textureSet || "none.png"; + rawColor = inner || '#ffffff'; + texName = colorInput.materialName || colorInput.textureSet || 'none.png'; finalAlpha = colorInput.alpha !== undefined ? colorInput.alpha : alpha; finalGlow = colorInput.glow ?? false; } } else { - rawColor = colorInput || "#ffffff"; + rawColor = colorInput || '#ffffff'; } - const colorKey = Array.isArray(rawColor) ? rawColor.join("-") : rawColor; + const colorKey = Array.isArray(rawColor) ? rawColor.join('-') : rawColor; const alphaKey = parseFloat(finalAlpha).toFixed(2); - const glowKey = finalGlow ? "glow" : "noglow"; - const cacheKey = - `mat_${colorKey}_${alphaKey}_${texName}_${glowKey}`.toLowerCase(); + const glowKey = finalGlow ? 'glow' : 'noglow'; + const cacheKey = `mat_${colorKey}_${alphaKey}_${texName}_${glowKey}`.toLowerCase(); if (!flock.materialCache) flock.materialCache = {}; if (flock.materialCache[cacheKey]) return flock.materialCache[cacheKey]; @@ -1578,28 +1478,23 @@ export const flockMaterial = { const applyColor = opts.applyColor ?? true; if (!applyColor || !rootMesh || !colorInput) return rootMesh; - const isTextPlaneMesh = (part) => - part?.name === "textPlane" || part?.metadata?.isTextPlane; + const isTextPlaneMesh = (part) => part?.name === 'textPlane' || part?.metadata?.isTextPlane; const geometryMeshes = rootMesh .getDescendants(false) .filter( - (n) => - n instanceof flock.BABYLON.Mesh && - n.getTotalVertices() > 0 && - !isTextPlaneMesh(n), + (n) => n instanceof flock.BABYLON.Mesh && n.getTotalVertices() > 0 && !isTextPlaneMesh(n) ) .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, - sensitivity: "base", - }), + sensitivity: 'base', + }) ); const targets = geometryMeshes.length ? geometryMeshes : [rootMesh]; - const isMaterialDescriptor = (v) => - typeof v === "object" && v !== null && !Array.isArray(v); + const isMaterialDescriptor = (v) => typeof v === 'object' && v !== null && !Array.isArray(v); const getRawColor = (v) => { const raw = isMaterialDescriptor(v) ? v.color || v.baseColor : v; @@ -1609,27 +1504,21 @@ export const flockMaterial = { }; const getTexName = (v) => - isMaterialDescriptor(v) - ? v.materialName || v.textureSet || "NONE" - : "NONE"; + isMaterialDescriptor(v) ? v.materialName || v.textureSet || 'NONE' : 'NONE'; const getAlpha = (v) => - isMaterialDescriptor(v) - ? (v.alpha ?? opts.alpha ?? 1) - : (opts.alpha ?? 1); + isMaterialDescriptor(v) ? (v.alpha ?? opts.alpha ?? 1) : (opts.alpha ?? 1); const makeTargetCacheKey = (v) => { const rawColor = getRawColor(v); const colorKey = Array.isArray(rawColor) - ? rawColor.join("-") - : flock.getColorFromString(rawColor) || "#ffffff"; + ? rawColor.join('-') + : flock.getColorFromString(rawColor) || '#ffffff'; const texName = String(getTexName(v)); const alphaKey = parseFloat(getAlpha(v)).toFixed(2); const glow = - typeof v === "object" && v !== null && !Array.isArray(v) - ? (v.glow ?? false) - : false; - const glowKey = glow ? "glow" : "noglow"; + typeof v === 'object' && v !== null && !Array.isArray(v) ? (v.glow ?? false) : false; + const glowKey = glow ? 'glow' : 'noglow'; return `mat_${colorKey}_${alphaKey}_${texName}_${glowKey}`.toLowerCase(); }; @@ -1640,8 +1529,7 @@ export const flockMaterial = { m.metadata = m.metadata || {}; if (index !== undefined) m.metadata.materialIndex = index; - else if (m.metadata.materialIndex !== undefined) - delete m.metadata.materialIndex; + else if (m.metadata.materialIndex !== undefined) delete m.metadata.materialIndex; const targetCacheKey = makeTargetCacheKey(v); if (m.material?.metadata?.cacheKey === targetCacheKey) return; @@ -1657,15 +1545,10 @@ export const flockMaterial = { !m.material.metadata._minMaxObserver ) { const mat = m.material; - mat.metadata._minMaxObserver = mat.onBindObservable.add( - (boundMesh) => { - const bb = boundMesh.getBoundingInfo().boundingBox; - mat.setVector2( - "minMax", - new flock.BABYLON.Vector2(bb.minimum.y, bb.maximum.y), - ); - }, - ); + mat.metadata._minMaxObserver = mat.onBindObservable.add((boundMesh) => { + const bb = boundMesh.getBoundingInfo().boundingBox; + mat.setVector2('minMax', new flock.BABYLON.Vector2(bb.minimum.y, bb.maximum.y)); + }); } } }; diff --git a/api/models.js b/api/models.js index 5f939f78..0d88c77a 100644 --- a/api/models.js +++ b/api/models.js @@ -11,12 +11,12 @@ export const flockModels = { scale = 1, position = { x: 0, y: 0, z: 0 }, colors = { - hair: "#000000", - skin: "#a15c33", - eyes: "#0000ff", - sleeves: "#ff0000", - shorts: "#00ff00", - tshirt: "#0000ff", + hair: '#000000', + skin: '#a15c33', + eyes: '#0000ff', + sleeves: '#ff0000', + shorts: '#00ff00', + tshirt: '#0000ff', }, callback = () => {}, }) { @@ -29,42 +29,42 @@ export const flockModels = { container.skeletons = []; container.animationGroups = []; } catch (_) { - console.warn("Suppressed non-critical error:", _); + console.warn('Suppressed non-critical error:', _); } }; // --- bail if there is no live scene (e.g. called during dispose) --- if (!flock.scene || flock.scene.isDisposed) { - console.warn("createCharacter: no active scene"); - return "error_no_scene"; + console.warn('createCharacter: no active scene'); + return 'error_no_scene'; } // --- validate --- - if (!modelName || typeof modelName !== "string" || modelName.length > 100) { - console.warn("createCharacter: invalid modelName"); - return "error_" + flock.scene.getUniqueId(); + if (!modelName || typeof modelName !== 'string' || modelName.length > 100) { + console.warn('createCharacter: invalid modelName'); + return 'error_' + flock.scene.getUniqueId(); } - if (!modelId || typeof modelId !== "string" || modelId.length > 100) { - console.warn("createCharacter: invalid modelId"); - return "error_" + flock.scene.getUniqueId(); + if (!modelId || typeof modelId !== 'string' || modelId.length > 100) { + console.warn('createCharacter: invalid modelId'); + return 'error_' + flock.scene.getUniqueId(); } // --- parse BEFORE sanitizing so blockKey remains RAW --- let desiredBase = modelId; // e.g. "player__j9#WY5..." let blockKey = null; - if (desiredBase.includes("__")) { - [desiredBase, blockKey] = desiredBase.split("__"); + if (desiredBase.includes('__')) { + [desiredBase, blockKey] = desiredBase.split('__'); } // If no "__" was provided, fall back to base as the key (keeps prior behavior safe) if (!blockKey) blockKey = desiredBase; // --- sanitize ONLY modelName + BASE (NOT the blockKey) --- - modelName = modelName.replace(/[^a-zA-Z0-9._-]/g, ""); + modelName = modelName.replace(/[^a-zA-Z0-9._-]/g, ''); // Capture the original base name before sanitization so we can register it as // an alias in modelReadyPromises. This lets whenModelReady("dimnnd monkey", ...) // resolve correctly even though the actual mesh name is "dimnndmonkey". const originalBase = desiredBase; - desiredBase = desiredBase.replace(/[^a-zA-Z0-9._-]/g, ""); + desiredBase = desiredBase.replace(/[^a-zA-Z0-9._-]/g, ''); // The sanitized base, captured before _reserveName may append a collision // suffix. The bare-base alias must only bridge a genuine sanitization // difference (originalBase !== sanitizedBase); it must NOT be registered for @@ -72,7 +72,7 @@ export const flockModels = { // would otherwise alias the bare model name onto the wrong instance. const sanitizedBase = desiredBase; - if (flock.maxMeshesReached()) return "error_" + flock.scene.getUniqueId(); + if (flock.maxMeshesReached()) return 'error_' + flock.scene.getUniqueId(); flock._recycleOldestByKey(modelName); @@ -82,23 +82,17 @@ export const flockModels = { const groupName = desiredBase; // group by base for onTrigger applyToGroup // position/scale clamps - const x = Number.isFinite(position?.x) - ? Math.max(-1000, Math.min(1000, position.x)) - : 0; + const x = Number.isFinite(position?.x) ? Math.max(-1000, Math.min(1000, position.x)) : 0; const groundLevelSentinel = -999999; - const numericY = - typeof position?.y === "string" ? Number(position.y) : position?.y; + const numericY = typeof position?.y === 'string' ? Number(position.y) : position?.y; const y = - position?.y === "__ground__level__" || numericY === groundLevelSentinel + position?.y === '__ground__level__' || numericY === groundLevelSentinel ? position.y : Number.isFinite(position?.y) ? Math.max(-1000, Math.min(1000, position.y)) : 0; - const z = Number.isFinite(position?.z) - ? Math.max(-1000, Math.min(1000, position.z)) - : 0; - if (!(typeof scale === "number" && scale >= 0.01 && scale <= 100)) - scale = 1; + const z = Number.isFinite(position?.z) ? Math.max(-1000, Math.min(1000, position.z)) : 0; + if (!(typeof scale === 'number' && scale >= 0.01 && scale <= 100)) scale = 1; // --- create readiness deferred (resolve OR reject) + abort hookup --- let resolveReady, rejectReady; @@ -125,9 +119,9 @@ export const flockModels = { const signal = flock.abortController?.signal; const onAbort = () => { try { - rejectReady(new Error("aborted")); + rejectReady(new Error('aborted')); } catch (error) { - console.warn("Suppressed non-critical error:", error); + console.warn('Suppressed non-critical error:', error); } flock.modelReadyPromises.delete(meshName); if ( @@ -136,10 +130,10 @@ export const flockModels = { ) flock.modelReadyPromises.delete(originalBase); flock._releaseName?.(meshName); - signal?.removeEventListener("abort", onAbort); + signal?.removeEventListener('abort', onAbort); }; - signal?.addEventListener("abort", onAbort, { once: true }); - const cleanupAbort = () => signal?.removeEventListener("abort", onAbort); + signal?.addEventListener('abort', onAbort, { once: true }); + const cleanupAbort = () => signal?.removeEventListener('abort', onAbort); // --- single load path --- flock.BABYLON.SceneLoader.LoadAssetContainerAsync( @@ -148,7 +142,7 @@ export const flockModels = { flock.scene, null, null, - { signal: flock.abortController?.signal }, + { signal: flock.abortController?.signal } ) .then((container) => { // The scene was disposed / the load was aborted while this @@ -161,10 +155,10 @@ export const flockModels = { try { container?.dispose?.(); } catch (_) { - console.warn("Suppressed non-critical error:", _); + console.warn('Suppressed non-critical error:', _); } if (!signal?.aborted) { - rejectReady(new Error("scene disposed")); + rejectReady(new Error('scene disposed')); flock.modelReadyPromises.delete(meshName); if ( originalBase !== sanitizedBase && @@ -181,16 +175,7 @@ export const flockModels = { container.addAllToScene(); const mesh = container.meshes[0]; - const bb = flock.setupMesh( - mesh, - modelName, - meshName, - blockKey, - scale, - x, - y, - z, - ); + const bb = flock.setupMesh(mesh, modelName, meshName, blockKey, scale, x, y, z); // materials & colors flock.ensureStandardMaterial(mesh); @@ -209,8 +194,8 @@ export const flockModels = { mesh.setEnabled(false); // Preload animations (non-blocking for readiness) - const animationPromises = ["Walk", "Jump", "Idle"].map((name) => - flock.switchToAnimation(flock.scene, bb, name, false, false, false), + const animationPromises = ['Walk', 'Jump', 'Idle'].map((name) => + flock.switchToAnimation(flock.scene, bb, name, false, false, false) ); // After anims, run optional callback (await if it returns a promise), then show @@ -219,9 +204,9 @@ export const flockModels = { if (callback) { try { const result = callback(); - if (result && typeof result.then === "function") await result; + if (result && typeof result.then === 'function') await result; } catch (err) { - console.error("Callback error:", err); + console.error('Callback error:', err); } } }) @@ -256,7 +241,7 @@ export const flockModels = { releaseContainer(container); }) .catch((error) => { - console.log("❌ Error loading character:", error); + console.log('❌ Error loading character:', error); rejectReady(error); flock._releaseName(meshName); flock.modelReadyPromises.delete(meshName); @@ -299,7 +284,7 @@ export const flockModels = { container.skeletons = []; container.animationGroups = []; } catch (_) { - console.warn("Suppressed non-critical error:", _); + console.warn('Suppressed non-critical error:', _); } }; @@ -316,10 +301,10 @@ export const flockModels = { m.metadata.originalNodeName = m.metadata.originalNodeName || m.name; m.metadata.isTemplate = true; m.metadata.templateTag = tag; - if ("isPickable" in m) m.isPickable = false; - if (typeof m.setEnabled === "function") m.setEnabled(false); - if ("isVisible" in m) m.isVisible = false; - if ("visibility" in m) m.visibility = 0; + if ('isPickable' in m) m.isPickable = false; + if (typeof m.setEnabled === 'function') m.setEnabled(false); + if ('isVisible' in m) m.isVisible = false; + if ('visibility' in m) m.visibility = 0; }); }; @@ -329,10 +314,10 @@ export const flockModels = { if (m.metadata?.isTemplate) { m.metadata = { ...m.metadata, isTemplate: false }; } - if ("isPickable" in m) m.isPickable = true; - if (typeof m.setEnabled === "function") m.setEnabled(true); - if ("isVisible" in m) m.isVisible = true; - if ("visibility" in m) m.visibility = 1; + if ('isPickable' in m) m.isPickable = true; + if (typeof m.setEnabled === 'function') m.setEnabled(true); + if ('isVisible' in m) m.isVisible = true; + if ('visibility' in m) m.visibility = 1; }); }; @@ -353,7 +338,7 @@ export const flockModels = { position.x, position.y, position.z, - color, + color ); applyMaterialToHierarchy(mesh, color); mesh.computeWorldMatrix(true); @@ -366,22 +351,20 @@ export const flockModels = { try { if (!flock.scene || flock.scene.isDisposed) { - console.warn("createObject: no active scene"); - return "error_no_scene"; + console.warn('createObject: no active scene'); + return 'error_no_scene'; } - if (flock.maxMeshesReached()) return "error_" + flock.scene.getUniqueId(); + if (flock.maxMeshesReached()) return 'error_' + flock.scene.getUniqueId(); - let [desiredBase, bKey] = modelId.includes("__") - ? modelId.split("__") - : [modelId, modelId]; - modelName = modelName.replace(/[^a-zA-Z0-9._-]/g, ""); - desiredBase = desiredBase.replace(/[^a-zA-Z0-9._-]/g, ""); + let [desiredBase, bKey] = modelId.includes('__') ? modelId.split('__') : [modelId, modelId]; + modelName = modelName.replace(/[^a-zA-Z0-9._-]/g, ''); + desiredBase = desiredBase.replace(/[^a-zA-Z0-9._-]/g, ''); const meshName = flock._reserveName(desiredBase); const groupName = desiredBase; if (applyColor && !color) { - color = flock.objectColours?.[modelName] || ["#FFFFFF", "#FFFFFF"]; + color = flock.objectColours?.[modelName] || ['#FFFFFF', '#FFFFFF']; } let resolveReady; @@ -392,9 +375,7 @@ export const flockModels = { if (flock.modelCache[modelName]) { flock._recycleOldestByKey(modelName); - const mesh = flock.modelCache[modelName].clone( - flock.modelCache[modelName].name, - ); + const mesh = flock.modelCache[modelName].clone(flock.modelCache[modelName].name); flock._registerInstance(modelName, meshName); finalizeMesh(mesh, meshName, groupName, bKey); resolveReady(mesh); @@ -405,9 +386,7 @@ export const flockModels = { flock.modelsBeingLoaded[modelName].then(() => { if (!flock.scene || flock.scene.isDisposed) return; flock._recycleOldestByKey(modelName); - const mesh = flock.modelCache[modelName].clone( - flock.modelCache[modelName].name, - ); + const mesh = flock.modelCache[modelName].clone(flock.modelCache[modelName].name); flock._registerInstance(modelName, meshName); finalizeMesh(mesh, meshName, groupName, bKey); resolveReady(mesh); @@ -418,7 +397,7 @@ export const flockModels = { const loadPromise = flock.BABYLON.SceneLoader.LoadAssetContainerAsync( flock.modelPath, modelName, - flock.scene, + flock.scene ); flock.modelsBeingLoaded[modelName] = loadPromise; @@ -430,7 +409,7 @@ export const flockModels = { try { container?.dispose?.(); } catch (_) { - console.warn("Suppressed non-critical error:", _); + console.warn('Suppressed non-critical error:', _); } delete flock.modelsBeingLoaded[modelName]; return; @@ -475,17 +454,11 @@ export const flockModels = { return meshName; } catch (error) { - console.warn("createObject failed; returning error id:", error); - return "error_" + flock.scene.getUniqueId(); + console.warn('createObject failed; returning error id:', error); + return 'error_' + flock.scene.getUniqueId(); } }, - createModel({ - modelName, - modelId, - scale = 1, - position = { x: 0, y: 0, z: 0 }, - callback = null, - }) { + createModel({ modelName, modelId, scale = 1, position = { x: 0, y: 0, z: 0 }, callback = null }) { return flock.createObject({ modelName, modelId, @@ -507,7 +480,7 @@ export const flockModels = { container.skeletons = []; container.animationGroups = []; } catch (error) { - console.warn("releaseContainer cleanup failed:", error); + console.warn('releaseContainer cleanup failed:', error); } }, }; diff --git a/api/movement.js b/api/movement.js index 32b413a0..e19d8bf1 100644 --- a/api/movement.js +++ b/api/movement.js @@ -173,7 +173,8 @@ export const flockMovement = { : false; // --- Horizontal control policy --- - const now = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); + const now = + typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); const prev = model._lastMoveForwardMs !== undefined ? model._lastMoveForwardMs : now; model._lastMoveForwardMs = now; diff --git a/api/physics.js b/api/physics.js index a9cb0508..ee79640a 100644 --- a/api/physics.js +++ b/api/physics.js @@ -623,7 +623,11 @@ export const flockPhysics = { }); }); }, - onIntersect(meshName, otherMeshName, { trigger, callback, applyToGroupOther = false, applyToGroupSelf = false } = {}) { + onIntersect( + meshName, + otherMeshName, + { trigger, callback, applyToGroupOther = false, applyToGroupSelf = false } = {} + ) { const getGroupRoot = (name) => (name.includes('__') ? name.split('__')[0] : name.split('_')[0]); const resolveCanonicalGroupName = (rawName) => { const scene = flock.scene; @@ -671,7 +675,7 @@ export const flockPhysics = { trigger, callback, applyToGroupOther: false, - }), + }) ); } } diff --git a/api/sensing.js b/api/sensing.js index 440066a7..6c05d453 100644 --- a/api/sensing.js +++ b/api/sensing.js @@ -11,9 +11,9 @@ export const flockSensing = { getProperty(modelName, propertyName) { const mesh = - modelName === "__active_camera__" + modelName === '__active_camera__' ? flock.scene.activeCamera - : modelName === "__main_light__" + : modelName === '__main_light__' ? flock.mainLight : flock.scene.getMeshByName(modelName); @@ -24,32 +24,28 @@ export const flockSensing = { // Use a consistent world position call (works for meshes, lights, and cameras). const absolutePosition = - typeof mesh.getAbsolutePosition === "function" + typeof mesh.getAbsolutePosition === 'function' ? mesh.getAbsolutePosition() : (mesh.globalPosition ?? mesh.position); const usesAbsolutePosition = - modelName === "__active_camera__" || modelName === "__main_light__"; + modelName === '__active_camera__' || modelName === '__main_light__'; const hasNumericXYZ = (value) => - value && - Number.isFinite(value.x) && - Number.isFinite(value.y) && - Number.isFinite(value.z); + value && Number.isFinite(value.x) && Number.isFinite(value.y) && Number.isFinite(value.z); const position = (() => { if (usesAbsolutePosition) { return absolutePosition; } - const anchor = - typeof flock._getAnchor === "function" ? flock._getAnchor(mesh) : null; + const anchor = typeof flock._getAnchor === 'function' ? flock._getAnchor(mesh) : null; if (hasNumericXYZ(anchor)) { return anchor; } const blockPosition = - typeof flock.getBlockPositionFromMesh === "function" + typeof flock.getBlockPositionFromMesh === 'function' ? flock.getBlockPositionFromMesh(mesh) : null; if (hasNumericXYZ(blockPosition)) { @@ -72,123 +68,111 @@ export const flockSensing = { let allMeshes, materialNode, materialNodes; switch (propertyName) { - case "POSITION_X": + case 'POSITION_X': propertyValue = position ? parseFloat(position.x.toFixed(2)) : null; break; - case "POSITION_Y": + case 'POSITION_Y': propertyValue = position ? parseFloat(position.y.toFixed(2)) : null; break; - case "POSITION_Z": + case 'POSITION_Z': propertyValue = position ? parseFloat(position.z.toFixed(2)) : null; break; - case "ROTATION_X": - propertyValue = parseFloat( - flock.BABYLON.Tools.ToDegrees(rotEuler.x).toFixed(2), - ); + case 'ROTATION_X': + propertyValue = parseFloat(flock.BABYLON.Tools.ToDegrees(rotEuler.x).toFixed(2)); break; - case "ROTATION_Y": - propertyValue = parseFloat( - flock.BABYLON.Tools.ToDegrees(rotEuler.y).toFixed(2), - ); + case 'ROTATION_Y': + propertyValue = parseFloat(flock.BABYLON.Tools.ToDegrees(rotEuler.y).toFixed(2)); break; - case "ROTATION_Z": - propertyValue = parseFloat( - flock.BABYLON.Tools.ToDegrees(rotEuler.z).toFixed(2), - ); + case 'ROTATION_Z': + propertyValue = parseFloat(flock.BABYLON.Tools.ToDegrees(rotEuler.z).toFixed(2)); break; - case "SCALE_X": + case 'SCALE_X': propertyValue = parseFloat(mesh.scaling.x.toFixed(2)); break; - case "SCALE_Y": + case 'SCALE_Y': propertyValue = parseFloat(mesh.scaling.y.toFixed(2)); break; - case "SCALE_Z": + case 'SCALE_Z': propertyValue = parseFloat(mesh.scaling.z.toFixed(2)); break; - case "SIZE_X": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'SIZE_X': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); propertyValue = parseFloat( - ( - bi.boundingBox.maximumWorld.x - bi.boundingBox.minimumWorld.x - ).toFixed(2), + (bi.boundingBox.maximumWorld.x - bi.boundingBox.minimumWorld.x).toFixed(2) ); break; } - case "SIZE_Y": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'SIZE_Y': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); propertyValue = parseFloat( - ( - bi.boundingBox.maximumWorld.y - bi.boundingBox.minimumWorld.y - ).toFixed(2), + (bi.boundingBox.maximumWorld.y - bi.boundingBox.minimumWorld.y).toFixed(2) ); break; } - case "SIZE_Z": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'SIZE_Z': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); propertyValue = parseFloat( - ( - bi.boundingBox.maximumWorld.z - bi.boundingBox.minimumWorld.z - ).toFixed(2), + (bi.boundingBox.maximumWorld.z - bi.boundingBox.minimumWorld.z).toFixed(2) ); break; } // MIN/MAX: return consistent world AABB extents. // (Origin-specific adjustments previously mixed local/world spaces and manual scaling.) - case "MIN_X": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'MIN_X': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); propertyValue = bi.boundingBox.minimumWorld.x; break; } - case "MAX_X": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'MAX_X': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); propertyValue = bi.boundingBox.maximumWorld.x; break; } - case "MIN_Y": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'MIN_Y': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); propertyValue = bi.boundingBox.minimumWorld.y; break; } - case "MAX_Y": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'MAX_Y': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); propertyValue = bi.boundingBox.maximumWorld.y; break; } - case "MIN_Z": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'MIN_Z': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); propertyValue = bi.boundingBox.minimumWorld.z; break; } - case "MAX_Z": { - if (typeof mesh.refreshBoundingInfo === "function") { + case 'MAX_Z': { + if (typeof mesh.refreshBoundingInfo === 'function') { mesh.refreshBoundingInfo(true); } const bi = mesh.getBoundingInfo(); @@ -196,11 +180,11 @@ export const flockSensing = { break; } - case "VISIBLE": + case 'VISIBLE': propertyValue = mesh.isEnabled() && mesh.isVisible; break; - case "ALPHA": + case 'ALPHA': allMeshes = [mesh].concat(mesh.getDescendants?.() ?? []); materialNode = allMeshes.find((node) => node.material); @@ -209,7 +193,7 @@ export const flockSensing = { } break; - case "COLOUR": + case 'COLOUR': allMeshes = [mesh].concat(mesh.getDescendants?.() ?? []); materialNodes = allMeshes.filter((node) => node.material); @@ -227,17 +211,14 @@ export const flockSensing = { if (colors.length === 1) { propertyValue = colors[0]; } else if (colors.length > 1) { - propertyValue = colors.join(", "); + propertyValue = colors.join(', '); } break; - case "DESCRIPTION": { + case 'DESCRIPTION': { const root = mesh.metadata?.boundingBox ?? mesh; propertyValue = root.metadata?.displayName ?? null; - if ( - propertyValue === null && - typeof root.getChildMeshes === "function" - ) { + if (propertyValue === null && typeof root.getChildMeshes === 'function') { for (const child of root.getChildMeshes(false)) { if (child.metadata?.displayName != null) { propertyValue = child.metadata.displayName; @@ -249,14 +230,14 @@ export const flockSensing = { } default: - console.log("Property not recognized."); + console.log('Property not recognized.'); } return propertyValue; }, keyPressed(key) { - if (key === "ANY") return flock.inputManager.heldKeyCount() > 0; - if (key === "NONE") return flock.inputManager.heldKeyCount() === 0; + if (key === 'ANY') return flock.inputManager.heldKeyCount() > 0; + if (key === 'NONE') return flock.inputManager.heldKeyCount() === 0; return flock.inputManager.isKeyDown(key); }, setActionKey(action, key) { @@ -268,11 +249,11 @@ export const flockSensing = { getTime(unit) { const now = Date.now(); switch (unit) { - case "milliseconds": + case 'milliseconds': return now; - case "minutes": + case 'minutes': return Math.floor(now / 60000); - case "seconds": + case 'seconds': default: return Math.floor(now / 1000); } diff --git a/api/sound.js b/api/sound.js index c1fbeed6..20853e06 100644 --- a/api/sound.js +++ b/api/sound.js @@ -12,14 +12,18 @@ const soundBufferCache = new Map(); // soundUrl → Promise // α = 0.4 gives a ~33 ms time constant at 60 fps, enough to damp // rapid panning swings without perceptible spatial lag. const LISTENER_SMOOTH = 0.4; -const PANNER_SMOOTH = 0.4; +const PANNER_SMOOTH = 0.4; // Smoothed listener state — shared across all sounds in the same context. // Stored at module level so every updateListenerPositionAndOrientation call // continues from the same running average rather than resetting each time. let _listenerCtx = null; -let _slx = 0, _sly = 0, _slz = 0; // smoothed listener position -let _sfx = 0, _sfy = 0, _sfz = 0; // smoothed listener forward +let _slx = 0, + _sly = 0, + _slz = 0; // smoothed listener position +let _sfx = 0, + _sfy = 0, + _sfz = 0; // smoothed listener forward async function loadAudioBuffer(url, context) { if (!soundBufferCache.has(url)) { @@ -34,7 +38,7 @@ async function loadAudioBuffer(url, context) { .catch((err) => { soundBufferCache.delete(url); throw err; - }), + }) ); } return soundBufferCache.get(url); @@ -55,8 +59,12 @@ function playBufferEverywhere(context, buffer, soundName, { loop, volume, playba const finish = () => { if (done) return; done = true; - try { source.stop(); } catch {} - try { gainNode.disconnect(); } catch {} + try { + source.stop(); + } catch {} + try { + gainNode.disconnect(); + } catch {} const idx = flock.globalSounds.indexOf(soundRef); if (idx !== -1) flock.globalSounds.splice(idx, 1); }; @@ -73,7 +81,10 @@ function playBufferEverywhere(context, buffer, soundName, { loop, volume, playba if (!loop) { return new Promise((resolve) => { - source.onended = () => { finish(); resolve(); }; + source.onended = () => { + finish(); + resolve(); + }; }); } return soundRef; @@ -84,7 +95,9 @@ function playBufferOnMesh(context, mesh, buffer, soundName, { loop, volume, play const currentSound = mesh.metadata.currentSound; if (currentSound) { - try { currentSound.stop(); } catch {} + try { + currentSound.stop(); + } catch {} } const panner = context.createPanner(); @@ -106,11 +119,19 @@ function playBufferOnMesh(context, mesh, buffer, soundName, { loop, volume, play source.connect(gainNode); // Smoothed panner position — initialised to the mesh's starting position. - let sx = mesh.position.x, sy = mesh.position.y, sz = mesh.position.z; + let sx = mesh.position.x, + sy = mesh.position.y, + sz = mesh.position.z; const updatePosition = () => { - if (!flock.scene || context.state === 'closed') { finish(); return; } - if (mesh.isDisposed?.()) { finish(); return; } + if (!flock.scene || context.state === 'closed') { + finish(); + return; + } + if (mesh.isDisposed?.()) { + finish(); + return; + } const { x, y, z } = mesh.position; sx += PANNER_SMOOTH * (x - sx); sy += PANNER_SMOOTH * (y - sy); @@ -130,10 +151,18 @@ function playBufferOnMesh(context, mesh, buffer, soundName, { loop, volume, play if (done) return; done = true; flock.scene?.onBeforeRenderObservable?.remove(observer); - try { source.stop(); } catch {} - try { source.disconnect(); } catch {} - try { gainNode.disconnect(); } catch {} - try { panner.disconnect(); } catch {} + try { + source.stop(); + } catch {} + try { + source.disconnect(); + } catch {} + try { + gainNode.disconnect(); + } catch {} + try { + panner.disconnect(); + } catch {} if (mesh.metadata?.currentSound === soundRef) delete mesh.metadata.currentSound; const idx = flock.globalSounds.indexOf(soundRef); if (idx !== -1) flock.globalSounds.splice(idx, 1); @@ -153,7 +182,10 @@ function playBufferOnMesh(context, mesh, buffer, soundName, { loop, volume, play if (!loop) { return new Promise((resolve) => { - source.onended = () => { finish(); resolve(); }; + source.onended = () => { + finish(); + resolve(); + }; }); } return soundRef; @@ -197,29 +229,30 @@ async function safeResume(context) { }; const handler = () => { cleanup(); - context.resume().catch(() => {}).finally(resolve); + context + .resume() + .catch(() => {}) + .finally(resolve); }; document.addEventListener('pointerdown', handler); document.addEventListener('touchstart', handler, { passive: true }); // Abandon after 10 s — prevents a permanent listener leak when programmatic // audio fires but the user never gestures (e.g. background tab, navigation). - timer = setTimeout(() => { cleanup(); resolve(); }, 10000); + timer = setTimeout(() => { + cleanup(); + resolve(); + }, 10000); }); } } export const flockSound = { - async playSound( - meshName, - { soundName, loop = false, volume = 1, playbackRate = 1 } = {}, - ) { + async playSound(meshName, { soundName, loop = false, volume = 1, playbackRate = 1 } = {}) { volume = Number.isFinite(Number(volume)) ? Math.max(0, Math.min(1, Number(volume))) : 1; playbackRate = - Number.isFinite(Number(playbackRate)) && Number(playbackRate) > 0 - ? Number(playbackRate) - : 1; - if (!soundName || typeof soundName !== "string") { - console.warn("playSound: invalid soundName"); + Number.isFinite(Number(playbackRate)) && Number(playbackRate) > 0 ? Number(playbackRate) : 1; + if (!soundName || typeof soundName !== 'string') { + console.warn('playSound: invalid soundName'); return; } @@ -234,13 +267,13 @@ export const flockSound = { try { buffer = await loadAudioBuffer(soundUrl, context); } catch (err) { - console.warn("playSound: failed to load audio:", soundUrl, err); + console.warn('playSound: failed to load audio:', soundUrl, err); return; } - if (context.state === "closed") return; + if (context.state === 'closed') return; - if (meshName === "__everywhere__") { + if (meshName === '__everywhere__') { return playBufferEverywhere(context, buffer, soundName, { loop, volume, playbackRate }); } @@ -251,14 +284,15 @@ export const flockSound = { return new Promise((resolve) => { flock.whenModelReady(meshName, async (resolvedMesh) => { - if (flock.audioContext !== context) { resolve(); return; } - const result = await playBufferOnMesh( - context, - resolvedMesh, - buffer, - soundName, - { loop, volume, playbackRate }, - ); + if (flock.audioContext !== context) { + resolve(); + return; + } + const result = await playBufferOnMesh(context, resolvedMesh, buffer, soundName, { + loop, + volume, + playbackRate, + }); resolve(result); }); }); @@ -272,14 +306,18 @@ export const flockSound = { try { sound.stop(); } catch (e) { - console.warn("Error stopping sound:", sound.name, e); + console.warn('Error stopping sound:', sound.name, e); } } // Immediately disconnect the __everywhere__ gain — context.close() is async and // a brief window exists where the old gain can still feed the destination. if (flock._everywhereGain) { - try { flock._everywhereGain.disconnect(); } catch { /* already detached */ } + try { + flock._everywhereGain.disconnect(); + } catch { + /* already detached */ + } flock._everywhereGain = null; } @@ -292,14 +330,14 @@ export const flockSound = { // so _listenerCtx would otherwise hold the last closed context indefinitely. _listenerCtx = null; - if (!ctx || ctx.state === "closed") return; + if (!ctx || ctx.state === 'closed') return; // Don't close Babylon's own context — it gets closed when audioEngine.dispose() runs. // Closing it here would break Babylon's audio graph on the next scene load. const isBabylonOwned = flock.audioEngine?._audioContext === ctx; if (!isBabylonOwned) { ctx.close().catch((error) => { - console.error("Error closing audio context:", error); + console.error('Error closing audio context:', error); }); } }, @@ -308,36 +346,28 @@ export const flockSound = { }, async playNotes( meshName, - { - notes = [], - durations = [], - instrument = flock.createInstrument("square"), - } = {}, + { notes = [], durations = [], instrument = flock.createInstrument('square') } = {} ) { // A freshly-initiated playNotes means "play now", so clear any prior stop // request. stopAllSounds sets _audioStopped to abort an in-flight sequence // (caught by the post-setup check below and the per-note loop); it must not // permanently disable future playback until a full scene dispose. flock._audioStopped = false; - notes = notes.map((note) => (note === "_" ? null : note)); + notes = notes.map((note) => (note === '_' ? null : note)); durations = durations.map(Number); const getBPM = (obj) => obj?.metadata?.bpm || null; await flock.ensureAudio(); const context = getOrCreateContext(); - if (!context || context.state === "closed") return; + if (!context || context.state === 'closed') return; await safeResume(context); - if (context.state === "closed") return; + if (context.state === 'closed') return; if (flock._audioStopped) return; const scheduleNotes = (mesh, outputNode, observer) => { return new Promise((resolve) => { - let bpm = - getBPM(mesh) || - getBPM(mesh?.parent) || - getBPM(flock.scene) || - 60; + let bpm = getBPM(mesh) || getBPM(mesh?.parent) || getBPM(flock.scene) || 60; bpm = Number(bpm); if (!isFinite(bpm) || bpm <= 0) bpm = 60; @@ -355,7 +385,7 @@ export const flockSound = { duration, bpm, context.currentTime + offsetTime, - instrument, + instrument ); } @@ -363,25 +393,27 @@ export const flockSound = { } const audioTail = - (instrument?.attack ?? 0.01) + - (instrument?.decay ?? 0.1) + - (instrument?.release ?? 0.2); + (instrument?.attack ?? 0.01) + (instrument?.decay ?? 0.1) + (instrument?.release ?? 0.2); setTimeout( () => { if (observer) flock.scene?.onBeforeRenderObservable?.remove(observer); resolve(); }, - (offsetTime + audioTail + 0.1) * 1000, + (offsetTime + audioTail + 0.1) * 1000 ); }); }; - if (meshName === "__everywhere__") { + if (meshName === '__everywhere__') { // Disconnect any lingering gain from a prior __everywhere__ call immediately. // context.close() is async; an old gain can still feed the destination for a // brief window, producing a comb-filter offset that sounds like echo. if (flock._everywhereGain) { - try { flock._everywhereGain.disconnect(); } catch { /* already detached */ } + try { + flock._everywhereGain.disconnect(); + } catch { + /* already detached */ + } flock._everywhereGain = null; } const gain = context.createGain(); @@ -389,7 +421,11 @@ export const flockSound = { gain.connect(context.destination); return scheduleNotes(null, gain, null).then(() => { if (flock._everywhereGain === gain) flock._everywhereGain = null; - try { gain.disconnect(); } catch { /* already detached */ } + try { + gain.disconnect(); + } catch { + /* already detached */ + } }); } @@ -400,19 +436,21 @@ export const flockSound = { return; } if (!mesh?.position) { - console.error("Mesh does not have a position property:", mesh); + console.error('Mesh does not have a position property:', mesh); resolve(); return; } if (!mesh.metadata.panner || mesh.metadata.panner.context !== context) { if (mesh.metadata.panner) { - try { mesh.metadata.panner.disconnect(); } catch (e) {} + try { + mesh.metadata.panner.disconnect(); + } catch (e) {} } const panner = context.createPanner(); mesh.metadata.panner = panner; - panner.panningModel = "equalpower"; - panner.distanceModel = "linear"; + panner.panningModel = 'equalpower'; + panner.distanceModel = 'linear'; panner.refDistance = 1; panner.maxDistance = 20; panner.rolloffFactor = 1; @@ -428,10 +466,7 @@ export const flockSound = { panner.positionY.value = y; panner.positionZ.value = z; if (!flock.audioEngine && flock.scene.activeCamera) { - flockSound.updateListenerPositionAndOrientation( - context, - flock.scene.activeCamera, - ); + flockSound.updateListenerPositionAndOrientation(context, flock.scene.activeCamera); } }; updatePositions(); @@ -442,20 +477,12 @@ export const flockSound = { }); }); }, - playMidiNote( - context, - mesh, - note, - duration, - bpm, - playTime, - instrument = null, - ) { - if (!context || context.state === "closed") return; + playMidiNote(context, mesh, note, duration, bpm, playTime, instrument = null) { + if (!context || context.state === 'closed') return; // Validate numeric parameters to prevent Web Audio API errors if (!isFinite(duration) || !isFinite(playTime) || !isFinite(bpm)) { - console.warn("playMidiNote: Invalid parameters", { + console.warn('playMidiNote: Invalid parameters', { duration, playTime, bpm, @@ -469,26 +496,24 @@ export const flockSound = { gainNode.gain.setValueAtTime(0, context.currentTime); // Start silent to prevent click on first note const panner = mesh.metadata.panner; - osc.type = instrument?.type ?? "sine"; + osc.type = instrument?.type ?? 'sine'; osc.frequency.value = flock.midiToFrequency(note); // Convert MIDI note to frequency // Set up LFO effect if specified - const effect = instrument?.effect ?? "none"; + const effect = instrument?.effect ?? 'none'; const effectRate = instrument?.effectRate ?? 5; const effectDepth = instrument?.effectDepth ?? 0.5; let lfo = null; let lfoGain = null; - if (effect !== "none") { + if (effect !== 'none') { lfo = context.createOscillator(); lfoGain = context.createGain(); - lfo.type = effect === "warble" ? "square" : "sine"; - lfo.frequency.value = effect === "robot" ? effectRate * 100 : effectRate; + lfo.type = effect === 'warble' ? 'square' : 'sine'; + lfo.frequency.value = effect === 'robot' ? effectRate * 100 : effectRate; lfoGain.gain.value = - effect === "tremolo" - ? effectDepth - : osc.frequency.value * effectDepth * 0.5; + effect === 'tremolo' ? effectDepth : osc.frequency.value * effectDepth * 0.5; lfo.connect(lfoGain); - if (effect === "tremolo") { + if (effect === 'tremolo') { lfoGain.connect(gainNode.gain); } else { lfoGain.connect(osc.frequency); @@ -513,10 +538,7 @@ export const flockSound = { const startTime = Math.max(playTime, context.currentTime + 0.01); const attackEnd = startTime + attack; const decayEnd = attackEnd + decay; - const releaseStart = Math.max( - decayEnd, - startTime + noteDuration - gap - release, - ); + const releaseStart = Math.max(decayEnd, startTime + noteDuration - gap - release); // Cap the release so a short note can't ring for longer than the note itself. // Without this, a 0.5s note with release=1.0 would extend 1.4s and audibly // bleed over the next notes, especially at full volume (__everywhere__ / inside mesh). @@ -564,15 +586,22 @@ export const flockSound = { const cleanupDelay = Math.max(100, (stopTime - context.currentTime + 0.5) * 1000); const cleanupTimer = setTimeout(doDisconnect, cleanupDelay); - noteRef = { name: 'note', stop() { try { osc.stop(0); } catch { /* already stopped */ } doDisconnect(); } }; + noteRef = { + name: 'note', + stop() { + try { + osc.stop(0); + } catch { + /* already stopped */ + } + doDisconnect(); + }, + }; if (flock.globalSounds) flock.globalSounds.push(noteRef); }, midiToFrequency(note) { const parsed = Number(note); - note = Math.min( - 127, - Math.max(0, Math.round(Number.isNaN(parsed) ? 60 : parsed)), - ); // Clamp to valid MIDI range 0-127 + note = Math.min(127, Math.max(0, Math.round(Number.isNaN(parsed) ? 60 : parsed))); // Clamp to valid MIDI range 0-127 return 440 * Math.pow(2, (note - 69) / 12); // Convert MIDI note to frequency }, durationInSeconds(duration, bpm) { @@ -586,18 +615,18 @@ export const flockSound = { decay = 0.3, sustain = 0.7, release = 1.0, - effect = "none", + effect = 'none', effectRate = 5, effectDepth = 0.5, - } = {}, + } = {} ) { // Clamp parameters to valid ranges const toNum = (v, def) => { const n = Number(v); return Number.isNaN(n) ? def : n; }; - const validEffects = ["none", "tremolo", "vibrato", "warble", "robot"]; - effect = validEffects.includes(effect) ? effect : "none"; + const validEffects = ['none', 'tremolo', 'vibrato', 'warble', 'robot']; + effect = validEffects.includes(effect) ? effect : 'none'; volume = Math.min(1, Math.max(0, toNum(volume, 1.0))); attack = Math.min(5, Math.max(0, toNum(attack, 0.1))); decay = Math.min(5, Math.max(0, toNum(decay, 0.3))); @@ -623,8 +652,8 @@ export const flockSound = { const safeBpm = Number.isFinite(Number(bpm)) && Number(bpm) > 0 ? Number(bpm) : 60; bpm = safeBpm; - if (meshName === "__everywhere__") { - if (!flock.scene.metadata || typeof flock.scene.metadata !== "object") { + if (meshName === '__everywhere__') { + if (!flock.scene.metadata || typeof flock.scene.metadata !== 'object') { flock.scene.metadata = {}; } flock.scene.metadata.bpm = bpm; @@ -639,7 +668,7 @@ export const flockSound = { return; } - if (!mesh.metadata || typeof mesh.metadata !== "object") { + if (!mesh.metadata || typeof mesh.metadata !== 'object') { mesh.metadata = {}; } @@ -649,11 +678,8 @@ export const flockSound = { }); }, async playMusic(meshName, { notes = [], instrument = null } = {}) { - const effectiveInstrument = instrument ?? flock.createInstrument("sine"); - const flatNotes = - notes.length > 0 && Array.isArray(notes[0]) - ? notes.flat() - : notes; + const effectiveInstrument = instrument ?? flock.createInstrument('sine'); + const flatNotes = notes.length > 0 && Array.isArray(notes[0]) ? notes.flat() : notes; const pitches = flatNotes.map((n) => n?.pitch ?? null); const baseDurations = flatNotes.map((n) => n?.duration ?? 0.5); @@ -661,10 +687,10 @@ export const flockSound = { Math.max( 0.01, Number( - meshName === "__everywhere__" + meshName === '__everywhere__' ? flock.scene?.metadata?.musicSpeed - : mesh?.metadata?.musicSpeed, - ) || 1, + : mesh?.metadata?.musicSpeed + ) || 1 ); const playForMesh = async (mesh) => { @@ -677,7 +703,7 @@ export const flockSound = { }); }; - if (meshName === "__everywhere__") { + if (meshName === '__everywhere__') { return playForMesh(null); } @@ -691,8 +717,8 @@ export const flockSound = { setMusicSpeed(meshName, speed) { const validSpeed = Math.max(0.01, Number(speed) || 1); - if (meshName === "__everywhere__") { - if (!flock.scene.metadata || typeof flock.scene.metadata !== "object") { + if (meshName === '__everywhere__') { + if (!flock.scene.metadata || typeof flock.scene.metadata !== 'object') { flock.scene.metadata = {}; } flock.scene.metadata.musicSpeed = validSpeed; @@ -701,7 +727,7 @@ export const flockSound = { return new Promise((resolve) => { flock.whenModelReady(meshName, function (mesh) { - if (!mesh.metadata || typeof mesh.metadata !== "object") { + if (!mesh.metadata || typeof mesh.metadata !== 'object') { mesh.metadata = {}; } mesh.metadata.musicSpeed = validSpeed; @@ -713,18 +739,24 @@ export const flockSound = { if (!context || !camera) return; const { x: cx, y: cy, z: cz } = camera.position; const fwd = camera.getForwardRay().direction; - const tfx = -fwd.x, tfy = fwd.y, tfz = fwd.z; + const tfx = -fwd.x, + tfy = fwd.y, + tfz = fwd.z; // Reset smooth state when the audio context changes (e.g. after stopAllSounds). // Otherwise continue from the running average so position/orientation never jump. if (context !== _listenerCtx) { _listenerCtx = context; - _slx = cx; _sly = cy; _slz = cz; - _sfx = tfx; _sfy = tfy; _sfz = tfz; + _slx = cx; + _sly = cy; + _slz = cz; + _sfx = tfx; + _sfy = tfy; + _sfz = tfz; } else { - _slx += LISTENER_SMOOTH * (cx - _slx); - _sly += LISTENER_SMOOTH * (cy - _sly); - _slz += LISTENER_SMOOTH * (cz - _slz); + _slx += LISTENER_SMOOTH * (cx - _slx); + _sly += LISTENER_SMOOTH * (cy - _sly); + _slz += LISTENER_SMOOTH * (cz - _slz); _sfx += LISTENER_SMOOTH * (tfx - _sfx); _sfy += LISTENER_SMOOTH * (tfy - _sfy); _sfz += LISTENER_SMOOTH * (tfz - _sfz); @@ -757,19 +789,12 @@ export const flockSound = { async speak( meshName, text, - { - voice = "female", - language = "en-US", - rate = 1, - pitch = 1, - volume = 1, - mode = "start", - } = {}, + { voice = 'female', language = 'en-US', rate = 1, pitch = 1, volume = 1, mode = 'start' } = {} ) { // Check for Web Speech API support - if (!("speechSynthesis" in window)) { - console.warn("Text-to-speech not supported in this browser"); - return mode === "await" ? Promise.resolve() : undefined; + if (!('speechSynthesis' in window)) { + console.warn('Text-to-speech not supported in this browser'); + return mode === 'await' ? Promise.resolve() : undefined; } // Stop any current speech @@ -782,15 +807,12 @@ export const flockSound = { utterance.rate = Math.max(0.1, Math.min(10, rate)); utterance.pitch = Math.max(0, Math.min(2, pitch)); utterance.volume = Math.max(0, Math.min(1, volume)); - utterance.lang = "en-US"; + utterance.lang = 'en-US'; // Handle spatial audio if meshName is provided and not "__everywhere__" let spatialAudioSetup = null; - if (meshName && meshName !== "__everywhere__") { - spatialAudioSetup = await flockSound.setupSpatialSpeech( - utterance, - meshName, - ); + if (meshName && meshName !== '__everywhere__') { + spatialAudioSetup = await flockSound.setupSpatialSpeech(utterance, meshName); } // Set voice if available - handle voice loading timing @@ -820,67 +842,67 @@ export const flockSound = { // Common voice names by platform and gender const commonVoices = { - "en-US": { + 'en-US': { male: [ // Windows - "david", - "mark", - "zira", - "james", + 'david', + 'mark', + 'zira', + 'james', // macOS/iOS - "alex", - "daniel", - "fred", - "jorge", - "tom", + 'alex', + 'daniel', + 'fred', + 'jorge', + 'tom', // Android/Chrome - "male", - "man", + 'male', + 'man', ], female: [ // Windows - "zira", - "hazel", - "helen", + 'zira', + 'hazel', + 'helen', // macOS/iOS - "samantha", - "susan", - "allison", - "ava", - "karen", - "moira", - "tessa", - "veera", - "victoria", + 'samantha', + 'susan', + 'allison', + 'ava', + 'karen', + 'moira', + 'tessa', + 'veera', + 'victoria', // Android/Chrome - "female", - "woman", + 'female', + 'woman', ], }, - "en-GB": { + 'en-GB': { male: [ // Windows - "george", - "hazel", + 'george', + 'hazel', // macOS/iOS - "daniel", - "oliver", - "serena", + 'daniel', + 'oliver', + 'serena', // Android/Chrome - "male", - "man", + 'male', + 'man', ], female: [ // Windows - "hazel", - "susan", + 'hazel', + 'susan', // macOS/iOS - "kate", - "serena", - "stephanie", + 'kate', + 'serena', + 'stephanie', // Android/Chrome - "female", - "woman", + 'female', + 'woman', ], }, }; @@ -889,11 +911,9 @@ export const flockSound = { const languageVoices = voices.filter((v) => v.lang.startsWith(language)); if (languageVoices.length === 0) { - console.warn( - `No voices found for language ${language}, falling back to any English voice`, - ); + console.warn(`No voices found for language ${language}, falling back to any English voice`); // Fallback to any English voice - const englishVoices = voices.filter((v) => v.lang.startsWith("en")); + const englishVoices = voices.filter((v) => v.lang.startsWith('en')); if (englishVoices.length > 0) { selectedVoice = englishVoices[0]; } @@ -903,7 +923,7 @@ export const flockSound = { // 1. Try common voice names for the platform/language for (const voiceName of voiceNames) { selectedVoice = languageVoices.find((v) => - v.name.toLowerCase().includes(voiceName.toLowerCase()), + v.name.toLowerCase().includes(voiceName.toLowerCase()) ); if (selectedVoice) break; } @@ -911,67 +931,61 @@ export const flockSound = { // 2. Look for explicit "Male" or "Female" in name if (!selectedVoice) { selectedVoice = languageVoices.find((v) => - v.name.toLowerCase().includes(voice.toLowerCase()), + v.name.toLowerCase().includes(voice.toLowerCase()) ); } // 3. For male voices, avoid known female names - if (!selectedVoice && voice === "male") { + if (!selectedVoice && voice === 'male') { const femaleTerms = [ - "female", - "woman", - "lady", - "girl", - "samantha", - "susan", - "kate", - "zira", - "hazel", - "helen", - "karen", - "moira", - "tessa", - "fiona", - "allison", - "ava", - "veera", - "victoria", - "stephanie", - "serena", + 'female', + 'woman', + 'lady', + 'girl', + 'samantha', + 'susan', + 'kate', + 'zira', + 'hazel', + 'helen', + 'karen', + 'moira', + 'tessa', + 'fiona', + 'allison', + 'ava', + 'veera', + 'victoria', + 'stephanie', + 'serena', ]; selectedVoice = languageVoices.find( - (v) => - !femaleTerms.some((term) => - v.name.toLowerCase().includes(term.toLowerCase()), - ), + (v) => !femaleTerms.some((term) => v.name.toLowerCase().includes(term.toLowerCase())) ); } // 4. For female voices, avoid known male names - if (!selectedVoice && voice === "female") { + if (!selectedVoice && voice === 'female') { const maleTerms = [ - "male", - "man", - "david", - "alex", - "daniel", - "mark", - "tom", - "george", - "peter", - "john", - "michael", - "robert", - "fred", - "jorge", - "james", - "oliver", + 'male', + 'man', + 'david', + 'alex', + 'daniel', + 'mark', + 'tom', + 'george', + 'peter', + 'john', + 'michael', + 'robert', + 'fred', + 'jorge', + 'james', + 'oliver', ]; selectedVoice = languageVoices.find( - (v) => - !maleTerms.some((term) => - v.name.toLowerCase().includes(term.toLowerCase()), - ), + (v) => !maleTerms.some((term) => v.name.toLowerCase().includes(term.toLowerCase())) ); } @@ -989,11 +1003,11 @@ export const flockSound = { if (selectedVoice) { utterance.voice = selectedVoice; } else { - console.warn("No voice found for type:", voice, "using default"); + console.warn('No voice found for type:', voice, 'using default'); } } - if (mode === "await") { + if (mode === 'await') { return new Promise((resolve) => { utterance.onend = () => { if (spatialAudioSetup) { @@ -1002,7 +1016,7 @@ export const flockSound = { resolve(); }; utterance.onerror = (event) => { - console.warn("Speech synthesis error:", event.error); + console.warn('Speech synthesis error:', event.error); if (spatialAudioSetup) { spatialAudioSetup.cleanup(); } @@ -1019,7 +1033,7 @@ export const flockSound = { } }; utterance.onerror = (event) => { - console.warn("Speech synthesis error:", event.error); + console.warn('Speech synthesis error:', event.error); if (spatialAudioSetup) { spatialAudioSetup.cleanup(); } @@ -1042,7 +1056,7 @@ export const flockSound = { if (!mesh || mesh.isDisposed?.() || !flock.scene.activeCamera) return; const distance = flock.BABYLON.Vector3.Distance( flock.scene.activeCamera.position, - mesh.position, + mesh.position ); // Camera can't get closer than ~7 units; treat that as "full volume" range. @@ -1057,7 +1071,7 @@ export const flockSound = { } else if (adjustedDistance < maxDistance) { volumeGain = Math.max( 0.15, - refDistance / (refDistance + rolloffFactor * (adjustedDistance - refDistance)), + refDistance / (refDistance + rolloffFactor * (adjustedDistance - refDistance)) ); } else { volumeGain = 0.1; diff --git a/api/ui.js b/api/ui.js index b441dbbd..7fb4577d 100644 --- a/api/ui.js +++ b/api/ui.js @@ -1,17 +1,17 @@ -import { getBoundKeys } from "../input/bindings.js"; -import { JoystickSource } from "../input/joystickSource.js"; +import { getBoundKeys } from '../input/bindings.js'; +import { JoystickSource } from '../input/joystickSource.js'; import { registerUIButton, registerUISlider, registerUIInput, registerUIText, unregisterUIControl, -} from "../accessibility/uiA11y.js"; -import { setInteractIndicatorEnabled } from "../ui/interactIndicator.js"; +} from '../accessibility/uiA11y.js'; +import { setInteractIndicatorEnabled } from '../ui/interactIndicator.js'; let flock; //let fontFamily = "Asap"; -let fontFamily = "Atkinson Hyperlegible Next"; +let fontFamily = 'Atkinson Hyperlegible Next'; export function setFlockReference(ref) { flock = ref; @@ -21,42 +21,37 @@ export const flockUI = { UIText({ text, x, y, fontSize, color, duration, id = null } = {}) { if (!flock.scene || !flock.GUI) return; - flock.scene.UITexture ??= - flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI( - "UI", - true, - flock.scene, - window.devicePixelRatio || 1, - ); + flock.scene.UITexture ??= flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI( + 'UI', + true, + flock.scene, + window.devicePixelRatio || 1 + ); - const textBlockId = - id || - `textBlock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const textBlockId = id || `textBlock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const resolvedX = Number(x || 0); const resolvedY = Number(y || 0); let textBlock = flock.scene.UITexture.getControlByName(textBlockId); - if (!textBlock) { + if (!textBlock) { textBlock = new flock.GUI.TextBlock(textBlockId); textBlock.name = textBlockId; textBlock.resizeToFit = true; textBlock.forceResizeWidth = true; - textBlock.paddingLeft = "0px"; - textBlock.paddingRight = "0px"; + textBlock.paddingLeft = '0px'; + textBlock.paddingRight = '0px'; flock.scene.UITexture.addControl(textBlock); textBlock.textWrapping = false; textBlock.isPointerBlocker = false; - textBlock.textHorizontalAlignment = - flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; - textBlock.textVerticalAlignment = - flock.GUI.Control.VERTICAL_ALIGNMENT_CENTER; + textBlock.textHorizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; + textBlock.textVerticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_CENTER; } textBlock.text = text; - textBlock.color = color || "white"; + textBlock.color = color || 'white'; textBlock.isVisible = true; const px = Number(fontSize || 24) * flock.displayScale; @@ -78,10 +73,9 @@ export const flockUI = { ? flock.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM : flock.GUI.Control.VERTICAL_ALIGNMENT_TOP; - textBlock.textHorizontalAlignment = - flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; + textBlock.textHorizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; - if (document.fonts && document.fonts.status !== "loaded") { + if (document.fonts && document.fonts.status !== 'loaded') { textBlock.alpha = 0; document.fonts.ready.then(() => { if (!textBlock.isDisposed) { @@ -112,25 +106,20 @@ export const flockUI = { return textBlockId; }, - UIButton({ - text, - x, - y, - width, - textSize, - textColor, - backgroundColor, - buttonId, - } = {}) { + UIButton({ text, x, y, width, textSize, textColor, backgroundColor, buttonId } = {}) { if (!flock.scene || !flock.GUI) { - throw new Error("flock.scene or flock.GUI is not initialized."); + throw new Error('flock.scene or flock.GUI is not initialized.'); } - flock.scene.UITexture ??= - flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI", true, flock.scene, window.devicePixelRatio || 1); + flock.scene.UITexture ??= flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI( + 'UI', + true, + flock.scene, + window.devicePixelRatio || 1 + ); - if (!buttonId || typeof buttonId !== "string") { - throw new Error("buttonId must be a valid non-empty string."); + if (!buttonId || typeof buttonId !== 'string') { + throw new Error('buttonId must be a valid non-empty string.'); } const existing = flock.scene.UITexture.getControlByName(buttonId); @@ -141,18 +130,16 @@ export const flockUI = { const button = flock.GUI.Button.CreateSimpleButton(buttonId, text); const buttonSizes = { - SMALL: { width: "100px", height: "40px" }, - MEDIUM: { width: "150px", height: "50px" }, - LARGE: { width: "200px", height: "60px" }, + SMALL: { width: '100px', height: '40px' }, + MEDIUM: { width: '150px', height: '50px' }, + LARGE: { width: '200px', height: '60px' }, }; - if (typeof width !== "string") { - throw new Error( - "Invalid button size. Use 'SMALL', 'MEDIUM', or 'LARGE'.", - ); + if (typeof width !== 'string') { + throw new Error("Invalid button size. Use 'SMALL', 'MEDIUM', or 'LARGE'."); } - const size = buttonSizes[width.toUpperCase()] || buttonSizes["SMALL"]; + const size = buttonSizes[width.toUpperCase()] || buttonSizes['SMALL']; const scaledWidth = Math.round(parseInt(size.width) * flock.displayScale); const scaledHeight = Math.round(parseInt(size.height) * flock.displayScale); button.width = `${scaledWidth}px`; @@ -166,8 +153,8 @@ export const flockUI = { button.textBlock.fontSize = scaledSize; } - button.color = textColor || "white"; - button.background = backgroundColor || "blue"; + button.color = textColor || 'white'; + button.background = backgroundColor || 'blue'; button.left = `${x}px`; button.top = `${y}px`; @@ -185,7 +172,8 @@ export const flockUI = { flock.scene.UITexture.addControl(button); registerUIButton(buttonId, text, button, { - x, y, + x, + y, w: parseInt(size.width), h: parseInt(size.height), }); @@ -201,22 +189,26 @@ export const flockUI = { textColor, backgroundColor, id = null, - mode = "AWAIT", + mode = 'AWAIT', } = {}) { if (!flock.scene || !flock.GUI) { - throw new Error("flock.scene or flock.GUI is not initialized."); + throw new Error('flock.scene or flock.GUI is not initialized.'); } - flock.scene.UITexture ??= - flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI", true, flock.scene, window.devicePixelRatio || 1); + flock.scene.UITexture ??= flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI( + 'UI', + true, + flock.scene, + window.devicePixelRatio || 1 + ); const sanitize = (val, { maxLen = 500 } = {}) => { - if (val == null) return ""; + if (val == null) return ''; let s = String(val); - s = s.normalize("NFKC"); - const doc = new DOMParser().parseFromString(s, "text/html"); - s = doc.body.textContent || ""; - s = s.replace(/\p{Cc}/gu, ""); - s = s.replace(/\s+/g, " ").trim(); + s = s.normalize('NFKC'); + const doc = new DOMParser().parseFromString(s, 'text/html'); + s = doc.body.textContent || ''; + s = s.replace(/\p{Cc}/gu, ''); + s = s.replace(/\s+/g, ' ').trim(); if (s.length > maxLen) s = s.slice(0, maxLen); return s; }; @@ -232,12 +224,11 @@ export const flockUI = { if (existingSubmit) existingSubmit.dispose(); const sizeMap = { - SMALL: { width: "200px", height: "40px" }, - MEDIUM: { width: "300px", height: "50px" }, - LARGE: { width: "400px", height: "60px" }, + SMALL: { width: '200px', height: '40px' }, + MEDIUM: { width: '300px', height: '50px' }, + LARGE: { width: '400px', height: '60px' }, }; - const resolvedSize = - sizeMap[(size || "").toUpperCase()] || sizeMap["MEDIUM"]; + const resolvedSize = sizeMap[(size || '').toUpperCase()] || sizeMap['MEDIUM']; const scaledInputWidth = Math.round(parseInt(resolvedSize.width) * flock.displayScale); const scaledInputHeight = Math.round(parseInt(resolvedSize.height) * flock.displayScale); const buttonWidth = Math.round(50 * flock.displayScale); @@ -247,11 +238,11 @@ export const flockUI = { input.placeholderText = sanitize(text); input.width = `${scaledInputWidth}px`; input.height = `${scaledInputHeight}px`; - input.color = textColor || "black"; - input.background = backgroundColor || "white"; - input.focusedBackground = backgroundColor || "white"; + input.color = textColor || 'black'; + input.background = backgroundColor || 'white'; + input.focusedBackground = backgroundColor || 'white'; input.fontSize = Math.round((fontSize || 24) * flock.displayScale); - input.text = ""; + input.text = ''; input.left = `${x}px`; input.top = `${y}px`; @@ -264,11 +255,11 @@ export const flockUI = { ? flock.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM : flock.GUI.Control.VERTICAL_ALIGNMENT_TOP; - const button = flock.GUI.Button.CreateSimpleButton(submitId, "✓"); + const button = flock.GUI.Button.CreateSimpleButton(submitId, '✓'); button.width = `${buttonWidth}px`; button.height = `${scaledInputHeight}px`; - button.color = backgroundColor || "gray"; - button.background = textColor || "white"; + button.color = backgroundColor || 'gray'; + button.background = textColor || 'white'; button.fontSize = Math.round((fontSize || 24) * flock.displayScale); const offset = scaledInputWidth + spacing; @@ -281,12 +272,16 @@ export const flockUI = { flock.scene.UITexture.addControl(button); const submitX = x < 0 ? x - (scaledInputWidth + spacing) : x + (scaledInputWidth + spacing); - registerUIInput(inputId, submitId, input, button, + registerUIInput( + inputId, + submitId, + input, + button, { x, y, w: scaledInputWidth, h: scaledInputHeight }, - { x: submitX, y, w: buttonWidth, h: scaledInputHeight }, + { x: submitX, y, w: buttonWidth, h: scaledInputHeight } ); - if (mode === "START") { + if (mode === 'START') { return inputId; } @@ -299,47 +294,35 @@ export const flockUI = { }; button.onPointerUpObservable.add(submit); input.onKeyboardEventProcessedObservable.add((event) => { - if (event.type === "keydown" && event.key === "Enter") { + if (event.type === 'keydown' && event.key === 'Enter') { submit(); } }); }); }, - UISlider({ - id, - min, - max, - value, - x, - y, - size, - textColor, - backgroundColor, - } = {}) { + UISlider({ id, min, max, value, x, y, size, textColor, backgroundColor } = {}) { if (!flock.scene || !flock.GUI) { - throw new Error("flock.scene or flock.GUI is not initialized."); + throw new Error('flock.scene or flock.GUI is not initialized.'); } if (!flock.scene.UITexture) { - flock.scene.UITexture = - flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI( - "UI", - true, - flock.scene, - ); + flock.scene.UITexture = flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI( + 'UI', + true, + flock.scene + ); } const existing = flock.scene.UITexture.getControlByName(id); if (existing) existing.dispose(); const sliderSizes = { - SMALL: { width: "100px", height: "20px" }, - MEDIUM: { width: "200px", height: "30px" }, - LARGE: { width: "300px", height: "40px" }, + SMALL: { width: '100px', height: '20px' }, + MEDIUM: { width: '200px', height: '30px' }, + LARGE: { width: '300px', height: '40px' }, }; - const resolvedSize = - sliderSizes[(size || "MEDIUM").toUpperCase()] || sliderSizes.MEDIUM; + const resolvedSize = sliderSizes[(size || 'MEDIUM').toUpperCase()] || sliderSizes.MEDIUM; const slider = new flock.GUI.Slider(id); slider.minimum = min; @@ -347,14 +330,14 @@ export const flockUI = { slider.value = value; slider.thickness = 0; - slider.borderColor = "transparent"; + slider.borderColor = 'transparent'; slider.isPointerBlocker = true; - slider.thumbWidth = "20px"; + slider.thumbWidth = '20px'; slider.isFocusLinker = true; - slider.color = textColor || "#000000"; // Color of the "filled" part and thumb - slider.background = backgroundColor || "#ffffff"; // Color of the "empty" track + slider.color = textColor || '#000000'; // Color of the "filled" part and thumb + slider.background = backgroundColor || '#ffffff'; // Color of the "empty" track slider.height = resolvedSize.height; slider.width = resolvedSize.width; @@ -374,7 +357,8 @@ export const flockUI = { flock.scene.UITexture.addControl(slider); registerUISlider(id, slider, { - x, y, + x, + y, w: parseInt(resolvedSize.width), h: parseInt(resolvedSize.height), }); @@ -385,9 +369,7 @@ export const flockUI = { if (!flock.controlsTexture) return; const keyList = Array.isArray(keys) ? keys : [keys]; - const uniqueKeys = Array.from( - new Set(keyList.filter((k) => k != null && k !== "")), - ); + const uniqueKeys = Array.from(new Set(keyList.filter((k) => k != null && k !== ''))); const buttonId = `small-${text}-${Math.random().toString(36).slice(2)}`; const button = flock.GUI.Button.CreateSimpleButton(buttonId, text); @@ -396,7 +378,7 @@ export const flockUI = { button.width = `${size}px`; button.height = `${size}px`; button.color = color; - button.background = "transparent"; + button.background = 'transparent'; button.fontSize = `${40 * flock.displayScale}px`; button.fontFamily = fontFamily; @@ -442,14 +424,10 @@ export const flockUI = { flock.controlsTexture.addControl(grid); - const upButton = flock.createSmallButton("△", ["w", "ArrowUp"], color); - const downButton = flock.createSmallButton("▽", ["s", "ArrowDown"], color); - const leftButton = flock.createSmallButton("◁", ["a", "ArrowLeft"], color); - const rightButton = flock.createSmallButton( - "▷", - ["d", "ArrowRight"], - color, - ); + const upButton = flock.createSmallButton('△', ['w', 'ArrowUp'], color); + const downButton = flock.createSmallButton('▽', ['s', 'ArrowDown'], color); + const leftButton = flock.createSmallButton('◁', ['a', 'ArrowLeft'], color); + const rightButton = flock.createSmallButton('▷', ['d', 'ArrowRight'], color); grid.addControl(upButton, 0, 1); grid.addControl(leftButton, 1, 0); @@ -462,8 +440,7 @@ export const flockUI = { const rightGrid = new flock.GUI.Grid(); rightGrid.width = `${160 * flock.displayScale}px`; rightGrid.height = `${160 * flock.displayScale}px`; - rightGrid.horizontalAlignment = - flock.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT; + rightGrid.horizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT; rightGrid.verticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; rightGrid.addRowDefinition(1); rightGrid.addRowDefinition(1); @@ -472,24 +449,24 @@ export const flockUI = { flock.controlsTexture.addControl(rightGrid); - const button1 = flock.createSmallButton("①", [...getBoundKeys("BUTTON1"), "PageUp"], color); - const button2 = flock.createSmallButton("②", getBoundKeys("BUTTON2"), color); - const button3 = flock.createSmallButton("③", [...getBoundKeys("BUTTON3"), "PageDown"], color); - const button4 = flock.createSmallButton("④", getBoundKeys("BUTTON4"), color); + const button1 = flock.createSmallButton('①', [...getBoundKeys('BUTTON1'), 'PageUp'], color); + const button2 = flock.createSmallButton('②', getBoundKeys('BUTTON2'), color); + const button3 = flock.createSmallButton('③', [...getBoundKeys('BUTTON3'), 'PageDown'], color); + const button4 = flock.createSmallButton('④', getBoundKeys('BUTTON4'), color); rightGrid.addControl(button1, 0, 0); rightGrid.addControl(button2, 0, 1); rightGrid.addControl(button3, 1, 0); rightGrid.addControl(button4, 1, 1); }, - buttonControls(control = "BOTH", mode = "AUTO", color = "#ffffff") { + buttonControls(control = 'BOTH', mode = 'AUTO', color = '#ffffff') { // Check if touch is available const isTouchDevice = - "ontouchstart" in window || + 'ontouchstart' in window || navigator.maxTouchPoints > 0 || - window.matchMedia("(pointer: coarse)").matches; + window.matchMedia('(pointer: coarse)').matches; - const shouldShow = mode === "ENABLED" || (mode === "AUTO" && isTouchDevice); + const shouldShow = mode === 'ENABLED' || (mode === 'AUTO' && isTouchDevice); if (!shouldShow) { if (flock.controlsTexture) { @@ -504,15 +481,13 @@ export const flockUI = { // Attach to the scene so it receives input flock.controlsTexture = flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI( - "VirtualControls", + 'VirtualControls', true, - flock.scene, + flock.scene ); - if (control === "ARROWS" || control === "BOTH") - flock.createArrowControls(color); - if (control === "ACTIONS" || control === "BOTH") - flock.createButtonControls(color); + if (control === 'ARROWS' || control === 'BOTH') flock.createArrowControls(color); + if (control === 'ACTIONS' || control === 'BOTH') flock.createButtonControls(color); }, createJoystickControls(color) { if (!flock.controlsTexture) return; @@ -525,7 +500,7 @@ export const flockUI = { base.height = `${baseRadius * 2}px`; base.color = color; base.thickness = 3 * flock.displayScale; - base.background = "transparent"; + base.background = 'transparent'; base.horizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; base.verticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; @@ -548,13 +523,13 @@ export const flockUI = { scene: flock.scene, }); }, - onScreenControls(movement = "ARROWS", actions = "YES", mode = "AUTO", color = "#ffffff") { + onScreenControls(movement = 'ARROWS', actions = 'YES', mode = 'AUTO', color = '#ffffff') { const isTouchDevice = - "ontouchstart" in window || + 'ontouchstart' in window || navigator.maxTouchPoints > 0 || - window.matchMedia("(pointer: coarse)").matches; + window.matchMedia('(pointer: coarse)').matches; - const shouldShow = mode === "ENABLED" || (mode === "AUTO" && isTouchDevice); + const shouldShow = mode === 'ENABLED' || (mode === 'AUTO' && isTouchDevice); if (flock._joystickSource) { flock._joystickSource.stop(); @@ -572,19 +547,19 @@ export const flockUI = { if (flock.controlsTexture) flock.controlsTexture.dispose(); flock.controlsTexture = flock.GUI.AdvancedDynamicTexture.CreateFullscreenUI( - "VirtualControls", + 'VirtualControls', true, - flock.scene, + flock.scene ); - if (movement === "ARROWS") { + if (movement === 'ARROWS') { flock.createArrowControls(color); - } else if (movement === "JOYSTICK") { + } else if (movement === 'JOYSTICK') { flock._joystickSource = flock.createJoystickControls(color); flock._joystickSource?.start(); } - if (actions === "YES") flock.createButtonControls(color); + if (actions === 'YES') flock.createButtonControls(color); }, canvasControls(setting) { flock._canvasControlsEnabled = !!setting; @@ -601,28 +576,23 @@ export const flockUI = { let { text, duration, - textColor = "#ffffff", - backgroundColor = "#000000", + textColor = '#ffffff', + backgroundColor = '#000000', alpha = 1, size = 24, - mode = "ADD", + mode = 'ADD', } = options; // Validate duration - duration = - isFinite(Number(duration)) && Number(duration) >= 0 - ? Number(duration) - : 0; + duration = isFinite(Number(duration)) && Number(duration) >= 0 ? Number(duration) : 0; // Validate alpha - alpha = isFinite(Number(alpha)) - ? Math.min(Math.max(Number(alpha), 0), 1) - : 0.7; + alpha = isFinite(Number(alpha)) ? Math.min(Math.max(Number(alpha), 0), 1) : 0.7; // Validate size size = isFinite(Number(size)) && Number(size) > 0 ? Number(size) : 16; if (!flock.scene) { - console.error("Scene is not available."); - return Promise.reject("Scene is not available."); + console.error('Scene is not available.'); + return Promise.reject('Scene is not available.'); } const mesh = flock.scene.getMeshByName(meshName); @@ -635,8 +605,8 @@ export const flockUI = { if (readyMesh) { handleMesh(readyMesh).then(resolve).catch(reject); } else { - console.error("Mesh is not defined."); - reject("Mesh is not defined."); + console.error('Mesh is not defined.'); + reject('Mesh is not defined.'); } }); }); @@ -645,25 +615,23 @@ export const flockUI = { function handleMesh(targetMesh) { return new Promise((resolve) => { let plane; - let background = "transparent"; + let background = 'transparent'; - if (targetMesh.metadata && targetMesh.metadata.shape == "plane") { + if (targetMesh.metadata && targetMesh.metadata.shape == 'plane') { plane = targetMesh; background = plane.material.diffuseColor.toHexString(); plane.material.needDepthPrePass = true; } else { - plane = targetMesh - .getDescendants() - .find((child) => child.name === "textPlane"); + plane = targetMesh.getDescendants().find((child) => child.name === 'textPlane'); } if (!plane) { plane = flock.BABYLON.MeshBuilder.CreatePlane( - "textPlane", + 'textPlane', { width: 4, height: 4 }, - flock.scene, + flock.scene ); - plane.name = "textPlane"; + plane.name = 'textPlane'; plane.parent = targetMesh; plane.metadata = { ...(plane.metadata || {}), @@ -682,13 +650,8 @@ export const flockUI = { } const boundingInfo = targetMesh.getBoundingInfo(); const parentScale = targetMesh.scaling; - plane.scaling.set( - 1 / parentScale.x, - 1 / parentScale.y, - 1 / parentScale.z, - ); - plane.position.y = - boundingInfo.boundingBox.maximum.y + 2.1 / parentScale.y; + plane.scaling.set(1 / parentScale.x, 1 / parentScale.y, 1 / parentScale.z); + plane.position.y = boundingInfo.boundingBox.maximum.y + 2.1 / parentScale.y; }); plane.onDisposeObservable.add(() => { @@ -703,35 +666,31 @@ export const flockUI = { const planeHeight = planeBoundingInfo.boundingBox.extendSize.y * 2; const aspectRatio = planeWidth / planeHeight; const baseResolution = 1024; - const textureWidth = - baseResolution * (aspectRatio > 1 ? 1 : aspectRatio); - const textureHeight = - baseResolution * (aspectRatio > 1 ? 1 / aspectRatio : 1); + const textureWidth = baseResolution * (aspectRatio > 1 ? 1 : aspectRatio); + const textureHeight = baseResolution * (aspectRatio > 1 ? 1 / aspectRatio : 1); advancedTexture = flock.GUI.AdvancedDynamicTexture.CreateForMesh( plane, textureWidth, - textureHeight, + textureHeight ); advancedTexture.isTransparent = true; plane.advancedTexture = advancedTexture; - if (targetMesh.metadata && targetMesh.metadata.shape == "plane") { + if (targetMesh.metadata && targetMesh.metadata.shape == 'plane') { let fullScreenRect = new flock.GUI.Rectangle(); - fullScreenRect.width = "100%"; - fullScreenRect.height = "100%"; + fullScreenRect.width = '100%'; + fullScreenRect.height = '100%'; fullScreenRect.background = background; - fullScreenRect.color = "transparent"; + fullScreenRect.color = 'transparent'; advancedTexture.addControl(fullScreenRect); } - const stackPanel = new flock.GUI.StackPanel("stackPanel"); - stackPanel.horizontalAlignment = - flock.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; - stackPanel.verticalAlignment = - flock.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; + const stackPanel = new flock.GUI.StackPanel('stackPanel'); + stackPanel.horizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; + stackPanel.verticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; stackPanel.isVertical = true; - stackPanel.width = "100%"; + stackPanel.width = '100%'; stackPanel.adaptHeightToChildren = true; stackPanel.resizeToFit = true; stackPanel.forceResizeWidth = true; @@ -742,13 +701,13 @@ export const flockUI = { advancedTexture = plane.advancedTexture; } - const stackPanel = advancedTexture.getControlByName("stackPanel"); - if (mode === "REPLACE") { + const stackPanel = advancedTexture.getControlByName('stackPanel'); + if (mode === 'REPLACE') { stackPanel.clearControls(); } if (text) { - const bg = new flock.GUI.Rectangle("textBackground"); + const bg = new flock.GUI.Rectangle('textBackground'); bg.background = flock.hexToRgba(backgroundColor, alpha); bg.adaptWidthToChildren = true; bg.adaptHeightToChildren = true; @@ -773,10 +732,8 @@ export const flockUI = { textBlock.paddingRight = 50; textBlock.paddingTop = 20; textBlock.paddingBottom = 20; - textBlock.textVerticalAlignment = - flock.GUI.Control.VERTICAL_ALIGNMENT_TOP; - textBlock.textHorizontalAlignment = - flock.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; + textBlock.textVerticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_TOP; + textBlock.textHorizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; bg.addControl(textBlock); if (duration > 0) { @@ -788,14 +745,14 @@ export const flockUI = { }, duration * 1000); flock.abortController.signal.addEventListener( - "abort", + 'abort', () => { clearTimeout(timeoutId); bg.dispose(); textBlock.dispose(); - resolve(new Error("Action aborted")); + resolve(new Error('Action aborted')); }, - { once: true }, + { once: true } ); } else { resolve(); @@ -806,41 +763,36 @@ export const flockUI = { }); } }, - printText({ text, duration = 30, color = "white" } = {}) { + printText({ text, duration = 30, color = 'white' } = {}) { console.log(text); if (!flock.scene || !flock.stackPanel) return; - const safeDuration = - isFinite(Number(duration)) && Number(duration) >= 0 - ? Number(duration) - : 0; + const safeDuration = isFinite(Number(duration)) && Number(duration) >= 0 ? Number(duration) : 0; try { - const bg = new flock.GUI.Rectangle("textBackground"); - bg.background = "rgba(255, 255, 255, 0.5)"; + const bg = new flock.GUI.Rectangle('textBackground'); + bg.background = 'rgba(255, 255, 255, 0.5)'; bg.adaptWidthToChildren = true; bg.adaptHeightToChildren = true; bg.cornerRadius = 2; bg.thickness = 0; bg.horizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; bg.verticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_TOP; - bg.left = "5px"; - bg.top = "5px"; + bg.left = '5px'; + bg.top = '5px'; - const textBlock = new flock.GUI.TextBlock("textBlock", text); + const textBlock = new flock.GUI.TextBlock('textBlock', text); textBlock.color = color; textBlock.fontSize = Math.round(20 * flock.displayScale); textBlock.fontFamily = fontFamily; - textBlock.height = "25px"; - textBlock.paddingLeft = "10px"; - textBlock.paddingRight = "10px"; - textBlock.paddingTop = "2px"; - textBlock.paddingBottom = "2px"; - textBlock.textHorizontalAlignment = - flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; - textBlock.textVerticalAlignment = - flock.GUI.Control.VERTICAL_ALIGNMENT_CENTER; + textBlock.height = '25px'; + textBlock.paddingLeft = '10px'; + textBlock.paddingRight = '10px'; + textBlock.paddingTop = '2px'; + textBlock.paddingBottom = '2px'; + textBlock.textHorizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; + textBlock.textVerticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_CENTER; textBlock.textWrapping = flock.GUI.TextWrapping.WordWrap; textBlock.resizeToFit = true; textBlock.forceResizeWidth = true; @@ -850,11 +802,11 @@ export const flockUI = { const fadeOut = () => { const anim = new flock.BABYLON.Animation( - "fadeOut", - "alpha", + 'fadeOut', + 'alpha', 30, flock.BABYLON.Animation.ANIMATIONTYPE_FLOAT, - flock.BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT, + flock.BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT ); anim.setKeys([ @@ -877,7 +829,7 @@ export const flockUI = { } flock.abortController.signal.addEventListener( - "abort", + 'abort', () => { if (timeoutId !== null) { clearTimeout(timeoutId); @@ -887,10 +839,10 @@ export const flockUI = { bg.dispose(); } }, - { once: true }, + { once: true } ); } catch (error) { - console.warn("Unable to print text:", error); + console.warn('Unable to print text:', error); } }, }; diff --git a/api/xr.js b/api/xr.js index 45cb3d58..d688d76b 100644 --- a/api/xr.js +++ b/api/xr.js @@ -1,4 +1,4 @@ -import { translate } from "../main/translation.js"; +import { translate } from '../main/translation.js'; let flock; @@ -14,17 +14,12 @@ export const flockXR = { setCameraBackground(cameraType) { if (!flock.scene) { console.error( - "Scene not available. Ensure the scene is initialised before setting the camera background.", + 'Scene not available. Ensure the scene is initialised before setting the camera background.' ); return; } - const videoLayer = new flock.BABYLON.Layer( - "videoLayer", - null, - flock.scene, - true, - ); + const videoLayer = new flock.BABYLON.Layer('videoLayer', null, flock.scene, true); flock.BABYLON.VideoTexture.CreateFromWebCam( flock.scene, @@ -39,50 +34,42 @@ export const flockXR = { minHeight: 480, maxWidth: 1920, maxHeight: 1080, - deviceId: "", - }, + deviceId: '', + } ); }, async setXRMode(mode) { await flock.initializeXR(mode); flock.printText({ - text: translate("xr_mode_message"), + text: translate('xr_mode_message'), duration: 5, - color: "white", + color: 'white', }); }, exportMesh(meshName, format) { //meshName = "scene"; - if (meshName === "scene" && format === "GLB") { + if (meshName === 'scene' && format === 'GLB') { const scene = flock.scene; const cls = (n) => n?.getClassName?.(); - const isEnabledDeep = (n) => - typeof n.isEnabled === "function" ? n.isEnabled(true) : true; + const isEnabledDeep = (n) => (typeof n.isEnabled === 'function' ? n.isEnabled(true) : true); // Treat ALL mesh subclasses as geometry; we'll still skip LinesMesh explicitly const isAbstractMesh = (n) => - typeof flock.BABYLON !== "undefined" && - n instanceof flock.BABYLON.AbstractMesh; - const isLines = (n) => cls(n) === "LinesMesh"; + typeof flock.BABYLON !== 'undefined' && n instanceof flock.BABYLON.AbstractMesh; + const isLines = (n) => cls(n) === 'LinesMesh'; // --- Ghost: top-level + enabled + AbstractMesh + no material (not lines) const targets = scene.meshes.filter( - (m) => - !m.parent && - isEnabledDeep(m) && - isAbstractMesh(m) && - !isLines(m) && - !m.material, + (m) => !m.parent && isEnabledDeep(m) && isAbstractMesh(m) && !isLines(m) && !m.material ); // Shared transparent PBR material (GLTF-friendly) - const ghostMat = new flock.BABYLON.PBRMaterial("_tmpExportGhost", scene); + const ghostMat = new flock.BABYLON.PBRMaterial('_tmpExportGhost', scene); ghostMat.alpha = 0; ghostMat.alphaMode = flock.BABYLON.Engine.ALPHA_BLEND; - ghostMat.transparencyMode = - flock.BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND; + ghostMat.transparencyMode = flock.BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND; ghostMat.disableLighting = true; ghostMat.metallic = 0; ghostMat.roughness = 1; @@ -95,7 +82,7 @@ export const flockXR = { for (const { mesh } of patches) mesh.material = ghostMat; // Optional: name allowlist for safety (keeps ground even if disabled, if you want) - const alwaysKeepNames = new Set(["ground", "Ground"]); + const alwaysKeepNames = new Set(['ground', 'Ground']); const shouldExportNode = (node) => { const c = cls(node); @@ -108,13 +95,13 @@ export const flockXR = { if (!isEnabledDeep(node)) return false; // Never export cameras/lights - if (c === "Camera" || c === "Light") return false; + if (c === 'Camera' || c === 'Light') return false; // Skip line helpers entirely - if (c === "LinesMesh") return false; + if (c === 'LinesMesh') return false; // Keep all transform containers - if (c === "TransformNode") return true; + if (c === 'TransformNode') return true; // Keep ALL mesh subclasses (e.g., Mesh, InstancedMesh, GroundMesh, etc.) if (isAbstractMesh(node)) return true; @@ -122,7 +109,7 @@ export const flockXR = { return false; }; - flock.EXPORT.GLTF2Export.GLBAsync(scene, "scene.glb", { + flock.EXPORT.GLTF2Export.GLBAsync(scene, 'scene.glb', { exportMaterials: true, exportTextures: true, shouldExportNode, @@ -140,9 +127,7 @@ export const flockXR = { return new Promise((resolve) => { flock.whenModelReady(meshName, async function (mesh) { const anchorMesh = mesh; - const rootChild = anchorMesh - .getChildMeshes() - .find((child) => child.name === "__root__"); + const rootChild = anchorMesh.getChildMeshes().find((child) => child.name === '__root__'); const exportAnchors = [anchorMesh]; if (rootChild) { @@ -152,39 +137,27 @@ export const flockXR = { const allowedNodes = new Set(); for (const anchor of exportAnchors) { allowedNodes.add(anchor); - anchor - .getChildMeshes(false) - .forEach((childMesh) => allowedNodes.add(childMesh)); + anchor.getChildMeshes(false).forEach((childMesh) => allowedNodes.add(childMesh)); } const hasDirectRootChild = (node) => - typeof node?.getChildMeshes === "function" && - node.getChildMeshes(true).some((child) => child.name === "__root__"); + typeof node?.getChildMeshes === 'function' && + node.getChildMeshes(true).some((child) => child.name === '__root__'); const wrapperNodes = [...allowedNodes].filter( - (node) => node.name !== "__root__" && hasDirectRootChild(node), + (node) => node.name !== '__root__' && hasDirectRootChild(node) ); const childMeshes = mesh.getChildMeshes(false); const meshList = [mesh, ...childMeshes]; - if (format === "STL") { - flock.EXPORT.STLExport.CreateSTL( - meshList, - true, - mesh.name, - false, - false, - ); - } else if (format === "OBJ") { + if (format === 'STL') { + flock.EXPORT.STLExport.CreateSTL(meshList, true, mesh.name, false, false); + } else if (format === 'OBJ') { flock.EXPORT.OBJExport.OBJ(mesh); - } else if (format === "GLB") { - const ghostMat = new flock.BABYLON.PBRMaterial( - "_tmpExportWrapperGhost", - flock.scene, - ); + } else if (format === 'GLB') { + const ghostMat = new flock.BABYLON.PBRMaterial('_tmpExportWrapperGhost', flock.scene); ghostMat.alpha = 0; ghostMat.alphaMode = flock.BABYLON.Engine.ALPHA_BLEND; - ghostMat.transparencyMode = - flock.BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND; + ghostMat.transparencyMode = flock.BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND; ghostMat.disableLighting = true; ghostMat.metallic = 0; ghostMat.roughness = 1; @@ -200,13 +173,9 @@ export const flockXR = { mesh.flipFaces(); try { - await flock.EXPORT.GLTF2Export.GLBAsync( - flock.scene, - mesh.name + ".glb", - { - shouldExportNode: (node) => allowedNodes.has(node), - }, - ).then((glb) => { + await flock.EXPORT.GLTF2Export.GLBAsync(flock.scene, mesh.name + '.glb', { + shouldExportNode: (node) => allowedNodes.has(node), + }).then((glb) => { glb.downloadFiles(); }); } finally { diff --git a/blocks/animate.js b/blocks/animate.js index 9b908500..43111bc1 100644 --- a/blocks/animate.js +++ b/blocks/animate.js @@ -1,240 +1,236 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { nextVariableIndexes, handleBlockCreateEvent, getHelpUrlFor, registerBlockHandler, -} from "./blocks.js"; -import { animationNames } from "../config.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; +} from './blocks.js'; +import { animationNames } from '../config.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; export function defineAnimateBlocks() { - Blockly.Blocks["glide_to"] = { + Blockly.Blocks['glide_to'] = { init: function () { this.jsonInit({ - type: "glide_to", - message0: translate("glide_to"), + type: 'glide_to', + message0: translate('glide_to'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, - text: "reverse", + text: 'reverse', }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, - text: "loop", + text: 'loop', }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("Linear"), - getDropdownOption("SineEase"), - getDropdownOption("CubicEase"), - getDropdownOption("QuadraticEase"), - getDropdownOption("ExponentialEase"), - getDropdownOption("BounceEase"), - getDropdownOption("ElasticEase"), - getDropdownOption("BackEase"), + getDropdownOption('Linear'), + getDropdownOption('SineEase'), + getDropdownOption('CubicEase'), + getDropdownOption('QuadraticEase'), + getDropdownOption('ExponentialEase'), + getDropdownOption('BounceEase'), + getDropdownOption('ElasticEase'), + getDropdownOption('BackEase'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("glide_to"), + colour: categoryColours['Animate'], + tooltip: getTooltip('glide_to'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["glide_to_seconds"] = { + Blockly.Blocks['glide_to_seconds'] = { init: function () { this.jsonInit({ - type: "glide_to_seconds", - message0: translate("glide_to_seconds"), + type: 'glide_to_seconds', + message0: translate('glide_to_seconds'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, - text: "reverse", + text: 'reverse', }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, - text: "loop", + text: 'loop', }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("Linear"), - getDropdownOption("SineEase"), - getDropdownOption("CubicEase"), - getDropdownOption("QuadraticEase"), - getDropdownOption("ExponentialEase"), - getDropdownOption("BounceEase"), - getDropdownOption("ElasticEase"), - getDropdownOption("BackEase"), + getDropdownOption('Linear'), + getDropdownOption('SineEase'), + getDropdownOption('CubicEase'), + getDropdownOption('QuadraticEase'), + getDropdownOption('ExponentialEase'), + getDropdownOption('BounceEase'), + getDropdownOption('ElasticEase'), + getDropdownOption('BackEase'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("glide_to_seconds"), + colour: categoryColours['Animate'], + tooltip: getTooltip('glide_to_seconds'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["glide_to_object"] = { + Blockly.Blocks['glide_to_object'] = { init: function () { this.jsonInit({ - type: "glide_to_object", - message0: translate("glide_to_object"), + type: 'glide_to_object', + message0: translate('glide_to_object'), args0: [ { - type: "field_variable", - name: "MODEL1", + type: 'field_variable', + name: 'MODEL1', variable: window.currentMesh, }, { - type: "field_variable", - name: "MODEL2", - variable: "object2", + type: 'field_variable', + name: 'MODEL2', + variable: 'object2', }, { - type: "input_value", - name: "X_OFFSET", - check: "Number", + type: 'input_value', + name: 'X_OFFSET', + check: 'Number', }, { - type: "input_value", - name: "Y_OFFSET", - check: "Number", + type: 'input_value', + name: 'Y_OFFSET', + check: 'Number', }, { - type: "input_value", - name: "Z_OFFSET", - check: "Number", + type: 'input_value', + name: 'Z_OFFSET', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, - text: "reverse", + text: 'reverse', }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, - text: "loop", + text: 'loop', }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("Linear"), - getDropdownOption("SineEase"), - getDropdownOption("CubicEase"), - getDropdownOption("QuadraticEase"), - getDropdownOption("ExponentialEase"), - getDropdownOption("BounceEase"), - getDropdownOption("ElasticEase"), - getDropdownOption("BackEase"), + getDropdownOption('Linear'), + getDropdownOption('SineEase'), + getDropdownOption('CubicEase'), + getDropdownOption('QuadraticEase'), + getDropdownOption('ExponentialEase'), + getDropdownOption('BounceEase'), + getDropdownOption('ElasticEase'), + getDropdownOption('BackEase'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("glide_to_object"), + colour: categoryColours['Animate'], + tooltip: getTooltip('glide_to_object'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); const addZeroShadow = (inputName) => { const input = this.getInput(inputName); @@ -249,605 +245,601 @@ export function defineAnimateBlocks() { input.connection.respawnShadow_(); }; - ["X_OFFSET", "Y_OFFSET", "Z_OFFSET"].forEach(addZeroShadow); + ['X_OFFSET', 'Y_OFFSET', 'Z_OFFSET'].forEach(addZeroShadow); }, }; - Blockly.Blocks["glide_to_axis"] = { + Blockly.Blocks['glide_to_axis'] = { init: function () { this.jsonInit({ - type: "glide_to_axis", - message0: translate("glide_to_axis"), + type: 'glide_to_axis', + message0: translate('glide_to_axis'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "AXIS", + type: 'field_dropdown', + name: 'AXIS', options: [ - ["x", "x"], - ["y", "y"], - ["z", "z"], - getDropdownOption("forward"), - getDropdownOption("sideways"), + ['x', 'x'], + ['y', 'y'], + ['z', 'z'], + getDropdownOption('forward'), + getDropdownOption('sideways'), ], }, { - type: "input_value", - name: "TARGET", - check: "Number", + type: 'input_value', + name: 'TARGET', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, - text: "reverse", + text: 'reverse', }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, - text: "loop", + text: 'loop', }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("Linear"), - getDropdownOption("SineEase"), - getDropdownOption("CubicEase"), - getDropdownOption("QuadraticEase"), - getDropdownOption("ExponentialEase"), - getDropdownOption("BounceEase"), - getDropdownOption("ElasticEase"), - getDropdownOption("BackEase"), + getDropdownOption('Linear'), + getDropdownOption('SineEase'), + getDropdownOption('CubicEase'), + getDropdownOption('QuadraticEase'), + getDropdownOption('ExponentialEase'), + getDropdownOption('BounceEase'), + getDropdownOption('ElasticEase'), + getDropdownOption('BackEase'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("glide_to_axis"), + colour: categoryColours['Animate'], + tooltip: getTooltip('glide_to_axis'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["rotate_anim"] = { + Blockly.Blocks['rotate_anim'] = { init: function () { this.jsonInit({ - type: "rotate_anim", - message0: translate("rotate_anim"), + type: 'rotate_anim', + message0: translate('rotate_anim'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "ROT_X", - check: "Number", + type: 'input_value', + name: 'ROT_X', + check: 'Number', }, { - type: "input_value", - name: "ROT_Y", - check: "Number", + type: 'input_value', + name: 'ROT_Y', + check: 'Number', }, { - type: "input_value", - name: "ROT_Z", - check: "Number", + type: 'input_value', + name: 'ROT_Z', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, - text: "reverse", + text: 'reverse', }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, - text: "loop", + text: 'loop', }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("Linear"), - getDropdownOption("SineEase"), - getDropdownOption("CubicEase"), - getDropdownOption("QuadraticEase"), - getDropdownOption("ExponentialEase"), - getDropdownOption("BounceEase"), - getDropdownOption("ElasticEase"), - getDropdownOption("BackEase"), + getDropdownOption('Linear'), + getDropdownOption('SineEase'), + getDropdownOption('CubicEase'), + getDropdownOption('QuadraticEase'), + getDropdownOption('ExponentialEase'), + getDropdownOption('BounceEase'), + getDropdownOption('ElasticEase'), + getDropdownOption('BackEase'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("rotate_anim"), + colour: categoryColours['Animate'], + tooltip: getTooltip('rotate_anim'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["rotate_anim_seconds"] = { + Blockly.Blocks['rotate_anim_seconds'] = { init: function () { this.jsonInit({ - type: "rotate_anim_seconds", - message0: translate("rotate_anim_seconds"), + type: 'rotate_anim_seconds', + message0: translate('rotate_anim_seconds'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "ROT_X", - check: "Number", + type: 'input_value', + name: 'ROT_X', + check: 'Number', }, { - type: "input_value", - name: "ROT_Y", - check: "Number", + type: 'input_value', + name: 'ROT_Y', + check: 'Number', }, { - type: "input_value", - name: "ROT_Z", - check: "Number", + type: 'input_value', + name: 'ROT_Z', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, - text: "reverse", + text: 'reverse', }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, - text: "loop", + text: 'loop', }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("Linear"), - getDropdownOption("SineEase"), - getDropdownOption("CubicEase"), - getDropdownOption("QuadraticEase"), - getDropdownOption("ExponentialEase"), - getDropdownOption("BounceEase"), - getDropdownOption("ElasticEase"), - getDropdownOption("BackEase"), + getDropdownOption('Linear'), + getDropdownOption('SineEase'), + getDropdownOption('CubicEase'), + getDropdownOption('QuadraticEase'), + getDropdownOption('ExponentialEase'), + getDropdownOption('BounceEase'), + getDropdownOption('ElasticEase'), + getDropdownOption('BackEase'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("rotate_anim_seconds"), + colour: categoryColours['Animate'], + tooltip: getTooltip('rotate_anim_seconds'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["rotate_to_object"] = { + Blockly.Blocks['rotate_to_object'] = { init: function () { this.jsonInit({ - type: "rotate_to_object", - message0: translate("rotate_to_object"), + type: 'rotate_to_object', + message0: translate('rotate_to_object'), args0: [ { - type: "field_variable", - name: "MODEL1", + type: 'field_variable', + name: 'MODEL1', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "ROTATE_MODE", - options: [ - getDropdownOption("TOWARDS"), - getDropdownOption("SAME_ROTATION"), - ], + type: 'field_dropdown', + name: 'ROTATE_MODE', + options: [getDropdownOption('TOWARDS'), getDropdownOption('SAME_ROTATION')], }, { - type: "field_variable", - name: "MODEL2", - variable: "object2", + type: 'field_variable', + name: 'MODEL2', + variable: 'object2', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, - text: "reverse", + text: 'reverse', }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, - text: "loop", + text: 'loop', }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("Linear"), - getDropdownOption("SineEase"), - getDropdownOption("CubicEase"), - getDropdownOption("QuadraticEase"), - getDropdownOption("ExponentialEase"), - getDropdownOption("BounceEase"), - getDropdownOption("ElasticEase"), - getDropdownOption("BackEase"), + getDropdownOption('Linear'), + getDropdownOption('SineEase'), + getDropdownOption('CubicEase'), + getDropdownOption('QuadraticEase'), + getDropdownOption('ExponentialEase'), + getDropdownOption('BounceEase'), + getDropdownOption('ElasticEase'), + getDropdownOption('BackEase'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("rotate_to_object"), + colour: categoryColours['Animate'], + tooltip: getTooltip('rotate_to_object'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["animate_property"] = { + Blockly.Blocks['animate_property'] = { init: function () { this.jsonInit({ - type: "animate_property", - message0: translate("animate_property"), + type: 'animate_property', + message0: translate('animate_property'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "PROPERTY", + type: 'field_dropdown', + name: 'PROPERTY', options: [ - getDropdownOption("diffuseColor"), - getDropdownOption("emissiveColor"), - getDropdownOption("ambientColor"), - getDropdownOption("specularColor"), - getDropdownOption("alpha"), + getDropdownOption('diffuseColor'), + getDropdownOption('emissiveColor'), + getDropdownOption('ambientColor'), + getDropdownOption('specularColor'), + getDropdownOption('alpha'), ], }, { - type: "input_value", - name: "TO", + type: 'input_value', + name: 'TO', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, - text: "reverse", + text: 'reverse', }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, - text: "loop", + text: 'loop', }, { - type: "field_dropdown", - name: "START_AWAIT", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'START_AWAIT', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("animate_property"), + colour: categoryColours['Animate'], + tooltip: getTooltip('animate_property'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["colour_keyframe"] = { + Blockly.Blocks['colour_keyframe'] = { init: function () { this.jsonInit({ - type: "colour_keyframe", - message0: translate("colour_keyframe"), + type: 'colour_keyframe', + message0: translate('colour_keyframe'), args0: [ { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "input_value", - name: "VALUE", - check: "Colour", // Reusing your existing colour block + type: 'input_value', + name: 'VALUE', + check: 'Colour', // Reusing your existing colour block }, ], - colour: categoryColours["Animate"], + colour: categoryColours['Animate'], inputsInline: true, - output: "Keyframe", - tooltip: getTooltip("colour_keyframe"), + output: 'Keyframe', + tooltip: getTooltip('colour_keyframe'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["number_keyframe"] = { + Blockly.Blocks['number_keyframe'] = { init: function () { this.jsonInit({ - type: "number_keyframe", - message0: translate("number_keyframe"), + type: 'number_keyframe', + message0: translate('number_keyframe'), args0: [ { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "input_value", - name: "VALUE", - check: "Number", // Reusing your existing colour block + type: 'input_value', + name: 'VALUE', + check: 'Number', // Reusing your existing colour block }, ], - colour: categoryColours["Animate"], + colour: categoryColours['Animate'], inputsInline: true, - output: "Keyframe", - tooltip: getTooltip("number_keyframe"), + output: 'Keyframe', + tooltip: getTooltip('number_keyframe'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["xyz_keyframe"] = { + Blockly.Blocks['xyz_keyframe'] = { init: function () { this.jsonInit({ - type: "xyz_keyframe", - message0: translate("xyz_keyframe"), + type: 'xyz_keyframe', + message0: translate('xyz_keyframe'), args0: [ { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], - colour: categoryColours["Animate"], + colour: categoryColours['Animate'], inputsInline: true, - output: "Keyframe", - tooltip: getTooltip("xyz_keyframe"), + output: 'Keyframe', + tooltip: getTooltip('xyz_keyframe'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["animate_keyframes"] = { + Blockly.Blocks['animate_keyframes'] = { init: function () { this.jsonInit({ - type: "animate_keyframes", - message0: translate("animate_keyframes"), + type: 'animate_keyframes', + message0: translate('animate_keyframes'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, // Assuming current mesh is stored here }, { - type: "field_dropdown", - name: "PROPERTY", + type: 'field_dropdown', + name: 'PROPERTY', options: [ - getDropdownOption("color"), - getDropdownOption("alpha"), - getDropdownOption("position"), - getDropdownOption("rotation"), - getDropdownOption("scaling"), + getDropdownOption('color'), + getDropdownOption('alpha'), + getDropdownOption('position'), + getDropdownOption('rotation'), + getDropdownOption('scaling'), ], }, { - type: "input_value", - name: "KEYFRAMES", - check: "Array", // Accepts an array of keyframes + type: 'input_value', + name: 'KEYFRAMES', + check: 'Array', // Accepts an array of keyframes }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("LINEAR"), - getDropdownOption("EASEIN"), - getDropdownOption("EASEOUT"), - getDropdownOption("EASEINOUT"), + getDropdownOption('LINEAR'), + getDropdownOption('EASEIN'), + getDropdownOption('EASEOUT'), + getDropdownOption('EASEINOUT'), ], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, // Checkbox for reversing the animation }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, // Checkbox for looping }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("AWAIT"), getDropdownOption("START")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('AWAIT'), getDropdownOption('START')], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("animate_keyframes"), + colour: categoryColours['Animate'], + tooltip: getTooltip('animate_keyframes'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["animation"] = { + Blockly.Blocks['animation'] = { init: function () { - const variableNamePrefix = "animation"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'animation'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "animation", - message0: translate("animation"), + type: 'animation', + message0: translate('animation'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, // Assuming current mesh is stored here }, { - type: "field_dropdown", - name: "PROPERTY", + type: 'field_dropdown', + name: 'PROPERTY', options: [ - getDropdownOption("color"), - getDropdownOption("alpha"), - getDropdownOption("position"), - getDropdownOption("rotation"), - getDropdownOption("scaling"), - getDropdownOption("position.x"), - getDropdownOption("position.y"), - getDropdownOption("position.z"), - getDropdownOption("rotation.x"), - getDropdownOption("rotation.y"), - getDropdownOption("rotation.z"), - getDropdownOption("scaling.x"), - getDropdownOption("scaling.y"), - getDropdownOption("scaling.z"), + getDropdownOption('color'), + getDropdownOption('alpha'), + getDropdownOption('position'), + getDropdownOption('rotation'), + getDropdownOption('scaling'), + getDropdownOption('position.x'), + getDropdownOption('position.y'), + getDropdownOption('position.z'), + getDropdownOption('rotation.x'), + getDropdownOption('rotation.y'), + getDropdownOption('rotation.z'), + getDropdownOption('scaling.x'), + getDropdownOption('scaling.y'), + getDropdownOption('scaling.z'), ], }, { - type: "field_variable", - name: "ANIMATION_GROUP", + type: 'field_variable', + name: 'ANIMATION_GROUP', variable: nextVariableName, // Use the dynamic variable name }, { - type: "input_value", - name: "KEYFRAMES", - check: "Array", + type: 'input_value', + name: 'KEYFRAMES', + check: 'Array', }, { - type: "field_dropdown", - name: "EASING", + type: 'field_dropdown', + name: 'EASING', options: [ - getDropdownOption("LINEAR"), - getDropdownOption("EASEIN"), - getDropdownOption("EASEOUT"), - getDropdownOption("EASEINOUT"), + getDropdownOption('LINEAR'), + getDropdownOption('EASEIN'), + getDropdownOption('EASEOUT'), + getDropdownOption('EASEINOUT'), ], }, { - type: "field_checkbox", - name: "REVERSE", + type: 'field_checkbox', + name: 'REVERSE', checked: false, }, { - type: "field_checkbox", - name: "LOOP", + type: 'field_checkbox', + name: 'LOOP', checked: false, }, { - type: "field_dropdown", - name: "MODE", + type: 'field_dropdown', + name: 'MODE', options: [ - getDropdownOption("AWAIT"), - getDropdownOption("START"), - getDropdownOption("CREATE"), + getDropdownOption('AWAIT'), + getDropdownOption('START'), + getDropdownOption('CREATE'), ], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("animation"), + colour: categoryColours['Animate'], + tooltip: getTooltip('animation'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); registerBlockHandler(this, (changeEvent) => { handleBlockCreateEvent( this, changeEvent, variableNamePrefix, nextVariableIndexes, - "ANIMATION_GROUP", + 'ANIMATION_GROUP' ); if (window.loadingCode) return; @@ -855,160 +847,160 @@ export function defineAnimateBlocks() { }, }; - Blockly.Blocks["control_animation_group"] = { + Blockly.Blocks['control_animation_group'] = { init: function () { this.jsonInit({ - type: "animation_group_control", - message0: translate("control_animation_group"), + type: 'animation_group_control', + message0: translate('control_animation_group'), args0: [ { - type: "field_variable", - name: "GROUP_NAME", - variable: "animation1", + type: 'field_variable', + name: 'GROUP_NAME', + variable: 'animation1', }, { - type: "field_dropdown", - name: "ACTION", + type: 'field_dropdown', + name: 'ACTION', options: [ - getDropdownOption("play"), - getDropdownOption("pause"), - getDropdownOption("stop"), + getDropdownOption('play'), + getDropdownOption('pause'), + getDropdownOption('stop'), ], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("control_animation_group"), + colour: categoryColours['Animate'], + tooltip: getTooltip('control_animation_group'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["animate_from"] = { + Blockly.Blocks['animate_from'] = { init: function () { this.jsonInit({ - type: "animate_from", - message0: translate("animate_from"), + type: 'animate_from', + message0: translate('animate_from'), args0: [ { - type: "field_variable", - name: "GROUP_NAME", - variable: "animation1", + type: 'field_variable', + name: 'GROUP_NAME', + variable: 'animation1', }, { - type: "input_value", - name: "TIME", - check: "Number", + type: 'input_value', + name: 'TIME', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("animate_from"), + colour: categoryColours['Animate'], + tooltip: getTooltip('animate_from'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["stop_animations"] = { + Blockly.Blocks['stop_animations'] = { init: function () { this.jsonInit({ - type: "stop_animations", - message0: translate("stop_animations"), + type: 'stop_animations', + message0: translate('stop_animations'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("stop_animations"), + colour: categoryColours['Animate'], + tooltip: getTooltip('stop_animations'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["animation_name"] = { + Blockly.Blocks['animation_name'] = { init: function () { this.jsonInit({ - type: "animation_name", - message0: "%1", + type: 'animation_name', + message0: '%1', args0: [ { - type: "field_dropdown", - name: "ANIMATION_NAME", + type: 'field_dropdown', + name: 'ANIMATION_NAME', options: animationNames(), }, ], - output: "String", - colour: categoryColours["Animate"], - tooltip: getTooltip("play_animation"), + output: 'String', + colour: categoryColours['Animate'], + tooltip: getTooltip('play_animation'), }); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["switch_animation"] = { + Blockly.Blocks['switch_animation'] = { init: function () { this.jsonInit({ - type: "switch_model_animation", - message0: translate("switch_animation"), + type: 'switch_model_animation', + message0: translate('switch_animation'), args0: [ { - type: "input_value", - name: "ANIMATION_NAME", - check: "String", + type: 'input_value', + name: 'ANIMATION_NAME', + check: 'String', }, { - type: "field_variable", - name: "MODEL", + type: 'field_variable', + name: 'MODEL', variable: window.currentMesh, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("switch_animation"), + colour: categoryColours['Animate'], + tooltip: getTooltip('switch_animation'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; - Blockly.Blocks["play_animation"] = { + Blockly.Blocks['play_animation'] = { init: function () { this.jsonInit({ - type: "play_model_animation_once", - message0: translate("play_animation"), + type: 'play_model_animation_once', + message0: translate('play_animation'), args0: [ { - type: "input_value", - name: "ANIMATION_NAME", - check: "String", + type: 'input_value', + name: 'ANIMATION_NAME', + check: 'String', }, { - type: "field_variable", - name: "MODEL", + type: 'field_variable', + name: 'MODEL', variable: window.currentMesh, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Animate"], - tooltip: getTooltip("play_animation"), + colour: categoryColours['Animate'], + tooltip: getTooltip('play_animation'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("animate_blocks"); + this.setStyle('animate_blocks'); }, }; } diff --git a/blocks/base.js b/blocks/base.js index 4acc1674..377f81b8 100644 --- a/blocks/base.js +++ b/blocks/base.js @@ -1,26 +1,26 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { translate, getTooltip } from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { translate, getTooltip } from '../main/translation.js'; -window.currentMesh = "object"; +window.currentMesh = 'object'; window.currentBlock = null; export function defineBaseBlocks() { - Blockly.Blocks["xyz"] = { + Blockly.Blocks['xyz'] = { init: function () { this.jsonInit({ - type: "xyz", - message0: translate("xyz"), + type: 'xyz', + message0: translate('xyz'), args0: [ - { type: "input_value", name: "X", check: "Number" }, - { type: "input_value", name: "Y", check: "Number" }, - { type: "input_value", name: "Z", check: "Number" }, + { type: 'input_value', name: 'X', check: 'Number' }, + { type: 'input_value', name: 'Y', check: 'Number' }, + { type: 'input_value', name: 'Z', check: 'Number' }, ], inputsInline: true, - output: "Vector", - colour: categoryColours["Transform"], - tooltip: getTooltip("xyz"), - helpUrl: "", + output: 'Vector', + colour: categoryColours['Transform'], + tooltip: getTooltip('xyz'), + helpUrl: '', }); }, }; diff --git a/blocks/blockIcons.js b/blocks/blockIcons.js index ee9c988a..69df7e8a 100644 --- a/blocks/blockIcons.js +++ b/blocks/blockIcons.js @@ -1,5 +1,5 @@ -import * as Blockly from "blockly"; -import { toolbox as toolboxDefinition } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { toolbox as toolboxDefinition } from '../toolbox.js'; // A purely decorative image field (block-type and category icons). The default // FieldImage contributes an "empty" placeholder to the block's ARIA label when @@ -7,12 +7,12 @@ import { toolbox as toolboxDefinition } from "../toolbox.js"; // the icon is silent to screen readers. export class DecorativeFieldImage extends Blockly.FieldImage { computeAriaLabel() { - return ""; + return ''; } } function buildSvgDataUri(svgContent) { - return "data:image/svg+xml," + encodeURIComponent(svgContent); + return 'data:image/svg+xml,' + encodeURIComponent(svgContent); } /** @@ -20,10 +20,9 @@ function buildSvgDataUri(svgContent) { * Returns a value in [0, 1] (0 = black, 1 = white). */ function relativeLuminance(hex) { - const clean = hex.replace("#", ""); + const clean = hex.replace('#', ''); if (clean.length !== 6) return 0; - const toLinear = (c) => - c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)); const r = toLinear(parseInt(clean.slice(0, 2), 16) / 255); const g = toLinear(parseInt(clean.slice(2, 4), 16) / 255); const b = toLinear(parseInt(clean.slice(4, 6), 16) / 255); @@ -31,52 +30,52 @@ function relativeLuminance(hex) { } export function getIconColorForBackground(hexColor) { - if (!hexColor || typeof hexColor !== "string") return "white"; + if (!hexColor || typeof hexColor !== 'string') return 'white'; const L = relativeLuminance(hexColor); - return L > 0.179 ? "black" : "white"; + return L > 0.179 ? 'black' : 'white'; } export function makeIconDataUrl(viewBox, pathD, fillColor) { return buildSvgDataUri( - ``, + `` ); } function makePathIcon(viewBox, pathD, color) { return buildSvgDataUri( - ``, + `` ); } const ICON_PATHS = { start: { - viewBox: "0 0 384 512", - d: "M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z", + viewBox: '0 0 384 512', + d: 'M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z', }, repeat: { - viewBox: "0 0 720 512", - d: "M470.6 118.6c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S352 19.1 352 32l0 32-160 0C86 64 0 150 0 256 0 273.7 14.3 288 32 288s32-14.3 32-32c0-70.7 57.3-128 128-128l160 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64zM41.4 393.4c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9S160 492.9 160 480l0-32 160 0c106 0 192-86 192-192 0-17.7-14.3-32-32-32s-32 14.3-32 32c0 70.7-57.3 128-128 128l-160 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64z", + viewBox: '0 0 720 512', + d: 'M470.6 118.6c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S352 19.1 352 32l0 32-160 0C86 64 0 150 0 256 0 273.7 14.3 288 32 288s32-14.3 32-32c0-70.7 57.3-128 128-128l160 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64zM41.4 393.4c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9S160 492.9 160 480l0-32 160 0c106 0 192-86 192-192 0-17.7-14.3-32-32-32s-32 14.3-32 32c0 70.7-57.3 128-128 128l-160 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64z', }, click: { - viewBox: "0 0 448 512", - d: "M429.6 92.1c4.9-11.9 2.1-25.6-7-34.7s-22.8-11.9-34.7-7l-352 144c-14.2 5.8-22.2 20.8-19.3 35.8s16.1 25.8 31.4 25.8l176 0 0 176c0 15.3 10.8 28.4 25.8 31.4s30-5.1 35.8-19.3l144-352z", + viewBox: '0 0 448 512', + d: 'M429.6 92.1c4.9-11.9 2.1-25.6-7-34.7s-22.8-11.9-34.7-7l-352 144c-14.2 5.8-22.2 20.8-19.3 35.8s16.1 25.8 31.4 25.8l176 0 0 176c0 15.3 10.8 28.4 25.8 31.4s30-5.1 35.8-19.3l144-352z', }, collision: { - viewBox: "0 0 448 512", - d: "M349.4 44.6c5.9-13.7 1.5-29.7-10.6-38.5s-28.6-8-39.9 1.8l-256 224c-10 8.8-13.6 22.9-8.9 35.3S50.7 288 64 288H175.5L98.6 467.4c-5.9 13.7-1.5 29.7 10.6 38.5s28.6 8 39.9-1.8l256-224c10-8.8 13.6-22.9 8.9-35.3s-16.6-20.7-30-20.7H272.5L349.4 44.6z", + viewBox: '0 0 448 512', + d: 'M349.4 44.6c5.9-13.7 1.5-29.7-10.6-38.5s-28.6-8-39.9 1.8l-256 224c-10 8.8-13.6 22.9-8.9 35.3S50.7 288 64 288H175.5L98.6 467.4c-5.9 13.7-1.5 29.7 10.6 38.5s28.6 8 39.9-1.8l256-224c10-8.8 13.6-22.9 8.9-35.3s-16.6-20.7-30-20.7H272.5L349.4 44.6z', }, keyboard: { - viewBox: "0 0 576 512", - d: "M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zm16 64h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zm0 96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V240c0-8.8 7.2-16 16-16zm16 160c-8.8 0-16-7.2-16-16v-32h224v32c0 8.8-7.2 16-16 16H96zm176-160c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H288c-8.8 0-16-7.2-16-16V240zm16-96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H288c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM160 144c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V144zm16 80h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V240c0-8.8 7.2-16 16-16zm176 96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H352c-8.8 0-16-7.2-16-16V320c0-8.8 7.2-16 16-16zm16-96c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H368c-8.8 0-16-7.2-16-16V240zm16-96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H368c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zm80 96c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V240zm16-96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16z", + viewBox: '0 0 576 512', + d: 'M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zm16 64h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zm0 96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V240c0-8.8 7.2-16 16-16zm16 160c-8.8 0-16-7.2-16-16v-32h224v32c0 8.8-7.2 16-16 16H96zm176-160c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H288c-8.8 0-16-7.2-16-16V240zm16-96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H288c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zM160 144c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V144zm16 80h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V240c0-8.8 7.2-16 16-16zm176 96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H352c-8.8 0-16-7.2-16-16V320c0-8.8 7.2-16 16-16zm16-96c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H368c-8.8 0-16-7.2-16-16V240zm16-96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H368c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zm80 96c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V240zm16-96h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H464c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16z', }, press: { - viewBox: "0 0 640 512", - d: "M256 0a256 256 0 1 0 0 512 256 256 0 1 0 0-512zM244.7 387.3l-104-104c-4.6-4.6-5.9-11.5-3.5-17.4s8.3-9.9 14.8-9.9l56 0 0-96c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 96 56 0c6.5 0 12.3 3.9 14.8 9.9s1.1 12.9-3.5 17.4l-104 104c-6.2 6.2-16.4 6.2-22.6 0z", + viewBox: '0 0 640 512', + d: 'M256 0a256 256 0 1 0 0 512 256 256 0 1 0 0-512zM244.7 387.3l-104-104c-4.6-4.6-5.9-11.5-3.5-17.4s8.3-9.9 14.8-9.9l56 0 0-96c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 96 56 0c6.5 0 12.3 3.9 14.8 9.9s1.1 12.9-3.5 17.4l-104 104c-6.2 6.2-16.4 6.2-22.6 0z', }, event: { - viewBox: "0 0 640 640", - d: "M352 96l64 0c17.7 0 32 14.3 32 32l0 256c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0c53 0 96-43 96-96l0-256c0-53-43-96-96-96l-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32zm-9.4 182.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L242.7 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l210.7 0-73.4 73.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l128-128z", + viewBox: '0 0 640 640', + d: 'M352 96l64 0c17.7 0 32 14.3 32 32l0 256c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0c53 0 96-43 96-96l0-256c0-53-43-96-96-96l-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32zm-9.4 182.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L242.7 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l210.7 0-73.4 73.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l128-128z', }, }; @@ -87,21 +86,13 @@ export function makeRepeatIcon(color) { return makePathIcon(ICON_PATHS.repeat.viewBox, ICON_PATHS.repeat.d, color); } export function makeClickIcon(color) { - return makePathIcon( - ICON_PATHS.click.viewBox, - ICON_PATHS.click.d, - color, - ); + return makePathIcon(ICON_PATHS.click.viewBox, ICON_PATHS.click.d, color); } export function makeCollisionIcon(color) { return makePathIcon(ICON_PATHS.collision.viewBox, ICON_PATHS.collision.d, color); } export function makeKeyboardIcon(color) { - return makePathIcon( - ICON_PATHS.keyboard.viewBox, - ICON_PATHS.keyboard.d, - color, - ); + return makePathIcon(ICON_PATHS.keyboard.viewBox, ICON_PATHS.keyboard.d, color); } export function makePressIcon(color) { return makePathIcon(ICON_PATHS.press.viewBox, ICON_PATHS.press.d, color); @@ -110,7 +101,7 @@ export function makeOnEventIcon(color) { return makePathIcon(ICON_PATHS.event.viewBox, ICON_PATHS.event.d, color); } -let _currentIconColor = "white"; +let _currentIconColor = 'white'; export function setCurrentIconColor(color) { _currentIconColor = color; @@ -120,77 +111,77 @@ export function getCurrentIconColor() { return _currentIconColor; } -export const startIcon = makeStartIcon("white"); -export const repeatIcon = makeRepeatIcon("white"); -export const clickIcon = makeClickIcon("white"); -export const collisionIcon = makeCollisionIcon("white"); -export const keyboardIcon = makeKeyboardIcon("white"); -export const pressIcon = makePressIcon("white"); -export const eventIcon = makeOnEventIcon("white"); +export const startIcon = makeStartIcon('white'); +export const repeatIcon = makeRepeatIcon('white'); +export const clickIcon = makeClickIcon('white'); +export const collisionIcon = makeCollisionIcon('white'); +export const keyboardIcon = makeKeyboardIcon('white'); +export const pressIcon = makePressIcon('white'); +export const eventIcon = makeOnEventIcon('white'); -export const BLOCK_ICON_FIELD_NAME = "BLOCK_ICON"; -export const TOGGLE_BUTTON_FIELD_NAME = "TOGGLE_BUTTON"; -const LOW_VISION_ICON_FIELD_NAME = "LOW_VISION_CATEGORY_ICON"; -const LOW_VISION_BAR_FIELD_NAME = "LOW_VISION_CATEGORY_BAR"; +export const BLOCK_ICON_FIELD_NAME = 'BLOCK_ICON'; +export const TOGGLE_BUTTON_FIELD_NAME = 'TOGGLE_BUTTON'; +const LOW_VISION_ICON_FIELD_NAME = 'LOW_VISION_CATEGORY_ICON'; +const LOW_VISION_BAR_FIELD_NAME = 'LOW_VISION_CATEGORY_BAR'; const CATEGORY_ICON_PATH_BY_STYLE = { - events_blocks: "../images/events.svg", - scene_blocks: "../images/scene.svg", - scene_meshes_blocks: "../images/meshes.svg", - scene_xr_blocks: "../images/xr.svg", - scene_lights_blocks: "../images/lights.svg", - scene_camera_blocks: "../images/camera.svg", - transform_blocks: "../images/motion.svg", - transform_physics_blocks: "../images/physics.svg", - transform_connect_blocks: "../images/connect.svg", - transform_combine_blocks: "../images/combine.svg", - animate_blocks: "../images/animate.svg", - animate_keyframe_blocks: "../images/keyframe.svg", - materials_blocks: "../images/looks.svg", - sound_blocks: "../images/sound.svg", - sensing_blocks: "../images/sensing.svg", - snippets_blocks: "../images/snippets.svg", - snippets_physics_blocks: "../images/physics.svg", - snippets_arrows_blocks: "../images/arrows.svg", - control_blocks: "../images/control.svg", - logic_blocks: "../images/conditions.svg", - variable_blocks: "../images/variables.svg", - variables_blocks: "../images/variables.svg", - text_blocks: "../images/text.svg", - list_blocks: "../images/lists.svg", - lists_blocks: "../images/lists.svg", - math_blocks: "../images/math.svg", - procedure_blocks: "../images/functions.svg", + events_blocks: '../images/events.svg', + scene_blocks: '../images/scene.svg', + scene_meshes_blocks: '../images/meshes.svg', + scene_xr_blocks: '../images/xr.svg', + scene_lights_blocks: '../images/lights.svg', + scene_camera_blocks: '../images/camera.svg', + transform_blocks: '../images/motion.svg', + transform_physics_blocks: '../images/physics.svg', + transform_connect_blocks: '../images/connect.svg', + transform_combine_blocks: '../images/combine.svg', + animate_blocks: '../images/animate.svg', + animate_keyframe_blocks: '../images/keyframe.svg', + materials_blocks: '../images/looks.svg', + sound_blocks: '../images/sound.svg', + sensing_blocks: '../images/sensing.svg', + snippets_blocks: '../images/snippets.svg', + snippets_physics_blocks: '../images/physics.svg', + snippets_arrows_blocks: '../images/arrows.svg', + control_blocks: '../images/control.svg', + logic_blocks: '../images/conditions.svg', + variable_blocks: '../images/variables.svg', + variables_blocks: '../images/variables.svg', + text_blocks: '../images/text.svg', + list_blocks: '../images/lists.svg', + lists_blocks: '../images/lists.svg', + math_blocks: '../images/math.svg', + procedure_blocks: '../images/functions.svg', }; const CATEGORY_ACCENT_BY_STYLE = { - events_blocks: "#d99d98", - scene_blocks: "#bed998", - scene_meshes_blocks: "#bed998", - scene_xr_blocks: "#bed998", - scene_lights_blocks: "#bed998", - scene_camera_blocks: "#bed998", - transform_blocks: "#d3d998", - transform_physics_blocks: "#d3d998", - transform_connect_blocks: "#d3d998", - transform_combine_blocks: "#d3d998", - animate_blocks: "#d9c898", - animate_keyframe_blocks: "#d9c898", - materials_blocks: "#c398d9", - sound_blocks: "#d9b398", - sensing_blocks: "#98d9d9", - snippets_blocks: "#98c3d9", - snippets_physics_blocks: "#98c3d9", - snippets_arrows_blocks: "#98c3d9", - control_blocks: "#98d998", - logic_blocks: "#98b8d9", - variable_blocks: "#d998b8", - variables_blocks: "#d998b8", - text_blocks: "#98d9c3", - list_blocks: "#ad98d9", - lists_blocks: "#ad98d9", - math_blocks: "#98a3d9", - procedure_blocks: "#ce98d9", + events_blocks: '#d99d98', + scene_blocks: '#bed998', + scene_meshes_blocks: '#bed998', + scene_xr_blocks: '#bed998', + scene_lights_blocks: '#bed998', + scene_camera_blocks: '#bed998', + transform_blocks: '#d3d998', + transform_physics_blocks: '#d3d998', + transform_connect_blocks: '#d3d998', + transform_combine_blocks: '#d3d998', + animate_blocks: '#d9c898', + animate_keyframe_blocks: '#d9c898', + materials_blocks: '#c398d9', + sound_blocks: '#d9b398', + sensing_blocks: '#98d9d9', + snippets_blocks: '#98c3d9', + snippets_physics_blocks: '#98c3d9', + snippets_arrows_blocks: '#98c3d9', + control_blocks: '#98d998', + logic_blocks: '#98b8d9', + variable_blocks: '#d998b8', + variables_blocks: '#d998b8', + text_blocks: '#98d9c3', + list_blocks: '#ad98d9', + lists_blocks: '#ad98d9', + math_blocks: '#98a3d9', + procedure_blocks: '#ce98d9', }; const LOW_VISION_ICON_DATA_URL_BY_STYLE = new Map(); const LOW_VISION_ICON_SVG_BY_STYLE = new Map(); @@ -198,43 +189,43 @@ const LOW_VISION_ICON_LOAD_PROMISE_BY_STYLE = new Map(); const LOW_VISION_REFRESH_PENDING_BY_WORKSPACE = new WeakMap(); const LOW_VISION_STYLE_BY_ICON_FILE = { - "events.svg": "events_blocks", - "scene.svg": "scene_blocks", - "meshes.svg": "scene_meshes_blocks", - "xr.svg": "scene_xr_blocks", - "lights.svg": "scene_lights_blocks", - "camera.svg": "scene_camera_blocks", - "motion.svg": "transform_blocks", - "physics.svg": "transform_physics_blocks", - "connect.svg": "transform_connect_blocks", - "combine.svg": "transform_combine_blocks", - "animate.svg": "animate_blocks", - "keyframe.svg": "animate_keyframe_blocks", - "looks.svg": "materials_blocks", - "sound.svg": "sound_blocks", - "sensing.svg": "sensing_blocks", - "snippets.svg": "snippets_blocks", - "arrows.svg": "snippets_arrows_blocks", - "control.svg": "control_blocks", - "conditions.svg": "logic_blocks", - "variables.svg": "variable_blocks", - "data.svg": "variable_blocks", - "text.svg": "text_blocks", - "lists.svg": "list_blocks", - "math.svg": "math_blocks", - "functions.svg": "procedure_blocks", + 'events.svg': 'events_blocks', + 'scene.svg': 'scene_blocks', + 'meshes.svg': 'scene_meshes_blocks', + 'xr.svg': 'scene_xr_blocks', + 'lights.svg': 'scene_lights_blocks', + 'camera.svg': 'scene_camera_blocks', + 'motion.svg': 'transform_blocks', + 'physics.svg': 'transform_physics_blocks', + 'connect.svg': 'transform_connect_blocks', + 'combine.svg': 'transform_combine_blocks', + 'animate.svg': 'animate_blocks', + 'keyframe.svg': 'animate_keyframe_blocks', + 'looks.svg': 'materials_blocks', + 'sound.svg': 'sound_blocks', + 'sensing.svg': 'sensing_blocks', + 'snippets.svg': 'snippets_blocks', + 'arrows.svg': 'snippets_arrows_blocks', + 'control.svg': 'control_blocks', + 'conditions.svg': 'logic_blocks', + 'variables.svg': 'variable_blocks', + 'data.svg': 'variable_blocks', + 'text.svg': 'text_blocks', + 'lists.svg': 'list_blocks', + 'math.svg': 'math_blocks', + 'functions.svg': 'procedure_blocks', }; const LOW_VISION_STYLE_BY_BLOCK_TYPE = buildLowVisionStyleByBlockType(); const LOW_VISION_SUBCATEGORY_STYLE_OVERRIDES = new Set([ - "scene_blocks", - "transform_blocks", - "animate_blocks", - "snippets_blocks", + 'scene_blocks', + 'transform_blocks', + 'animate_blocks', + 'snippets_blocks', ]); export function makeInlineIcon(color) { const svg = ``; - return "data:image/svg+xml," + encodeURIComponent(svg); + return 'data:image/svg+xml,' + encodeURIComponent(svg); } const BLOCK_ICON_MAKERS = { @@ -251,7 +242,7 @@ export function updateBlockIcons(workspace, iconColor) { if (!workspace) return; const blocks = getWorkspaceAndFlyoutBlocks(workspace); for (const block of blocks) { - if (!block || typeof block.getField !== "function") continue; + if (!block || typeof block.getField !== 'function') continue; const iconField = block.getField(BLOCK_ICON_FIELD_NAME); if (iconField) { const maker = BLOCK_ICON_MAKERS[block.type]; @@ -267,11 +258,11 @@ export function updateAllBlockIcons(workspace, iconColor) { } function getBlockStyleName(block) { - if (!block) return ""; - if (typeof block.getStyleName === "function") { - return block.getStyleName() || ""; + if (!block) return ''; + if (typeof block.getStyleName === 'function') { + return block.getStyleName() || ''; } - return block.styleName_ || ""; + return block.styleName_ || ''; } function scheduleLowVisionIconRefresh(workspace, styleName) { @@ -307,42 +298,41 @@ function getLowVisionStyleNameForBlock(block) { if (explicitSubcategoryStyle) return explicitSubcategoryStyle; if (styleName) return styleName; - const blockType = block?.type || ""; - if (blockType.startsWith("lists_")) return "list_blocks"; - if (blockType.startsWith("variables_")) return "variable_blocks"; + const blockType = block?.type || ''; + if (blockType.startsWith('lists_')) return 'list_blocks'; + if (blockType.startsWith('variables_')) return 'variable_blocks'; return styleName; } function withSvgFill(svg, fillColor) { - if (!svg || !fillColor || !svg.includes("*{fill:${fillColor} !important;}`; - if (svg.includes("")) { + if (svg.includes('')) { return svg.replace(/]*)>/, `${fillStyle}`); } return svg; } function getStyleNameFromIconPath(iconPath) { - const iconName = (iconPath || "").toLowerCase().split("/").pop(); - return LOW_VISION_STYLE_BY_ICON_FILE[iconName] || ""; + const iconName = (iconPath || '').toLowerCase().split('/').pop(); + return LOW_VISION_STYLE_BY_ICON_FILE[iconName] || ''; } function buildLowVisionStyleByBlockType() { const byType = {}; - const walkToolbox = (items, inheritedStyle = "") => { + const walkToolbox = (items, inheritedStyle = '') => { if (!Array.isArray(items)) return; for (const item of items) { - if (!item || typeof item !== "object") continue; + if (!item || typeof item !== 'object') continue; - if (item.kind === "category") { - const categoryStyle = - getStyleNameFromIconPath(item.icon) || inheritedStyle; + if (item.kind === 'category') { + const categoryStyle = getStyleNameFromIconPath(item.icon) || inheritedStyle; walkToolbox(item.contents, categoryStyle); continue; } - if (item.kind === "block" && item.type && inheritedStyle) { + if (item.kind === 'block' && item.type && inheritedStyle) { byType[item.type] = inheritedStyle; } } @@ -359,10 +349,10 @@ export function makeLowVisionCategoryIconDataUrl(styleName) { const iconSvg = LOW_VISION_ICON_SVG_BY_STYLE.get(styleName); if (!iconSvg) { loadLowVisionCategoryIconSvg(styleName); - return ""; + return ''; } const accent = CATEGORY_ACCENT_BY_STYLE[styleName]; - if (!iconSvg || !accent) return ""; + if (!iconSvg || !accent) return ''; const dataUrl = buildSvgDataUri(withSvgFill(iconSvg, accent)); LOW_VISION_ICON_DATA_URL_BY_STYLE.set(styleName, dataUrl); return dataUrl; @@ -376,26 +366,26 @@ function loadLowVisionCategoryIconSvg(styleName) { return LOW_VISION_ICON_LOAD_PROMISE_BY_STYLE.get(styleName); } const iconPath = CATEGORY_ICON_PATH_BY_STYLE[styleName]; - if (!iconPath) return Promise.resolve(""); + if (!iconPath) return Promise.resolve(''); const iconUrl = new URL(iconPath, import.meta.url).href; const loadPromise = fetch(iconUrl) - .then((response) => (response.ok ? response.text() : "")) + .then((response) => (response.ok ? response.text() : '')) .then((svgText) => { - LOW_VISION_ICON_SVG_BY_STYLE.set(styleName, svgText || ""); + LOW_VISION_ICON_SVG_BY_STYLE.set(styleName, svgText || ''); LOW_VISION_ICON_LOAD_PROMISE_BY_STYLE.delete(styleName); - return svgText || ""; + return svgText || ''; }) .catch(() => { LOW_VISION_ICON_LOAD_PROMISE_BY_STYLE.delete(styleName); - return ""; + return ''; }); LOW_VISION_ICON_LOAD_PROMISE_BY_STYLE.set(styleName, loadPromise); return loadPromise; } export function preloadLowVisionCategoryIcons() { - if (typeof Image === "undefined" || typeof fetch !== "function") { + if (typeof Image === 'undefined' || typeof fetch !== 'function') { return Promise.resolve(); } const pendingDecodes = []; @@ -405,12 +395,12 @@ export function preloadLowVisionCategoryIcons() { const dataUrl = makeLowVisionCategoryIconDataUrl(styleName); if (!dataUrl) return; const img = new Image(); - img.decoding = "sync"; + img.decoding = 'sync'; img.src = dataUrl; - if (typeof img.decode === "function") { + if (typeof img.decode === 'function') { return img.decode().catch(() => {}); } - }), + }) ); } return Promise.allSettled(pendingDecodes); @@ -420,11 +410,7 @@ export function applyLowVisionCategoryIcons(workspace) { if (!workspace) return; const blocks = getWorkspaceAndFlyoutBlocks(workspace); for (const block of blocks) { - if ( - !block || - typeof block.getField !== "function" || - !Array.isArray(block.inputList) - ) { + if (!block || typeof block.getField !== 'function' || !Array.isArray(block.inputList)) { continue; } const styleName = getLowVisionStyleNameForBlock(block); @@ -439,8 +425,8 @@ export function applyLowVisionCategoryIcons(workspace) { if (!block.getField(LOW_VISION_ICON_FIELD_NAME)) { firstInput.insertFieldAt( 0, - new DecorativeFieldImage(iconPath, 18, 18, "", null), - LOW_VISION_ICON_FIELD_NAME, + new DecorativeFieldImage(iconPath, 18, 18, '', null), + LOW_VISION_ICON_FIELD_NAME ); } } @@ -450,11 +436,11 @@ export function clearLowVisionCategoryIcons(workspace) { if (!workspace) return; const blocks = getWorkspaceAndFlyoutBlocks(workspace); for (const block of blocks) { - if (!block || typeof block.getField !== "function") { + if (!block || typeof block.getField !== 'function') { continue; } const firstInput = block.inputList?.[0]; - if (!firstInput || typeof firstInput.removeField !== "function") continue; + if (!firstInput || typeof firstInput.removeField !== 'function') continue; if (block.getField(LOW_VISION_BAR_FIELD_NAME)) firstInput.removeField(LOW_VISION_BAR_FIELD_NAME, true); if (block.getField(LOW_VISION_ICON_FIELD_NAME)) diff --git a/blocks/blocks.js b/blocks/blocks.js index 6c0a876b..9f63db0f 100644 --- a/blocks/blocks.js +++ b/blocks/blocks.js @@ -1,9 +1,9 @@ -import * as Blockly from "blockly"; +import * as Blockly from 'blockly'; //import "@blockly/block-plus-minus"; -import * as BlockDynamicConnection from "@blockly/block-dynamic-connection"; -import { toolbox, categoryColours } from "../toolbox.js"; -import { translate, getTooltip } from "../main/translation.js"; -import { flock } from "../flock.js"; +import * as BlockDynamicConnection from '@blockly/block-dynamic-connection'; +import { toolbox, categoryColours } from '../toolbox.js'; +import { translate, getTooltip } from '../main/translation.js'; +import { flock } from '../flock.js'; import { deleteMeshFromBlock, @@ -12,18 +12,18 @@ import { getActiveSceneControllerBlockId, clearSkyMesh, setClearSkyToBlack, -} from "../ui/blockmesh.js"; -import { FieldColour, registerFieldColour } from "@blockly/field-colour"; -import { createThemeConfig } from "../main/themes.js"; -import { makeInlineIcon, TOGGLE_BUTTON_FIELD_NAME } from "./blockIcons.js"; +} from '../ui/blockmesh.js'; +import { FieldColour, registerFieldColour } from '@blockly/field-colour'; +import { createThemeConfig } from '../main/themes.js'; +import { makeInlineIcon, TOGGLE_BUTTON_FIELD_NAME } from './blockIcons.js'; registerFieldColour(); const normaliseHexColour = (value) => { - if (typeof value !== "string") return ""; + if (typeof value !== 'string') return ''; let hex = value.trim().toLowerCase(); - if (!hex) return ""; - if (!hex.startsWith("#")) hex = `#${hex}`; + if (!hex) return ''; + if (!hex.startsWith('#')) hex = `#${hex}`; if (/^#[\da-f]{3}$/.test(hex)) { hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`; } @@ -31,7 +31,7 @@ const normaliseHexColour = (value) => { }; // When using keyboard navigation, when the colour in a block isn't one of those in the grid this makes the editor start with the container selected so you can use arrow keys to navigate to the swatches. -const flockFocusPatchKey = Symbol.for("flock.fieldColourFocusPatch"); +const flockFocusPatchKey = Symbol.for('flock.fieldColourFocusPatch'); const fieldColourPrototype = FieldColour.prototype; if (!fieldColourPrototype[flockFocusPatchKey]) { const originalShowEditor = fieldColourPrototype.showEditor_; @@ -55,7 +55,7 @@ if (!fieldColourPrototype[flockFocusPatchKey]) { // the field manually as a stopgap until upstream ships a v13 fix. const xy = this.getAbsoluteXY_(); const size = this.getSize(); - const dd = document.querySelector(".blocklyDropDownDiv"); + const dd = document.querySelector('.blocklyDropDownDiv'); const parent = dd?.parentElement; if (dd && parent && xy) { const parentRect = parent.getBoundingClientRect(); @@ -67,15 +67,13 @@ if (!fieldColourPrototype[flockFocusPatchKey]) { if (!currentValue) return; const hasMatchingSwatch = this.getOptions?.(false)?.some( - (option) => normaliseHexColour(option?.[1]) === currentValue, + (option) => normaliseHexColour(option?.[1]) === currentValue ); if (hasMatchingSwatch) return; const openedWithKeyboard = e === undefined || e instanceof KeyboardEvent; if (!openedWithKeyboard) return; - Blockly.DropDownDiv.getContentDiv() - .querySelector(".blocklyFieldGrid") - ?.focus(); + Blockly.DropDownDiv.getContentDiv().querySelector('.blocklyFieldGrid')?.focus(); }; } @@ -84,12 +82,12 @@ if (!fieldColourPrototype[flockFocusPatchKey]) { // field's SVG click target. The result is stored as a flag on the picker so // that excludeFromClose can let the click through only for colour-field hits. document.addEventListener( - "pointerdown", + 'pointerdown', (e) => { const picker = window.flockColorPicker; if (!picker?.isOpen) return; - const blocklyDiv = document.getElementById("blocklyDiv"); + const blocklyDiv = document.getElementById('blocklyDiv'); if (!blocklyDiv?.contains(e.target)) return; const workspace = Blockly.getMainWorkspace(); @@ -103,11 +101,11 @@ document.addEventListener( if (!(field instanceof FieldColour)) return false; const ct = field.clickTarget_ ?? field.fieldGroup_; return ct && (ct === e.target || ct.contains(e.target)); - }), - ), + }) + ) ); }, - true, + true ); export let nextVariableIndexes = Object.create(null); @@ -166,10 +164,10 @@ export function registerBlockHandler(block, handler) { blockHandlerRegistry.set(block.id, handler); } -export const inlineIcon = makeInlineIcon("white"); +export const inlineIcon = makeInlineIcon('white'); export function getHelpUrlFor(_blockType) { - return "https://hub.flockxr.com"; + return 'https://hub.flockxr.com'; } // Text of the static labels immediately preceding an input on its row (e.g. @@ -181,12 +179,12 @@ function inputFieldRowLabel(input) { const labels = []; for (let i = fields.length - 1; i >= 0; i--) { if (!(fields[i] instanceof Blockly.FieldLabel)) break; - labels.unshift(fields[i].getText ? fields[i].getText() : ""); + labels.unshift(fields[i].getText ? fields[i].getText() : ''); } - const text = labels.join(" ").replace(/\s+/g, " ").trim(); + const text = labels.join(' ').replace(/\s+/g, ' ').trim(); // Strip surrounding punctuation/separators so "x:" reads as "x" and // "| skin:" reads as "skin" (messages separate fields with "|"). - return text.replace(/^[^\p{L}\p{N}]+/u, "").replace(/[^\p{L}\p{N}]+$/u, ""); + return text.replace(/^[^\p{L}\p{N}]+/u, '').replace(/[^\p{L}\p{N}]+$/u, ''); } // Derived field-row labels that add no useful per-input context: connectors and @@ -194,10 +192,31 @@ function inputFieldRowLabel(input) { // after the input it describes, so "for %2 seconds %3" would otherwise label the // %3 input "seconds". Meaningful nouns (hair, skin, x, size, color) are kept. const SKIP_DERIVED_LABELS = new Set([ - "for", "to", "on", "with", "at", "by", "from", "of", "in", "into", "and", - "or", "the", "a", "an", - "seconds", "second", "s", "ms", "milliseconds", "degrees", "degree", "deg", - "times", "ms.", + 'for', + 'to', + 'on', + 'with', + 'at', + 'by', + 'from', + 'of', + 'in', + 'into', + 'and', + 'or', + 'the', + 'a', + 'an', + 'seconds', + 'second', + 's', + 'ms', + 'milliseconds', + 'degrees', + 'degree', + 'deg', + 'times', + 'ms.', ]); // Gives value/statement inputs an ARIA label so screen readers announce the @@ -229,7 +248,7 @@ export function applyInputAriaLabels(block, overrides) { label = derived; } } - if (label && typeof input.setAriaLabelProvider === "function") { + if (label && typeof input.setAriaLabelProvider === 'function') { input.setAriaLabelProvider(label); } } @@ -241,7 +260,7 @@ export function applyInputAriaLabels(block, overrides) { // into a slot. Recompute affected fields so e.g. a number moved into scale's X // slot updates from "number" to "x, number". Called from a workspace listener. export function refreshReporterAriaLabels(block) { - if (!block || typeof block.getDescendants !== "function") return; + if (!block || typeof block.getDescendants !== 'function') return; for (const descendant of block.getDescendants(false)) { if (descendant.isSimpleReporter?.()) { descendant.getFullBlockField?.()?.recomputeAriaContext?.(); @@ -252,13 +271,13 @@ export function refreshReporterAriaLabels(block) { // Shared utility to add the toggle button to a block export function addToggleButton(block) { const toggleButton = new Blockly.FieldImage( - makeInlineIcon("white"), + makeInlineIcon('white'), 30, 30, - "toggle inline blocks", + 'toggle inline blocks', () => { block.toggleDoBlock(); - }, + } ); block @@ -269,14 +288,14 @@ export function addToggleButton(block) { // Shared utility for the mutationToDom function export function mutationToDom(block) { - const container = document.createElement("mutation"); - container.setAttribute("inline", block.isInline); + const container = document.createElement('mutation'); + container.setAttribute('inline', block.isInline); return container; } // Shared utility for the domToMutation function export function domToMutation(block, xmlElement) { - const isInline = xmlElement.getAttribute("inline") === "true"; + const isInline = xmlElement.getAttribute('inline') === 'true'; block.updateShape_(isInline); } @@ -297,12 +316,12 @@ export function handleBlockSelect(event) { if ( block && - block.type !== "create_ground" && - block.type !== "create_map" && - (block.type.startsWith("create_") || block.type.startsWith("load_")) + block.type !== 'create_ground' && + block.type !== 'create_map' && + (block.type.startsWith('create_') || block.type.startsWith('load_')) ) { // If the block is a create block, update the window.currentMesh variable - window.updateCurrentMeshName(block, "ID_VAR"); + window.updateCurrentMeshName(block, 'ID_VAR'); } } } @@ -315,8 +334,8 @@ export function handleBlockDelete(event) { type: Blockly.Events.BLOCK_CHANGE, blockId: b.id, workspaceId: b.workspace?.id, - element: "field", - name: "__restore__", + element: 'field', + name: '__restore__', oldValue: null, newValue: null, recordUndo: false, @@ -325,29 +344,20 @@ export function handleBlockDelete(event) { // Recursively delete meshes for qualifying blocks function deleteMeshesRecursively(blockJson) { // Check if block type matches the prefixes - if ( - blockJson.type.startsWith("load_") || - blockJson.type.startsWith("create_") - ) { + if (blockJson.type.startsWith('load_') || blockJson.type.startsWith('create_')) { deleteMeshFromBlock(blockJson.id); - if (blockJson.type === "create_map") { + if (blockJson.type === 'create_map') { const ws = Blockly.getMainWorkspace(); const nextMapBlock = ws ?.getAllBlocks(false) .find( (b) => - b.type === "create_map" && - b.id !== blockJson.id && - b.isEnabled() && - b.getParent(), + b.type === 'create_map' && b.id !== blockJson.id && b.isEnabled() && b.getParent() ); if (nextMapBlock) - updateOrCreateMeshFromBlock( - nextMapBlock, - makeRestoreEvent(nextMapBlock), - ); + updateOrCreateMeshFromBlock(nextMapBlock, makeRestoreEvent(nextMapBlock)); } - } else if (blockJson.type === "set_background_color") { + } else if (blockJson.type === 'set_background_color') { deleteMeshFromBlock(blockJson.id); if (activeControllerBlockId === blockJson.id) { clearSkyMesh(); @@ -356,20 +366,16 @@ export function handleBlockDelete(event) { ?.getAllBlocks(false) .find( (b) => - (b.type === "set_sky_color" || - b.type === "set_background_color") && + (b.type === 'set_sky_color' || b.type === 'set_background_color') && b.id !== blockJson.id && b.isEnabled() && - b.getParent(), + b.getParent() ); if (nextSkyBlock) - updateOrCreateMeshFromBlock( - nextSkyBlock, - makeRestoreEvent(nextSkyBlock), - ); + updateOrCreateMeshFromBlock(nextSkyBlock, makeRestoreEvent(nextSkyBlock)); else setClearSkyToBlack(); } - } else if (blockJson.type === "set_sky_color") { + } else if (blockJson.type === 'set_sky_color') { if (activeControllerBlockId === blockJson.id) { clearSkyMesh(); const ws = Blockly.getMainWorkspace(); @@ -377,17 +383,13 @@ export function handleBlockDelete(event) { ?.getAllBlocks(false) .find( (b) => - (b.type === "set_sky_color" || - b.type === "set_background_color") && + (b.type === 'set_sky_color' || b.type === 'set_background_color') && b.id !== blockJson.id && b.isEnabled() && - b.getParent(), + b.getParent() ); if (nextSkyBlock) - updateOrCreateMeshFromBlock( - nextSkyBlock, - makeRestoreEvent(nextSkyBlock), - ); + updateOrCreateMeshFromBlock(nextSkyBlock, makeRestoreEvent(nextSkyBlock)); else setClearSkyToBlack(); } } @@ -414,21 +416,13 @@ export function handleBlockDelete(event) { } export function handleMeshLifecycleChange(block, changeEvent) { - if ( - !block || - block.disposed || - !block.workspace || - block.workspace.isFlyout - ) { + if (!block || block.disposed || !block.workspace || block.workspace.isFlyout) { return false; } const mesh = getMeshFromBlock(block); - if ( - changeEvent.type === Blockly.Events.BLOCK_MOVE && - changeEvent.blockId === block.id - ) { + if (changeEvent.type === Blockly.Events.BLOCK_MOVE && changeEvent.blockId === block.id) { if (block.getParent() && !mesh) { updateOrCreateMeshFromBlock(block, changeEvent); } @@ -438,10 +432,9 @@ export function handleMeshLifecycleChange(block, changeEvent) { if ( changeEvent.type === Blockly.Events.BLOCK_CHANGE && changeEvent.blockId === block.id && - changeEvent.element === "disabled" + changeEvent.element === 'disabled' ) { - const isDisabling = - changeEvent.newValue === true || changeEvent.newValue === "true"; + const isDisabling = changeEvent.newValue === true || changeEvent.newValue === 'true'; if (!isDisabling) { setTimeout(() => { @@ -454,8 +447,7 @@ export function handleMeshLifecycleChange(block, changeEvent) { } else { deleteMeshFromBlock(block.id); if ( - (block.type === "set_background_color" || - block.type === "set_sky_color") && + (block.type === 'set_background_color' || block.type === 'set_sky_color') && getActiveSceneControllerBlockId() === block.id ) { clearSkyMesh(); @@ -465,10 +457,7 @@ export function handleMeshLifecycleChange(block, changeEvent) { return true; } - if ( - changeEvent.type === Blockly.Events.BLOCK_CREATE && - block.workspace.getBlockById(block.id) - ) { + if (changeEvent.type === Blockly.Events.BLOCK_CREATE && block.workspace.getBlockById(block.id)) { const createdBlockIds = Array.isArray(changeEvent.ids) ? changeEvent.ids : [changeEvent.blockId]; @@ -495,9 +484,7 @@ export function isValueInputDescendantOf(containerBlock, changedBlock) { if (!parent) return false; const viaValueInput = (parent.inputList || []).some( - (inp) => - inp?.type === Blockly.INPUT_VALUE && - inp?.connection?.targetBlock?.() === child, + (inp) => inp?.type === Blockly.INPUT_VALUE && inp?.connection?.targetBlock?.() === child ); if (!viaValueInput) return false; @@ -508,10 +495,7 @@ export function isValueInputDescendantOf(containerBlock, changedBlock) { } export function handleFieldOrChildChange(containerBlock, changeEvent) { - if ( - changeEvent.type !== Blockly.Events.BLOCK_CHANGE || - changeEvent.element !== "field" - ) { + if (changeEvent.type !== Blockly.Events.BLOCK_CHANGE || changeEvent.element !== 'field') { return false; } @@ -541,19 +525,14 @@ export function handleParentLinkedUpdate(containerBlock, changeEvent) { const ws = containerBlock.workspace; const changedBlocks = - changeEvent.type === Blockly.Events.BLOCK_CREATE && - Array.isArray(changeEvent.ids) + changeEvent.type === Blockly.Events.BLOCK_CREATE && Array.isArray(changeEvent.ids) ? changeEvent.ids.map((id) => ws.getBlockById(id)).filter(Boolean) : [ws.getBlockById(changeEvent.blockId)].filter(Boolean); for (const changed of changedBlocks) { const parent = findCreateBlock(changed); - if ( - changed && - parent === containerBlock && - isValueInputDescendantOf(containerBlock, changed) - ) { + if (changed && parent === containerBlock && isValueInputDescendantOf(containerBlock, changed)) { if (!window.loadingCode) { updateOrCreateMeshFromBlock(containerBlock, changeEvent); } @@ -565,23 +544,23 @@ export function handleParentLinkedUpdate(containerBlock, changeEvent) { } export function findCreateBlock(block) { - if (!block || typeof block.getParent !== "function") { + if (!block || typeof block.getParent !== 'function') { return null; } let parent = block; while (parent) { - if (parent.type === "scale" || parent.type === "rotate_to") { + if (parent.type === 'scale' || parent.type === 'rotate_to') { // Don't update parent if we're modifying a nested scale or rotate return null; } if ( - parent.type.startsWith("create_") || - parent.type.startsWith("load_") || - parent.type === "set_sky_color" || - parent.type === "set_background_color" + parent.type.startsWith('create_') || + parent.type.startsWith('load_') || + parent.type === 'set_sky_color' || + parent.type === 'set_background_color' ) { return parent; } @@ -596,12 +575,7 @@ export function findCreateBlock(block) { export function handleBlockChange(block, changeEvent, variableNamePrefix) { // Always run first to handle variable naming - handleBlockCreateEvent( - block, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ); + handleBlockCreateEvent(block, changeEvent, variableNamePrefix, nextVariableIndexes); // Handle lifecycle events like enable/disable/move on the block directly. // Also handle BLOCK_CREATE events where this block is in the created ids @@ -625,45 +599,34 @@ export function handleBlockChange(block, changeEvent, variableNamePrefix) { changeEvent.type === Blockly.Events.BLOCK_MOVE) && changeEvent.workspaceId === ws.id ) { - if (flock.blockDebug) - console.log("The changed block is", changeEvent.block); - if (flock.blockDebug) - console.log("The changed block is", changeEvent.blockId); + if (flock.blockDebug) console.log('The changed block is', changeEvent.block); + if (flock.blockDebug) console.log('The changed block is', changeEvent.blockId); const changedBlock = ws.getBlockById(changeEvent.blockId); const createdBlocks = - changeEvent.type === Blockly.Events.BLOCK_CREATE && - Array.isArray(changeEvent.ids) + changeEvent.type === Blockly.Events.BLOCK_CREATE && Array.isArray(changeEvent.ids) ? changeEvent.ids.map((id) => ws.getBlockById(id)).filter(Boolean) : [changedBlock].filter(Boolean); if (!createdBlocks.length) { - if (flock.blockDebug) console.log("Changed block not found in workspace"); + if (flock.blockDebug) console.log('Changed block not found in workspace'); return; } const parents = createdBlocks.map((cb) => findCreateBlock(cb)); - if (flock.blockDebug) - console.log("The type of the changed block is", changedBlock.type); + if (flock.blockDebug) console.log('The type of the changed block is', changedBlock.type); if (changedBlock.getParent()) { if (flock.blockDebug) - console.log( - "The ID of the parent of the changed block is", - changedBlock.getParent().id, - ); + console.log('The ID of the parent of the changed block is', changedBlock.getParent().id); if (flock.blockDebug) console.log( - "The type of the parent of the changed block is", - changedBlock.getParent().type, + 'The type of the parent of the changed block is', + changedBlock.getParent().type ); } - if (flock.blockDebug) console.log("This block is", block.id); - if (flock.blockDebug) console.log("The type of this block is", block.type); - if ( - changedBlock && - parents.includes(block) && - isValueInputDescendantOf(block, changedBlock) - ) { + if (flock.blockDebug) console.log('This block is', block.id); + if (flock.blockDebug) console.log('The type of this block is', block.type); + if (changedBlock && parents.includes(block) && isValueInputDescendantOf(block, changedBlock)) { // Only configuration inputs (value-input subtree) affect preview mesh; runtime statement blocks do not. const blockInWorkspace = ws.getBlockById(block.id); if (blockInWorkspace) { @@ -676,9 +639,7 @@ export function handleBlockChange(block, changeEvent, variableNamePrefix) { const _pendingRetarget = new WeakMap(); // block -> { from, to, type, prefix } | undefined function getBlockly(opts) { - return ( - (opts && opts.Blockly) || (typeof Blockly !== "undefined" ? Blockly : null) - ); + return (opts && opts.Blockly) || (typeof Blockly !== 'undefined' ? Blockly : null); } function getVariableFieldsOnBlock(block, BlocklyNS) { @@ -691,13 +652,7 @@ function getVariableFieldsOnBlock(block, BlocklyNS) { return out; } -function isVariableUsedElsewhere( - workspace, - varId, - excludingBlockId, - BlocklyNS, - allBlocks, -) { +function isVariableUsedElsewhere(workspace, varId, excludingBlockId, BlocklyNS, allBlocks) { if (!varId) return false; const blocks = allBlocks ?? workspace.getAllBlocks(false); for (const b of blocks) { @@ -712,13 +667,12 @@ function isVariableUsedElsewhere( function getFieldVariableType(block, fieldName) { const field = block.getField(fieldName); - if (!field) return ""; - const model = - typeof field.getVariable === "function" ? field.getVariable() : null; - if (model && typeof model.type === "string") return model.type || ""; + if (!field) return ''; + const model = typeof field.getVariable === 'function' ? field.getVariable() : null; + if (model && typeof model.type === 'string') return model.type || ''; const varId = field.getValue && field.getValue(); const byId = varId ? block.workspace.getVariableMap().getVariableById(varId) : null; - return (byId && byId.type) || ""; + return (byId && byId.type) || ''; } function parseNumericSuffix(name, prefix) { @@ -729,7 +683,7 @@ function parseNumericSuffix(name, prefix) { } function deriveVariableNameParts(name, fallbackPrefix) { - if (typeof name !== "string" || !name.length) { + if (typeof name !== 'string' || !name.length) { return { prefix: fallbackPrefix, suffix: null }; } const numberMatch = name.match(/^(.*?)(\d+)$/); @@ -750,10 +704,7 @@ function createFreshVariable(workspace, prefix, type, nextVariableIndexes) { while (workspace.getVariableMap().getVariable(`${prefix}${n}`, type)) n += 1; // Update the counter - nextVariableIndexes[prefix] = Math.max( - nextVariableIndexes[prefix] || 1, - n + 1, - ); + nextVariableIndexes[prefix] = Math.max(nextVariableIndexes[prefix] || 1, n + 1); const newVarName = `${prefix}${n}`; @@ -769,13 +720,7 @@ function createFreshVariable(workspace, prefix, type, nextVariableIndexes) { return workspace.getVariableMap().createVariable(`${prefix}${n}`, type); } -function retargetDescendantsVariables( - rootBlock, - fromVarId, - toVarId, - BlocklyNS, - createdIds = null, -) { +function retargetDescendantsVariables(rootBlock, fromVarId, toVarId, BlocklyNS, createdIds = null) { if (!fromVarId || !toVarId || fromVarId === toVarId) return 0; // Get descendants but ONLY through input connections, not next/previous @@ -835,7 +780,7 @@ function adoptIsolatedDefaultVarsTo( workspace, BlocklyNS, createdIds, - allBlocks, + allBlocks ) { const descendantIds = buildDescendantIdSet(rootBlock); let adopted = 0; @@ -895,7 +840,7 @@ function adoptIsolatedDefaultVarsTo( try { workspace.getVariableMap().deleteVariable(model); } catch (error) { - console.warn("Failed to delete unreferenced variable by id:", error); + console.warn('Failed to delete unreferenced variable by id:', error); } } } @@ -916,12 +861,12 @@ export function ensureFreshVarOnDuplicate( changeEvent, variableNamePrefix, nextVariableIndexes, - opts = {}, + opts = {} ) { const BlocklyNS = getBlockly(opts); if (!BlocklyNS) return false; - const fieldName = opts.fieldName || "ID_VAR"; + const fieldName = opts.fieldName || 'ID_VAR'; // Finish any pending work (retarget, adopt, normalize) from earlier in the same dup group. const pending = _pendingRetarget.get(block); @@ -931,13 +876,7 @@ export function ensureFreshVarOnDuplicate( BlocklyNS.Events.disable(); // Only retarget blocks that were created in the same copy operation - retargetDescendantsVariables( - block, - pending.from, - pending.to, - BlocklyNS, - pending.createdIds, - ); + retargetDescendantsVariables(block, pending.from, pending.to, BlocklyNS, pending.createdIds); adoptIsolatedDefaultVarsTo( block, pending.to, @@ -945,12 +884,10 @@ export function ensureFreshVarOnDuplicate( pending.prefix, block.workspace, BlocklyNS, - pending.createdIds, + pending.createdIds ); - if ( - !subtreeHasVarId(block, pending.from, BlocklyNS, pending.createdIds) - ) { + if (!subtreeHasVarId(block, pending.from, BlocklyNS, pending.createdIds)) { _pendingRetarget.set(block, undefined); } } finally { @@ -970,19 +907,20 @@ export function ensureFreshVarOnDuplicate( const oldVarId = idField.getValue && idField.getValue(); if (!oldVarId) return false; const oldVarModel = ws.getVariableMap().getVariableById(oldVarId); - const { prefix: duplicatePrefix, suffix: duplicateSuffix } = - deriveVariableNameParts(oldVarModel?.name, variableNamePrefix); + const { prefix: duplicatePrefix, suffix: duplicateSuffix } = deriveVariableNameParts( + oldVarModel?.name, + variableNamePrefix + ); // Duplicate/copy/duplicate-parent case? const allBlocks = ws.getAllBlocks(false); - if (!isVariableUsedElsewhere(ws, oldVarId, block.id, BlocklyNS, allBlocks)) - return false; + if (!isVariableUsedElsewhere(ws, oldVarId, block.id, BlocklyNS, allBlocks)) return false; if (Number.isInteger(duplicateSuffix)) { const nextFromSource = duplicateSuffix + 1; nextVariableIndexes[duplicatePrefix] = Math.max( nextVariableIndexes[duplicatePrefix] || 1, - nextFromSource, + nextFromSource ); } @@ -997,28 +935,16 @@ export function ensureFreshVarOnDuplicate( BlocklyNS.Events.disable(); // Mint a new var with the *lowest* available suffix now. - const newVarModel = createFreshVariable( - ws, - duplicatePrefix, - varType, - nextVariableIndexes, - ); + const newVarModel = createFreshVariable(ws, duplicatePrefix, varType, nextVariableIndexes); const newVarId = - newVarModel.id || - (typeof newVarModel.getId === "function" ? newVarModel.getId() : null); + newVarModel.id || (typeof newVarModel.getId === 'function' ? newVarModel.getId() : null); if (!newVarId) return false; // Point the creator at the fresh variable. idField.setValue(newVarId); // Pass 1: retarget descendants old -> new (ONLY blocks created in this event) - retargetDescendantsVariables( - block, - oldVarId, - newVarId, - BlocklyNS, - createdIds, - ); + retargetDescendantsVariables(block, oldVarId, newVarId, BlocklyNS, createdIds); // Pass 2: adopt any isolated default-looking vars inside subtree to the new var adoptIsolatedDefaultVarsTo( @@ -1029,7 +955,7 @@ export function ensureFreshVarOnDuplicate( ws, BlocklyNS, createdIds, - allBlocks, + allBlocks ); // If more children will connect later, remember to finish on subsequent events. @@ -1074,11 +1000,11 @@ export class CustomConstantProvider extends Blockly.zelos.ConstantProvider { this.NOTCH_OFFSET_LEFT = 2 * this.GRID_UNIT; this.NOTCH_HEIGHT = 2 * this.GRID_UNIT; this.FIELD_DROPDOWN_SVG_ARROW_DATAURI = - "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMi43MSIgaGVpZ2h0PSI4Ljc5IiB2aWV3Qm94PSIwIDAgMTIuNzEgOC43OSI+PHRpdGxlPmRyb3Bkb3duLWFycm93PC90aXRsZT48ZyBvcGFjaXR5PSIwLjEiPjxwYXRoIGQ9Ik0xMi43MSwyLjQ0QTIuNDEsMi40MSwwLDAsMSwxMiw0LjE2TDguMDgsOC4wOGEyLjQ1LDIuNDUsMCwwLDEtMy40NSwwTDAuNzIsNC4xNkEyLjQyLDIuNDIsMCwwLDEsMCwyLjQ0LDIuNDgsMi40OCwwLDAsMSwuNzEuNzFDMSwwLjQ3LDEuNDMsMCw2LjM2LDBTMTEuNzUsMC40NiwxMiwuNzFBMi40NCwyLjQ0LDAsMCwxLDEyLjcxLDIuNDRaIiBmaWxsPSIjMjMxZjIwIi8+PC9nPjxwYXRoIGQ9Ik02LjM2LDcuNzlhMS40MywxLjQzLDAsMCwxLTEuNDItTDEuNDIsMy40NWExLjQ0LDEuNDQsMCwwLDEsMC0yYzAuNTYtLjU2LDkuMzEtMC41Niw5Ljg3LDBhMS40NCwxLjQ0LDAsMCwxLDAsMkw3LjM3LDcuMzdBMS40MywxLjQzLDAsMCwxLDYuMzYsNy43OVoiIGZpbGw9IiMwMDAiLz48L3N2Zz4="; + 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMi43MSIgaGVpZ2h0PSI4Ljc5IiB2aWV3Qm94PSIwIDAgMTIuNzEgOC43OSI+PHRpdGxlPmRyb3Bkb3duLWFycm93PC90aXRsZT48ZyBvcGFjaXR5PSIwLjEiPjxwYXRoIGQ9Ik0xMi43MSwyLjQ0QTIuNDEsMi40MSwwLDAsMSwxMiw0LjE2TDguMDgsOC4wOGEyLjQ1LDIuNDUsMCwwLDEtMy40NSwwTDAuNzIsNC4xNkEyLjQyLDIuNDIsMCwwLDEsMCwyLjQ0LDIuNDgsMi40OCwwLDAsMSwuNzEuNzFDMSwwLjQ3LDEuNDMsMCw2LjM2LDBTMTEuNzUsMC40NiwxMiwuNzFBMi40NCwyLjQ0LDAsMCwxLDEyLjcxLDIuNDRaIiBmaWxsPSIjMjMxZjIwIi8+PC9nPjxwYXRoIGQ9Ik02LjM2LDcuNzlhMS40MywxLjQzLDAsMCwxLTEuNDItTDEuNDIsMy40NWExLjQ0LDEuNDQsMCwwLDEsMC0yYzAuNTYtLjU2LDkuMzEtMC41Niw5Ljg3LDBhMS40NCwxLjQ0LDAsMCwxLDAsMkw3LjM3LDcuMzdBMS40MywxLjQzLDAsMCwxLDYuMzYsNy43OVoiIGZpbGw9IiMwMDAiLz48L3N2Zz4='; } } -const MODE = { IF: "IF", ELSEIF: "ELSEIF", ELSE: "ELSE" }; +const MODE = { IF: 'IF', ELSEIF: 'ELSEIF', ELSE: 'ELSE' }; class CustomRenderInfo extends Blockly.zelos.RenderInfo { constructor(renderer, block) { @@ -1091,8 +1017,8 @@ class CustomRenderInfo extends Blockly.zelos.RenderInfo { super.addElemSpacing_(); // Add extra height to the top row for IF blocks only - if (this.block_.type === "if_clause") { - const mode = this.block_.getFieldValue?.("MODE"); + if (this.block_.type === 'if_clause') { + const mode = this.block_.getFieldValue?.('MODE'); if (mode === MODE.IF && this.rows.length > 0) { // Find the first row with fields or inputs (skip the top cap row) for (let i = 0; i < this.rows.length; i++) { @@ -1102,7 +1028,7 @@ class CustomRenderInfo extends Blockly.zelos.RenderInfo { if (row.elements && row.elements.length > 0) { // Check if it's not just a top/bottom cap or connection row const hasContent = row.elements.some( - (el) => el.field || el.input || (el.type && el.type !== 0), + (el) => el.field || el.input || (el.type && el.type !== 0) ); if (hasContent) { @@ -1126,21 +1052,19 @@ class CustomZelosDrawer extends Blockly.zelos.Drawer { drawTop_() { const b = this.block_; - if (b?.type !== "if_clause") return super.drawTop_(); + if (b?.type !== 'if_clause') return super.drawTop_(); // Never change shape for insertion markers. if (b.isInsertionMarker?.()) return super.drawTop_(); - const mode = b.getFieldValue?.("MODE"); + const mode = b.getFieldValue?.('MODE'); const isClause = mode === MODE.ELSE || mode === MODE.ELSEIF; // Require an ACTUAL previous connection to a real if_clause block. const prevConn = b.previousConnection; - const prev = - prevConn && prevConn.isConnected() ? prevConn.targetBlock() : null; + const prev = prevConn && prevConn.isConnected() ? prevConn.targetBlock() : null; - const prevIsRealIfClause = - prev && prev.type === "if_clause" && !prev.isInsertionMarker?.(); + const prevIsRealIfClause = prev && prev.type === 'if_clause' && !prev.isInsertionMarker?.(); if (isClause && prevIsRealIfClause) { this.drawFlatTop_(); @@ -1152,12 +1076,12 @@ class CustomZelosDrawer extends Blockly.zelos.Drawer { drawBottom_() { const b = this.block_; - if (b?.type !== "if_clause") return super.drawBottom_(); + if (b?.type !== 'if_clause') return super.drawBottom_(); // Never change shape for insertion markers. if (b.isInsertionMarker?.()) return super.drawBottom_(); - const mode = b.getFieldValue?.("MODE"); + const mode = b.getFieldValue?.('MODE'); // Only clauses that can legally have something after them in the same chain. const canContinueChain = mode === MODE.IF || mode === MODE.ELSEIF; @@ -1165,19 +1089,16 @@ class CustomZelosDrawer extends Blockly.zelos.Drawer { // Require an ACTUAL next connection to a real if_clause block. const nextConn = b.nextConnection; - const next = - nextConn && nextConn.isConnected() ? nextConn.targetBlock() : null; + const next = nextConn && nextConn.isConnected() ? nextConn.targetBlock() : null; - const nextIsRealIfClause = - next && next.type === "if_clause" && !next.isInsertionMarker?.(); + const nextIsRealIfClause = next && next.type === 'if_clause' && !next.isInsertionMarker?.(); if (!nextIsRealIfClause) return super.drawBottom_(); // Only flatten when the NEXT clause is a joined clause (else/else if), // not when it’s a new IF statement. - const nextMode = next.getFieldValue?.("MODE"); - const nextIsJoinedClause = - nextMode === MODE.ELSE || nextMode === MODE.ELSEIF; + const nextMode = next.getFieldValue?.('MODE'); + const nextIsJoinedClause = nextMode === MODE.ELSE || nextMode === MODE.ELSEIF; if (nextIsJoinedClause) { this.drawFlatBottom_(); @@ -1191,22 +1112,18 @@ class CustomZelosDrawer extends Blockly.zelos.Drawer { const b = this.block_; const svgRoot = b.getSvgRoot?.(); if (!svgRoot) return; - svgRoot.removeAttribute("data-axis"); + svgRoot.removeAttribute('data-axis'); if (!b.outputConnection?.isConnected()) return; const targetConn = b.outputConnection.targetConnection; if (!targetConn) return; const parentBlock = b.outputConnection.targetBlock(); if (!parentBlock) return; const parentInput = (parentBlock.inputList || []).find( - (input) => input.connection === targetConn, + (input) => input.connection === targetConn ); if (!parentInput) return; - if ( - parentInput.name === "X" || - parentInput.name === "Y" || - parentInput.name === "Z" - ) { - svgRoot.setAttribute("data-axis", parentInput.name); + if (parentInput.name === 'X' || parentInput.name === 'Y' || parentInput.name === 'Z') { + svgRoot.setAttribute('data-axis', parentInput.name); } } @@ -1215,61 +1132,54 @@ class CustomZelosDrawer extends Blockly.zelos.Drawer { this.colorizeAxisInput_(); const b = this.block_; - if (b?.type !== "if_clause") return; + if (b?.type !== 'if_clause') return; // Don’t paint seam covers on insertion markers / connection previews. - if (typeof b.isInsertionMarker === "function" && b.isInsertionMarker()) - return; + if (typeof b.isInsertionMarker === 'function' && b.isInsertionMarker()) return; const svgRoot = b.getSvgRoot?.(); if (!svgRoot) return; // Always remove any previous cover (so disabling can hide it). - const existing = svgRoot.querySelector?.( - ":scope > rect.ifclause-seam-cover", - ); + const existing = svgRoot.querySelector?.(':scope > rect.ifclause-seam-cover'); if (existing) existing.remove(); // If the block is disabled, we’re done (no cover). // Use isEnabled (covers setDisabledReason etc), with a fallback to `disabled`. - const isDisabled = - (typeof b.isEnabled === "function" ? !b.isEnabled() : false) || - !!b.disabled; + const isDisabled = (typeof b.isEnabled === 'function' ? !b.isEnabled() : false) || !!b.disabled; if (isDisabled) return; const prev = b.getPreviousBlock?.(); - const prevIsIfClause = prev && prev.type === "if_clause"; + const prevIsIfClause = prev && prev.type === 'if_clause'; - const mode = b.getFieldValue?.("MODE"); + const mode = b.getFieldValue?.('MODE'); const isJoinedClause = mode === MODE.ELSE || mode === MODE.ELSEIF; if (!prevIsIfClause || !isJoinedClause) return; // Get the actual rendered fill from the block path (avoids black during previews). const pathObj = this.pathObject_; - const mainPath = - pathObj?.svgPath_ || pathObj?.svgPath || pathObj?.path_ || null; + const mainPath = pathObj?.svgPath_ || pathObj?.svgPath || pathObj?.path_ || null; const fill = - (mainPath?.getAttribute && mainPath.getAttribute("fill")) || + (mainPath?.getAttribute && mainPath.getAttribute('fill')) || mainPath?.style?.fill || - (typeof b.getColour === "function" ? b.getColour() : null); + (typeof b.getColour === 'function' ? b.getColour() : null); - if (!fill || fill === "none") return; + if (!fill || fill === 'none') return; const coverPx = 16; - const strokePx = - this.constants_?.OUTLINE_WIDTH ?? this.constants_?.STROKE_WIDTH ?? 1; - - const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - rect.setAttribute("class", "ifclause-seam-cover"); - rect.setAttribute("x", "1"); - rect.setAttribute("y", String(-strokePx * 2)); - rect.setAttribute("width", String(coverPx)); - rect.setAttribute("height", String(strokePx * 4)); - rect.setAttribute("fill", fill); - rect.setAttribute("stroke", "none"); - rect.setAttribute("pointer-events", "none"); + const strokePx = this.constants_?.OUTLINE_WIDTH ?? this.constants_?.STROKE_WIDTH ?? 1; + + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('class', 'ifclause-seam-cover'); + rect.setAttribute('x', '1'); + rect.setAttribute('y', String(-strokePx * 2)); + rect.setAttribute('width', String(coverPx)); + rect.setAttribute('height', String(strokePx * 4)); + rect.setAttribute('fill', fill); + rect.setAttribute('stroke', 'none'); + rect.setAttribute('pointer-events', 'none'); svgRoot.appendChild(rect); } @@ -1294,8 +1204,8 @@ export class CustomZelosRenderer extends Blockly.zelos.Renderer { } function getBlocklyMediaPath() { - let baseUrl = import.meta.env.BASE_URL || "/"; - if (!baseUrl.endsWith("/")) baseUrl += "/"; + let baseUrl = import.meta.env.BASE_URL || '/'; + if (!baseUrl.endsWith('/')) baseUrl += '/'; return `${baseUrl}blockly/media/`; } @@ -1303,10 +1213,10 @@ const mediaPath = getBlocklyMediaPath(); export const options = { //theme: FlockTheme, - theme: createThemeConfig("light"), + theme: createThemeConfig('light'), //theme: "flockTheme", //renderer: "zelos", - renderer: "custom_zelos_renderer", + renderer: 'custom_zelos_renderer', media: mediaPath, modalInputs: false, zoom: { @@ -1358,7 +1268,7 @@ export function initializeVariableIndexes() { plane: 1, wall: 1, text: 1, - "3dtext": 1, + '3dtext': 1, sound: 1, character: 1, object: 1, @@ -1383,7 +1293,7 @@ export function initializeVariableIndexes() { // Process each type of variable and use the lowest available suffix. if (workspace) { Object.keys(nextVariableIndexes).forEach(function (type) { - nextVariableIndexes[type] = lowestAvailableSuffix(workspace, type, ""); + nextVariableIndexes[type] = lowestAvailableSuffix(workspace, type, ''); }); } @@ -1411,68 +1321,66 @@ export function defineBlocks() { window.updateCurrentMeshName = updateCurrentMeshName; - Blockly.Blocks["create_wall"] = { + Blockly.Blocks['create_wall'] = { init: function () { - const variableNamePrefix = "wall"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; // Start with "wall1"; + const variableNamePrefix = 'wall'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; // Start with "wall1"; this.jsonInit({ - type: "create_wall", - message0: - "new wall %1 type %2 colour %3 \n start x %4 z %5 end x %6 z %7 y position %8", + type: 'create_wall', + message0: 'new wall %1 type %2 colour %3 \n start x %4 z %5 end x %6 z %7 y position %8', args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "field_dropdown", - name: "WALL_TYPE", + type: 'field_dropdown', + name: 'WALL_TYPE', options: [ - ["solid", "SOLID_WALL"], - ["door", "WALL_WITH_DOOR"], - ["window", "WALL_WITH_WINDOW"], - ["floor/roof", "FLOOR"], + ['solid', 'SOLID_WALL'], + ['door', 'WALL_WITH_DOOR'], + ['window', 'WALL_WITH_WINDOW'], + ['floor/roof', 'FLOOR'], ], }, { - type: "input_value", - name: "COLOR", - check: "Colour", + type: 'input_value', + name: 'COLOR', + check: 'Colour', }, { - type: "input_value", - name: "START_X", - check: "Number", + type: 'input_value', + name: 'START_X', + check: 'Number', }, { - type: "input_value", - name: "START_Z", - check: "Number", + type: 'input_value', + name: 'START_Z', + check: 'Number', }, { - type: "input_value", - name: "END_X", - check: "Number", + type: 'input_value', + name: 'END_X', + check: 'Number', }, { - type: "input_value", - name: "END_Z", - check: "Number", + type: 'input_value', + name: 'END_Z', + check: 'Number', }, { - type: "input_value", - name: "Y_POSITION", - check: "Number", + type: 'input_value', + name: 'Y_POSITION', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], + colour: categoryColours['Scene'], tooltip: - "Create a wall with the selected type and color between specified start and end positions.\nKeyword: wall", + 'Create a wall with the selected type and color between specified start and end positions.\nKeyword: wall', }); this.setHelpUrl(getHelpUrlFor(this.type)); registerBlockHandler(this, (changeEvent) => { @@ -1483,23 +1391,18 @@ export function defineBlocks() { const blockInWorkspace = this.workspace?.getBlockById(this.id); // Check if block is in the main workspace if (blockInWorkspace) { - window.updateCurrentMeshName(this, "ID_VAR"); // Call the function to update window.currentMesh + window.updateCurrentMeshName(this, 'ID_VAR'); // Call the function to update window.currentMesh } } - handleBlockCreateEvent( - this, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ); + handleBlockCreateEvent(this, changeEvent, variableNamePrefix, nextVariableIndexes); }); }, }; - Blockly.Extensions.register("dynamic_mesh_dropdown", function () { + Blockly.Extensions.register('dynamic_mesh_dropdown', function () { const dropdown = new Blockly.FieldDropdown(function () { - const options = [[translate("everywhere_option"), "__everywhere__"]]; + const options = [[translate('everywhere_option'), '__everywhere__']]; const workspace = this.sourceBlock_ && this.sourceBlock_.workspace; if (workspace) { const variables = workspace.getVariableMap().getAllVariables(); @@ -1511,180 +1414,180 @@ export function defineBlocks() { }); // Attach the dropdown to the block - this.getInput("MESH_INPUT").appendField(dropdown, "MESH_NAME"); + this.getInput('MESH_INPUT').appendField(dropdown, 'MESH_NAME'); }); - Blockly.Blocks["rotate_camera"] = { + Blockly.Blocks['rotate_camera'] = { init: function () { this.jsonInit({ - type: "rotate_camera", - message0: translate("rotate_camera"), + type: 'rotate_camera', + message0: translate('rotate_camera'), args0: [ { - type: "input_value", - name: "DEGREES", - check: "Number", + type: 'input_value', + name: 'DEGREES', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("rotate_camera"), + colour: categoryColours['Transform'], + tooltip: getTooltip('rotate_camera'), }); this.setHelpUrl(getHelpUrlFor(this.type)); }, }; - Blockly.Blocks["up"] = { + Blockly.Blocks['up'] = { init: function () { this.jsonInit({ - type: "up", - message0: translate("up"), + type: 'up', + message0: translate('up'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "UP_FORCE", - check: "Number", + type: 'input_value', + name: 'UP_FORCE', + check: 'Number', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("up"), + colour: categoryColours['Transform'], + tooltip: getTooltip('up'), }); this.setHelpUrl(getHelpUrlFor(this.type)); }, }; - Blockly.Blocks["random_seeded_int"] = { + Blockly.Blocks['random_seeded_int'] = { init: function () { this.jsonInit({ - type: "random_seeded_int", - message0: translate("random_seeded_int"), + type: 'random_seeded_int', + message0: translate('random_seeded_int'), args0: [ { - type: "input_value", - name: "FROM", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'FROM', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "TO", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'TO', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "SEED", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'SEED', + check: 'Number', + align: 'RIGHT', }, ], inputsInline: true, - output: "Number", + output: 'Number', colour: 230, - tooltip: getTooltip("random_seeded_int"), + tooltip: getTooltip('random_seeded_int'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("math_blocks"); + this.setStyle('math_blocks'); }, }; - Blockly.Blocks["lists_add_item"] = { + Blockly.Blocks['lists_add_item'] = { init: function () { this.jsonInit({ - type: "lists_add_item", - message0: "add %1 to %2", + type: 'lists_add_item', + message0: 'add %1 to %2', args0: [ { - type: "input_value", - name: "TO", + type: 'input_value', + name: 'TO', }, { - type: "field_variable", - name: "LIST", - variable: "list1", + type: 'field_variable', + name: 'LIST', + variable: 'list1', }, ], previousStatement: null, nextStatement: null, - tooltip: "Add an item to the end of a list.", + tooltip: 'Add an item to the end of a list.', }); - this.setStyle("list_blocks"); + this.setStyle('list_blocks'); this.setHelpUrl(getHelpUrlFor(this.type)); }, }; - Blockly.Blocks["lists_delete_nth"] = { + Blockly.Blocks['lists_delete_nth'] = { init: function () { this.jsonInit({ - type: "lists_delete_nth", - message0: "delete %1 from %2", + type: 'lists_delete_nth', + message0: 'delete %1 from %2', args0: [ { - type: "input_value", - name: "INDEX", - check: "Number", + type: 'input_value', + name: 'INDEX', + check: 'Number', }, { - type: "field_variable", - name: "LIST", - variable: "list1", + type: 'field_variable', + name: 'LIST', + variable: 'list1', }, ], previousStatement: null, nextStatement: null, - tooltip: "Delete item at index n from a list (0-based).", + tooltip: 'Delete item at index n from a list (0-based).', }); - this.setStyle("list_blocks"); + this.setStyle('list_blocks'); this.setHelpUrl(getHelpUrlFor(this.type)); }, }; - Blockly.Blocks["to_number"] = { + Blockly.Blocks['to_number'] = { init: function () { this.jsonInit({ - type: "to_number", - message0: translate("to_number"), + type: 'to_number', + message0: translate('to_number'), args0: [ { - type: "input_value", - name: "STRING", - check: "String", + type: 'input_value', + name: 'STRING', + check: 'String', }, { - type: "field_dropdown", - name: "TYPE", + type: 'field_dropdown', + name: 'TYPE', options: [ - ["integer", "INT"], - ["float", "FLOAT"], + ['integer', 'INT'], + ['float', 'FLOAT'], ], }, ], inputsInline: true, - output: "Number", + output: 'Number', colour: 230, - tooltip: getTooltip("to_number"), + tooltip: getTooltip('to_number'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("math_blocks"); + this.setStyle('math_blocks'); }, }; - Blockly.Blocks["keyword_block"] = { + Blockly.Blocks['keyword_block'] = { init: function () { this.appendDummyInput().appendField( - new Blockly.FieldTextInput("type a keyword to add a block"), - "KEYWORD", + new Blockly.FieldTextInput('type a keyword to add a block'), + 'KEYWORD' ); - this.setTooltip("Type a keyword to change this block."); + this.setTooltip('Type a keyword to change this block.'); this.setHelpUrl(getHelpUrlFor(this.type)); this.setOnChange(function () { @@ -1693,7 +1596,7 @@ export function defineBlocks() { return; } // Get the entered keyword. - const keyword = this.getFieldValue("KEYWORD").trim(); + const keyword = this.getFieldValue('KEYWORD').trim(); // Lookup the exact toolbox definition based on the keyword. const blockDefinition = findBlockDefinitionByKeyword(keyword); if (blockDefinition?.type) { @@ -1711,10 +1614,7 @@ export function defineBlocks() { const pos = this.getRelativeToSurfaceXY(); newBlock.moveBy(pos.x, pos.y); - if ( - this.previousConnection && - this.previousConnection.isConnected() - ) { + if (this.previousConnection && this.previousConnection.isConnected()) { const parentConnection = this.previousConnection.targetConnection; if (parentConnection) { parentConnection.disconnect(); @@ -1745,10 +1645,10 @@ export function defineBlocks() { }, }; - Blockly.Blocks["keyword"] = { + Blockly.Blocks['keyword'] = { init: function () { // Call the original keyword_block init method. - Blockly.Blocks["keyword_block"].init.call(this); + Blockly.Blocks['keyword_block'].init.call(this); // Add chaining connections. this.setPreviousStatement(true); this.setNextStatement(true); @@ -1763,11 +1663,11 @@ export function defineBlocks() { } for (const item of contents) { // If this item is a block with the matching keyword, return its definition. - if (item.kind === "block" && item.keyword === keyword) { + if (item.kind === 'block' && item.keyword === keyword) { return item; } // If the item is a category with its own contents, search recursively. - if (item.kind === "category" && Array.isArray(item.contents)) { + if (item.kind === 'category' && Array.isArray(item.contents)) { const result = searchContents(item.contents); if (result !== null) { return result; @@ -1788,10 +1688,7 @@ export function defineBlocks() { block.loadExtraState(definition.extraState); } - if ( - typeof definition.inline === "boolean" && - typeof block.setInputsInline === "function" - ) { + if (typeof definition.inline === 'boolean' && typeof block.setInputsInline === 'function') { block.setInputsInline(definition.inline); } @@ -1801,37 +1698,37 @@ export function defineBlocks() { const field = block.getField?.(fieldName); if ( - typeof fieldValue === "string" || - typeof fieldValue === "number" || - typeof fieldValue === "boolean" + typeof fieldValue === 'string' || + typeof fieldValue === 'number' || + typeof fieldValue === 'boolean' ) { block.setFieldValue(fieldValue, fieldName); continue; } - if (!fieldValue || typeof fieldValue !== "object") { + if (!fieldValue || typeof fieldValue !== 'object') { continue; } if ( - typeof block.setVariableFieldValue === "function" && - typeof fieldValue.name === "string" + typeof block.setVariableFieldValue === 'function' && + typeof fieldValue.name === 'string' ) { block.setVariableFieldValue(fieldValue.name, fieldName); continue; } - if (typeof field?.loadState === "function") { + if (typeof field?.loadState === 'function') { field.loadState(fieldValue); continue; } - if (fieldValue.id && typeof block.setFieldValue === "function") { + if (fieldValue.id && typeof block.setFieldValue === 'function') { block.setFieldValue(fieldValue.id, fieldName); continue; } - if (fieldValue.name && typeof block.setFieldValue === "function") { + if (fieldValue.name && typeof block.setFieldValue === 'function') { block.setFieldValue(fieldValue.name, fieldName); } } @@ -1878,8 +1775,7 @@ export function defineBlocks() { childBlock.render(); applyBlockDefinition(childBlock, definition); - const childConnection = - childBlock.outputConnection || childBlock.previousConnection; + const childConnection = childBlock.outputConnection || childBlock.previousConnection; if (!childConnection) { childBlock.dispose(); return; @@ -1904,41 +1800,41 @@ export function defineBlocks() { export function addDoMutatorWithToggleBehavior(block) { // Custom function to toggle the "do" block mutation block.toggleDoBlock = function () { - const hasDo = this.getInput("DO") ? true : false; + const hasDo = this.getInput('DO') ? true : false; if (hasDo) { - this.removeInput("DO"); + this.removeInput('DO'); } else { - this.appendStatementInput("DO").setCheck(null).appendField(""); + this.appendStatementInput('DO').setCheck(null).appendField(''); } }; // Add the toggle button to the block const toggleButton = new Blockly.FieldImage( - "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xNSA2djloLTl2M2g5djloM3YtOWg5di0zaC05di05eiIvPjwvc3ZnPg==", // Custom icon + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xNSA2djloLTl2M2g5djloM3YtOWg5di0zaC05di05eiIvPjwvc3ZnPg==', // Custom icon 30, 30, - "toggle do block", // Width, Height, Alt text - block.toggleDoBlock.bind(block), // Bind the event handler to the block + 'toggle do block', // Width, Height, Alt text + block.toggleDoBlock.bind(block) // Bind the event handler to the block ); // Add the button to the block block .appendDummyInput() .setAlign(Blockly.inputs.Align.RIGHT) - .appendField(toggleButton, "TOGGLE_BUTTON"); + .appendField(toggleButton, 'TOGGLE_BUTTON'); // Save the mutation state block.mutationToDom = function () { - const container = document.createElement("mutation"); - container.setAttribute("has_do", this.getInput("DO") ? "true" : "false"); + const container = document.createElement('mutation'); + container.setAttribute('has_do', this.getInput('DO') ? 'true' : 'false'); return container; }; // Restore the mutation state block.domToMutation = function (xmlElement) { - const hasDo = xmlElement.getAttribute("has_do") === "true"; + const hasDo = xmlElement.getAttribute('has_do') === 'true'; if (hasDo) { - this.appendStatementInput("DO").setCheck(null).appendField(""); + this.appendStatementInput('DO').setCheck(null).appendField(''); } }; } @@ -1948,7 +1844,7 @@ export function handleBlockCreateEvent( changeEvent, variableNamePrefix, nextVariableIndexes, - fieldName = "ID_VAR", // Default field name to handle + fieldName = 'ID_VAR' // Default field name to handle ) { if (window.loadingCode) return; // Don't rename variables during code loading @@ -1958,8 +1854,8 @@ export function handleBlockCreateEvent( variableNamePrefix, nextVariableIndexes, { - fieldName: "ID_VAR", - }, + fieldName: 'ID_VAR', + } ); if (handledDuplicate) return; if (blockInstance.id !== changeEvent.blockId) return; @@ -1977,13 +1873,11 @@ export function handleBlockCreateEvent( const variableField = blockInstance.getField(fieldName); if (variableField) { const variableId = variableField.getValue(); - const variable = blockInstance.workspace - .getVariableMap() - .getVariableById(variableId); + const variable = blockInstance.workspace.getVariableMap().getVariableById(variableId); // Check if the variable name matches the pattern "prefixn" const variableNamePattern = new RegExp(`^${variableNamePrefix}\\d+$`); - const variableName = variable ? variable.name : ""; + const variableName = variable ? variable.name : ''; if (!variableNamePattern.test(variableName)) { // Handle custom variables @@ -1993,12 +1887,10 @@ export function handleBlockCreateEvent( if (numberMatch) { newVariableName = numberMatch[1] + (parseInt(numberMatch[2]) + 1); } else { - newVariableName = variableName + "1"; + newVariableName = variableName + '1'; } - let newVariable = blockInstance.workspace - .getVariableMap() - .getVariable(newVariableName); + let newVariable = blockInstance.workspace.getVariableMap().getVariable(newVariableName); if (!newVariable) { newVariable = blockInstance.workspace .getVariableMap() @@ -2011,7 +1903,7 @@ export function handleBlockCreateEvent( if (newSuffix !== null) { nextVariableIndexes[variableNamePrefix] = Math.max( nextVariableIndexes[variableNamePrefix] || 1, - newSuffix + 1, + newSuffix + 1 ); } } @@ -2020,18 +1912,13 @@ export function handleBlockCreateEvent( if (!nextVariableIndexes[variableNamePrefix]) { nextVariableIndexes[variableNamePrefix] = 1; } - const currentSuffix = parseNumericSuffix( - variableName, - variableNamePrefix, - ); + const currentSuffix = parseNumericSuffix(variableName, variableNamePrefix); if (currentSuffix) { const nextIndex = nextVariableIndexes[variableNamePrefix]; // If the current suffix doesn't match the expected next index, rename it if (currentSuffix !== nextIndex) { const newVariableName = variableNamePrefix + nextIndex; - let newVariable = blockInstance.workspace - .getVariableMap() - .getVariable(newVariableName); + let newVariable = blockInstance.workspace.getVariableMap().getVariable(newVariableName); if (!newVariable) { newVariable = blockInstance.workspace .getVariableMap() @@ -2041,7 +1928,7 @@ export function handleBlockCreateEvent( } nextVariableIndexes[variableNamePrefix] = Math.max( nextVariableIndexes[variableNamePrefix], - currentSuffix + 1, + currentSuffix + 1 ); } } @@ -2052,7 +1939,7 @@ export function handleBlockCreateEvent( // Extend the built-in Blockly procedures_defreturn block to add custom toggle functionality // Reference to the original init function of the procedures_defreturn block -Blockly.Blocks["procedures_defreturn"].init = (function (originalInit) { +Blockly.Blocks['procedures_defreturn'].init = (function (originalInit) { return function () { // Call the original initialization function to ensure the block retains its default behaviour originalInit.call(this); @@ -2060,10 +1947,10 @@ Blockly.Blocks["procedures_defreturn"].init = (function (originalInit) { // Use the existing addToggleButton helper to add the button to the block addToggleButton(this); }; -})(Blockly.Blocks["procedures_defreturn"].init); +})(Blockly.Blocks['procedures_defreturn'].init); // Create an extension that adds extra UI logic without modifying the core mutator methods -Blockly.Extensions.register("custom_procedure_ui_extension", function () { +Blockly.Extensions.register('custom_procedure_ui_extension', function () { this.toggleDoBlock = function () { const isInline = !this.isInline; @@ -2076,28 +1963,23 @@ Blockly.Extensions.register("custom_procedure_ui_extension", function () { updateShape(this, isInline); // Optionally re-enable if previously disabled (for orphaned block UX) - if (this.hasDisabledReason && this.hasDisabledReason("ORPHANED_BLOCK")) { - this.setDisabledReason(false, "ORPHANED_BLOCK"); + if (this.hasDisabledReason && this.hasDisabledReason('ORPHANED_BLOCK')) { + this.setDisabledReason(false, 'ORPHANED_BLOCK'); } // Fire Blockly events so undo/redo and UI updates are tracked - Blockly.Events.fire( - new Blockly.Events.BlockChange(this, "mutation", null, "", ""), - ); + Blockly.Events.fire(new Blockly.Events.BlockChange(this, 'mutation', null, '', '')); Blockly.Events.fire(new Blockly.Events.BlockMove(this)); }; }); // Apply the extension to the built-in 'procedures_defreturn' block -Blockly.Extensions.apply( - "custom_procedure_ui_extension", - Blockly.Blocks["procedures_defreturn"], -); +Blockly.Extensions.apply('custom_procedure_ui_extension', Blockly.Blocks['procedures_defreturn']); // Extend the built-in Blockly procedures_defnoreturn block to add custom toggle functionality // Reference to the original init function of the procedures_defnoreturn block -Blockly.Blocks["procedures_defnoreturn"].init = (function (originalInit) { +Blockly.Blocks['procedures_defnoreturn'].init = (function (originalInit) { return function () { // Call the original initialization function to ensure the block retains its default behaviour originalInit.call(this); @@ -2105,18 +1987,15 @@ Blockly.Blocks["procedures_defnoreturn"].init = (function (originalInit) { // Use the existing addToggleButton helper to add the button to the block addToggleButton(this); }; -})(Blockly.Blocks["procedures_defnoreturn"].init); +})(Blockly.Blocks['procedures_defnoreturn'].init); // Apply the extension to the built-in 'procedures_defnoreturn' block -Blockly.Extensions.apply( - "custom_procedure_ui_extension", - Blockly.Blocks["procedures_defnoreturn"], -); +Blockly.Extensions.apply('custom_procedure_ui_extension', Blockly.Blocks['procedures_defnoreturn']); // Define unique IDs for each option -Blockly.FieldVariable.ADD_VARIABLE_ID = "ADD_VARIABLE_ID"; -Blockly.FieldVariable.RENAME_VARIABLE_ID = "RENAME_VARIABLE_ID"; -Blockly.FieldVariable.DELETE_VARIABLE_ID = "DELETE_VARIABLE_ID"; +Blockly.FieldVariable.ADD_VARIABLE_ID = 'ADD_VARIABLE_ID'; +Blockly.FieldVariable.RENAME_VARIABLE_ID = 'RENAME_VARIABLE_ID'; +Blockly.FieldVariable.DELETE_VARIABLE_ID = 'DELETE_VARIABLE_ID'; // Extend `getOptions` to include "New variable..." at the top of the dropdown const originalGetOptions = Blockly.FieldVariable.prototype.getOptions; @@ -2125,10 +2004,7 @@ Blockly.FieldVariable.prototype.getOptions = function () { const options = originalGetOptions.call(this); // Add the "New variable..." option at the beginning - options.unshift([ - translate("new_variable_decision"), - Blockly.FieldVariable.ADD_VARIABLE_ID, - ]); + options.unshift([translate('new_variable_decision'), Blockly.FieldVariable.ADD_VARIABLE_ID]); return options; }; @@ -2155,7 +2031,7 @@ Blockly.FieldVariable.prototype.onItemSelected_ = function (menu, menuItem) { this.forceRerender(); // Refresh the UI to show the new selection } } - }, + } ); } else { // Use the stored reference to avoid recursion @@ -2164,7 +2040,7 @@ Blockly.FieldVariable.prototype.onItemSelected_ = function (menu, menuItem) { }; (function () { - const dynamicIf = Blockly.Blocks["dynamic_if"]; + const dynamicIf = Blockly.Blocks['dynamic_if']; if (!dynamicIf) return; const originalFinalize = dynamicIf.finalizeConnections; @@ -2173,12 +2049,12 @@ Blockly.FieldVariable.prototype.onItemSelected_ = function (menu, menuItem) { dynamicIf.mutationToDom = function () { if (this._skipFinalizeInMutationToDom) { if (!this.elseifCount && !this.elseCount) return null; - const container = Blockly.utils.xml.createElement("mutation"); + const container = Blockly.utils.xml.createElement('mutation'); if (this.elseifCount) { - container.setAttribute("elseif", `${this.elseifCount}`); + container.setAttribute('elseif', `${this.elseifCount}`); } if (this.elseCount) { - container.setAttribute("else", "1"); + container.setAttribute('else', '1'); } return container; } @@ -2208,16 +2084,10 @@ Blockly.FieldVariable.prototype.onItemSelected_ = function (menu, menuItem) { // Fire one synthetic mutation event to represent the entire rebuild. let mutationEvent; - if (typeof Blockly.Events.Mutation === "function") { + if (typeof Blockly.Events.Mutation === 'function') { mutationEvent = new Blockly.Events.Mutation(this, oldState, newState); } else { - mutationEvent = new Blockly.Events.BlockChange( - this, - "mutation", - "", - oldState, - newState, - ); + mutationEvent = new Blockly.Events.BlockChange(this, 'mutation', '', oldState, newState); } Blockly.Events.fire(mutationEvent); }; diff --git a/blocks/camera.js b/blocks/camera.js index 5d32a98d..6ef5aa3e 100644 --- a/blocks/camera.js +++ b/blocks/camera.js @@ -1,146 +1,142 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { getHelpUrlFor } from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor } from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; export function defineCameraBlocks() { - Blockly.Blocks["camera_control"] = { + Blockly.Blocks['camera_control'] = { init: function () { this.jsonInit({ - type: "camera_control", - message0: translate("camera_control"), + type: 'camera_control', + message0: translate('camera_control'), args0: [ { - type: "field_dropdown", - name: "ACTION", + type: 'field_dropdown', + name: 'ACTION', options: [ - getDropdownOption("rotateLeft"), - getDropdownOption("rotateRight"), - getDropdownOption("rotateUp"), - getDropdownOption("rotateDown"), - getDropdownOption("moveUp"), - getDropdownOption("moveDown"), - getDropdownOption("moveLeft"), - getDropdownOption("moveRight"), + getDropdownOption('rotateLeft'), + getDropdownOption('rotateRight'), + getDropdownOption('rotateUp'), + getDropdownOption('rotateDown'), + getDropdownOption('moveUp'), + getDropdownOption('moveDown'), + getDropdownOption('moveLeft'), + getDropdownOption('moveRight'), ], }, { - type: "field_grid_dropdown", - name: "KEY", + type: 'field_grid_dropdown', + name: 'KEY', columns: 10, options: [ - getDropdownOption("0"), - getDropdownOption("1"), - getDropdownOption("2"), - getDropdownOption("3"), - getDropdownOption("4"), - getDropdownOption("5"), - getDropdownOption("6"), - getDropdownOption("7"), - getDropdownOption("8"), - getDropdownOption("9"), - getDropdownOption("a"), - getDropdownOption("b"), - getDropdownOption("c"), - getDropdownOption("d"), - getDropdownOption("e"), - getDropdownOption("f"), - getDropdownOption("g"), - getDropdownOption("h"), - getDropdownOption("i"), - getDropdownOption("j"), - getDropdownOption("k"), - getDropdownOption("l"), - getDropdownOption("m"), - getDropdownOption("n"), - getDropdownOption("o"), - getDropdownOption("p"), - getDropdownOption("q"), - getDropdownOption("r"), - getDropdownOption("s"), - getDropdownOption("t"), - getDropdownOption("u"), - getDropdownOption("v"), - getDropdownOption("w"), - getDropdownOption("x"), - getDropdownOption("y"), - getDropdownOption("z"), - getDropdownOption(" "), - getDropdownOption(","), - getDropdownOption("."), - getDropdownOption("/"), - getDropdownOption("ArrowLeft"), - getDropdownOption("ArrowUp"), - getDropdownOption("ArrowRight"), - getDropdownOption("ArrowDown"), + getDropdownOption('0'), + getDropdownOption('1'), + getDropdownOption('2'), + getDropdownOption('3'), + getDropdownOption('4'), + getDropdownOption('5'), + getDropdownOption('6'), + getDropdownOption('7'), + getDropdownOption('8'), + getDropdownOption('9'), + getDropdownOption('a'), + getDropdownOption('b'), + getDropdownOption('c'), + getDropdownOption('d'), + getDropdownOption('e'), + getDropdownOption('f'), + getDropdownOption('g'), + getDropdownOption('h'), + getDropdownOption('i'), + getDropdownOption('j'), + getDropdownOption('k'), + getDropdownOption('l'), + getDropdownOption('m'), + getDropdownOption('n'), + getDropdownOption('o'), + getDropdownOption('p'), + getDropdownOption('q'), + getDropdownOption('r'), + getDropdownOption('s'), + getDropdownOption('t'), + getDropdownOption('u'), + getDropdownOption('v'), + getDropdownOption('w'), + getDropdownOption('x'), + getDropdownOption('y'), + getDropdownOption('z'), + getDropdownOption(' '), + getDropdownOption(','), + getDropdownOption('.'), + getDropdownOption('/'), + getDropdownOption('ArrowLeft'), + getDropdownOption('ArrowUp'), + getDropdownOption('ArrowRight'), + getDropdownOption('ArrowDown'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("camera_control"), + colour: categoryColours['Scene'], + tooltip: getTooltip('camera_control'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; - Blockly.Blocks["camera_follow"] = { + Blockly.Blocks['camera_follow'] = { init: function () { this.jsonInit({ - type: "camera_follow", - message0: translate("camera_follow"), + type: 'camera_follow', + message0: translate('camera_follow'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "RADIUS", - check: "Number", + type: 'input_value', + name: 'RADIUS', + check: 'Number', }, { - type: "field_checkbox", - name: "FRONT", + type: 'field_checkbox', + name: 'FRONT', checked: false, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("camera_follow"), + colour: categoryColours['Scene'], + tooltip: getTooltip('camera_follow'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; - Blockly.Blocks["get_camera"] = { + Blockly.Blocks['get_camera'] = { init: function () { this.jsonInit({ - type: "get_camera", - message0: translate("get_camera"), + type: 'get_camera', + message0: translate('get_camera'), args0: [ { - type: "field_variable", - name: "VAR", - variable: "camera", // Default variable is 'camera' + type: 'field_variable', + name: 'VAR', + variable: 'camera', // Default variable is 'camera' }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("get_camera"), + colour: categoryColours['Scene'], + tooltip: getTooltip('get_camera'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; } diff --git a/blocks/colour.js b/blocks/colour.js index 717da59a..87c99a1d 100644 --- a/blocks/colour.js +++ b/blocks/colour.js @@ -1,113 +1,113 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { getHelpUrlFor } from "./blocks.js"; -import { translate, getTooltip } from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor } from './blocks.js'; +import { translate, getTooltip } from '../main/translation.js'; export function defineColourBlocks() { // Basic colour picker block - Blockly.Blocks["colour_picker"] = { + Blockly.Blocks['colour_picker'] = { init: function () { this.jsonInit({ - type: "colour_picker", - message0: "%1", + type: 'colour_picker', + message0: '%1', args0: [ { - type: "field_colour", - name: "COLOUR", - colour: "#ff0000", + type: 'field_colour', + name: 'COLOUR', + colour: '#ff0000', }, ], - output: "Colour", - colour: categoryColours["Materials"], - tooltip: getTooltip("colour_picker"), - helpUrl: "", + output: 'Colour', + colour: categoryColours['Materials'], + tooltip: getTooltip('colour_picker'), + helpUrl: '', }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; // Random colour block - Blockly.Blocks["colour_random"] = { + Blockly.Blocks['colour_random'] = { init: function () { this.jsonInit({ - type: "colour_random", - message0: translate("colour_random"), - output: "Colour", - colour: categoryColours["Materials"], - tooltip: getTooltip("colour_random"), - helpUrl: "", + type: 'colour_random', + message0: translate('colour_random'), + output: 'Colour', + colour: categoryColours['Materials'], + tooltip: getTooltip('colour_random'), + helpUrl: '', }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; // RGB colour block - Blockly.Blocks["colour_rgb"] = { + Blockly.Blocks['colour_rgb'] = { init: function () { this.jsonInit({ - type: "colour_rgb", - message0: translate("colour_rgb"), + type: 'colour_rgb', + message0: translate('colour_rgb'), args0: [ { - type: "input_value", - name: "RED", - check: "Number", + type: 'input_value', + name: 'RED', + check: 'Number', }, { - type: "input_value", - name: "GREEN", - check: "Number", + type: 'input_value', + name: 'GREEN', + check: 'Number', }, { - type: "input_value", - name: "BLUE", - check: "Number", + type: 'input_value', + name: 'BLUE', + check: 'Number', }, ], inputsInline: true, - output: "Colour", - colour: categoryColours["Materials"], - tooltip: getTooltip("colour_rgb"), - helpUrl: "", + output: 'Colour', + colour: categoryColours['Materials'], + tooltip: getTooltip('colour_rgb'), + helpUrl: '', }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; // Blend colours block - Blockly.Blocks["colour_blend"] = { + Blockly.Blocks['colour_blend'] = { init: function () { this.jsonInit({ - type: "colour_blend", - message0: translate("colour_blend"), + type: 'colour_blend', + message0: translate('colour_blend'), args0: [ { - type: "input_value", - name: "COLOUR1", - check: "Colour", + type: 'input_value', + name: 'COLOUR1', + check: 'Colour', }, { - type: "input_value", - name: "COLOUR2", - check: "Colour", + type: 'input_value', + name: 'COLOUR2', + check: 'Colour', }, { - type: "input_value", - name: "RATIO", - check: "Number", + type: 'input_value', + name: 'RATIO', + check: 'Number', }, ], inputsInline: true, - output: "Colour", - colour: categoryColours["Materials"], - tooltip: getTooltip("colour_blend"), - helpUrl: "", + output: 'Colour', + colour: categoryColours['Materials'], + tooltip: getTooltip('colour_blend'), + helpUrl: '', }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; } diff --git a/blocks/combine.js b/blocks/combine.js index 1da2bd23..be8d47c5 100644 --- a/blocks/combine.js +++ b/blocks/combine.js @@ -1,43 +1,42 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { getHelpUrlFor, nextVariableIndexes, handleBlockCreateEvent, registerBlockHandler, -} from "./blocks.js"; -import { translate, getTooltip } from "../main/translation.js"; +} from './blocks.js'; +import { translate, getTooltip } from '../main/translation.js'; export function defineCombineBlocks() { - Blockly.Blocks["merge_meshes"] = { + Blockly.Blocks['merge_meshes'] = { init: function () { - const variableNamePrefix = "merged"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'merged'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "merge_meshes", - message0: translate("merge_meshes"), + type: 'merge_meshes', + message0: translate('merge_meshes'), args0: [ { - type: "field_variable", - name: "RESULT_VAR", + type: 'field_variable', + name: 'RESULT_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "MESH_LIST", - check: "Array", + type: 'input_value', + name: 'MESH_LIST', + check: 'Array', }, ], - colour: categoryColours["Transform"], - tooltip: getTooltip("merge_meshes"), + colour: categoryColours['Transform'], + tooltip: getTooltip('merge_meshes'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); registerBlockHandler(this, (changeEvent) => handleBlockCreateEvent( @@ -45,46 +44,45 @@ export function defineCombineBlocks() { changeEvent, variableNamePrefix, nextVariableIndexes, - "RESULT_VAR", - ), + 'RESULT_VAR' + ) ); }, }; - Blockly.Blocks["subtract_meshes"] = { + Blockly.Blocks['subtract_meshes'] = { init: function () { - const variableNamePrefix = "subtracted"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'subtracted'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "subtract_meshes", - message0: translate("subtract_meshes"), + type: 'subtract_meshes', + message0: translate('subtract_meshes'), args0: [ { - type: "field_variable", - name: "RESULT_VAR", + type: 'field_variable', + name: 'RESULT_VAR', variable: nextVariableName, }, { - type: "field_variable", - name: "BASE_MESH", - variable: "object", + type: 'field_variable', + name: 'BASE_MESH', + variable: 'object', }, { - type: "input_value", - name: "MESH_LIST", - check: "Array", + type: 'input_value', + name: 'MESH_LIST', + check: 'Array', }, ], - colour: categoryColours["Transform"], - tooltip: getTooltip("subtract_meshes"), + colour: categoryColours['Transform'], + tooltip: getTooltip('subtract_meshes'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); registerBlockHandler(this, (changeEvent) => handleBlockCreateEvent( @@ -92,41 +90,40 @@ export function defineCombineBlocks() { changeEvent, variableNamePrefix, nextVariableIndexes, - "RESULT_VAR", - ), + 'RESULT_VAR' + ) ); }, }; - Blockly.Blocks["intersection_meshes"] = { + Blockly.Blocks['intersection_meshes'] = { init: function () { - const variableNamePrefix = "intersection"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'intersection'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "intersection_meshes", - message0: translate("intersection_meshes"), + type: 'intersection_meshes', + message0: translate('intersection_meshes'), args0: [ { - type: "field_variable", - name: "RESULT_VAR", + type: 'field_variable', + name: 'RESULT_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "MESH_LIST", - check: "Array", + type: 'input_value', + name: 'MESH_LIST', + check: 'Array', }, ], - colour: categoryColours["Transform"], - tooltip: getTooltip("intersection_meshes"), + colour: categoryColours['Transform'], + tooltip: getTooltip('intersection_meshes'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); registerBlockHandler(this, (changeEvent) => handleBlockCreateEvent( @@ -134,41 +131,40 @@ export function defineCombineBlocks() { changeEvent, variableNamePrefix, nextVariableIndexes, - "RESULT_VAR", - ), + 'RESULT_VAR' + ) ); }, }; - Blockly.Blocks["hull_meshes"] = { + Blockly.Blocks['hull_meshes'] = { init: function () { - const variableNamePrefix = "hull"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'hull'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "hull_meshes", - message0: translate("hull_meshes"), + type: 'hull_meshes', + message0: translate('hull_meshes'), args0: [ { - type: "field_variable", - name: "RESULT_VAR", + type: 'field_variable', + name: 'RESULT_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "MESH_LIST", - check: "Array", + type: 'input_value', + name: 'MESH_LIST', + check: 'Array', }, ], - colour: categoryColours["Transform"], - tooltip: getTooltip("hull_meshes"), + colour: categoryColours['Transform'], + tooltip: getTooltip('hull_meshes'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); registerBlockHandler(this, (changeEvent) => handleBlockCreateEvent( @@ -176,8 +172,8 @@ export function defineCombineBlocks() { changeEvent, variableNamePrefix, nextVariableIndexes, - "RESULT_VAR", - ), + 'RESULT_VAR' + ) ); }, }; diff --git a/blocks/condition.js b/blocks/condition.js index e6354d6a..0d155933 100644 --- a/blocks/condition.js +++ b/blocks/condition.js @@ -1,31 +1,31 @@ -import * as Blockly from "blockly"; -import { getHelpUrlFor } from "./blocks.js"; +import * as Blockly from 'blockly'; +import { getHelpUrlFor } from './blocks.js'; export function defineConditionBlocks() { - Blockly.Blocks["controls_if"] = Blockly.Blocks["dynamic_if"]; + Blockly.Blocks['controls_if'] = Blockly.Blocks['dynamic_if']; - const oldInit = Blockly.Blocks["controls_if"].init; + const oldInit = Blockly.Blocks['controls_if'].init; - Blockly.Blocks["controls_if"].init = function () { + Blockly.Blocks['controls_if'].init = function () { this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("condition_blocks"); + this.setStyle('condition_blocks'); // Call the original init function oldInit.call(this); // Override the tooltip after the original init this.setTooltip(() => { - let tooltip = "Execute actions if a condition is true."; + let tooltip = 'Execute actions if a condition is true.'; tooltip += ` Drag additional conditions to create else if branches.`; - tooltip += " Drag a statement at the end to create an else branch."; + tooltip += ' Drag a statement at the end to create an else branch.'; return tooltip; }); }; - Blockly.Blocks["controls_if_custom"] = { + Blockly.Blocks['controls_if_custom'] = { init: function () { this.elseifCount_ = 0; this.elseCount_ = 0; @@ -57,7 +57,7 @@ export function defineConditionBlocks() { makePlusIcon_: function (onClick) { return new Blockly.FieldImage( - "data:image/svg+xml;utf8," + + 'data:image/svg+xml;utf8,' + encodeURIComponent(` @@ -66,14 +66,14 @@ export function defineConditionBlocks() { `), 20, 20, - "add branch", - onClick, + 'add branch', + onClick ); }, makeMinusIcon_: function (onClick) { return new Blockly.FieldImage( - "data:image/svg+xml;utf8," + + 'data:image/svg+xml;utf8,' + encodeURIComponent(` @@ -81,8 +81,8 @@ export function defineConditionBlocks() { `), 20, 20, - "remove branch", - onClick, + 'remove branch', + onClick ); }, @@ -91,40 +91,34 @@ export function defineConditionBlocks() { while (this.inputList.length) this.removeInput(this.inputList[0].name); // IF0 row - this.appendValueInput("IF0") - .setCheck("Boolean") - .appendField("if"); - this.appendDummyInput("ICONS_IF0") + this.appendValueInput('IF0').setCheck('Boolean').appendField('if'); + this.appendDummyInput('ICONS_IF0') .setAlign(Blockly.inputs.Align.RIGHT) .appendField(this.makePlusIcon_(() => this.addElseIfAt(0))); - this.appendStatementInput("DO0").appendField(new Blockly.FieldLabel("")); + this.appendStatementInput('DO0').appendField(new Blockly.FieldLabel('')); // ELSE IFs for (let i = 1; i <= this.elseifCount_; i++) { - this.appendValueInput("IF" + i) - .setCheck("Boolean") - .appendField("else if"); - this.appendDummyInput("ICONS_" + i) + this.appendValueInput('IF' + i) + .setCheck('Boolean') + .appendField('else if'); + this.appendDummyInput('ICONS_' + i) .setAlign(Blockly.inputs.Align.RIGHT) .appendField(this.makePlusIcon_(() => this.addElseIfAt(i))) .appendField(this.makeMinusIcon_(() => this.removeElseIf_(i))); - this.appendStatementInput("DO" + i).appendField( - new Blockly.FieldLabel(""), - ); + this.appendStatementInput('DO' + i).appendField(new Blockly.FieldLabel('')); } // ELSE if (this.elseCount_) { - const elseInput = this.appendDummyInput("ELSE_ROW"); - elseInput.appendField("else"); + const elseInput = this.appendDummyInput('ELSE_ROW'); + elseInput.appendField('else'); elseInput.appendField(this.makeMinusIcon_(() => this.removeElse_())); - this.appendStatementInput("ELSE") - .setCheck(null) - .appendField(new Blockly.FieldLabel("")); + this.appendStatementInput('ELSE').setCheck(null).appendField(new Blockly.FieldLabel('')); } - const bottom = this.appendDummyInput("BOTTOM_ADD"); + const bottom = this.appendDummyInput('BOTTOM_ADD'); bottom .setAlign(Blockly.inputs.Align.RIGHT) .appendField(this.makePlusIcon_(() => this.addElseOrElseIf())); @@ -144,29 +138,29 @@ export function defineConditionBlocks() { addElseIfAt: function (index) { this.elseifCount_++; for (let i = this.elseifCount_ - 1; i >= index + 1; i--) { - this.renameInput_("IF" + i, "IF" + (i + 1)); - this.renameInput_("DO" + i, "DO" + (i + 1)); - this.renameInput_("ICONS_" + i, "ICONS_" + (i + 1)); + this.renameInput_('IF' + i, 'IF' + (i + 1)); + this.renameInput_('DO' + i, 'DO' + (i + 1)); + this.renameInput_('ICONS_' + i, 'ICONS_' + (i + 1)); } this.updateShape_(); }, removeElseIf_: function (index) { - this.removeInput("IF" + index); - this.removeInput("DO" + index); - this.removeInput("ICONS_" + index); + this.removeInput('IF' + index); + this.removeInput('DO' + index); + this.removeInput('ICONS_' + index); for (let i = index + 1; i <= this.elseifCount_; i++) { - this.renameInput_("IF" + i, "IF" + (i - 1)); - this.renameInput_("DO" + i, "DO" + (i - 1)); - this.renameInput_("ICONS_" + i, "ICONS_" + (i - 1)); + this.renameInput_('IF' + i, 'IF' + (i - 1)); + this.renameInput_('DO' + i, 'DO' + (i - 1)); + this.renameInput_('ICONS_' + i, 'ICONS_' + (i - 1)); } this.elseifCount_--; this.updateShape_(); }, removeElse_: function () { - this.removeInput("ELSE"); - this.removeInput("ELSE_ROW"); + this.removeInput('ELSE'); + this.removeInput('ELSE_ROW'); this.elseCount_ = 0; this.updateShape_(); }, @@ -177,16 +171,15 @@ export function defineConditionBlocks() { }, mutationToDom: function () { - const container = document.createElement("mutation"); - if (this.elseifCount_) - container.setAttribute("elseif", this.elseifCount_); - if (this.elseCount_) container.setAttribute("else", 1); + const container = document.createElement('mutation'); + if (this.elseifCount_) container.setAttribute('elseif', this.elseifCount_); + if (this.elseCount_) container.setAttribute('else', 1); return container; }, domToMutation: function (xmlElement) { - this.elseifCount_ = parseInt(xmlElement.getAttribute("elseif")) || 0; - this.elseCount_ = parseInt(xmlElement.getAttribute("else")) || 0; + this.elseifCount_ = parseInt(xmlElement.getAttribute('elseif')) || 0; + this.elseCount_ = parseInt(xmlElement.getAttribute('else')) || 0; this.updateShape_(); }, diff --git a/blocks/connect.js b/blocks/connect.js index 83d7a77c..b5dee477 100644 --- a/blocks/connect.js +++ b/blocks/connect.js @@ -1,329 +1,325 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { getHelpUrlFor } from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor } from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; -import { getAttachNames } from "../config.js"; +import { getAttachNames } from '../config.js'; export function defineConnectBlocks() { - Blockly.Blocks["parent"] = { + Blockly.Blocks['parent'] = { init: function () { this.jsonInit({ - type: "parent", - message0: translate("parent"), + type: 'parent', + message0: translate('parent'), args0: [ { - type: "field_variable", - name: "PARENT_MESH", - variable: "parent", + type: 'field_variable', + name: 'PARENT_MESH', + variable: 'parent', }, { - type: "field_variable", - name: "CHILD_MESH", + type: 'field_variable', + name: 'CHILD_MESH', variable: window.currentMesh, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("parent"), + tooltip: getTooltip('parent'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["parent_child"] = { + Blockly.Blocks['parent_child'] = { init: function () { this.jsonInit({ - type: "parent_child", - message0: translate("parent_child"), + type: 'parent_child', + message0: translate('parent_child'), args0: [ { - type: "field_variable", - name: "PARENT_MESH", - variable: "parent", + type: 'field_variable', + name: 'PARENT_MESH', + variable: 'parent', }, { - type: "field_variable", - name: "CHILD_MESH", + type: 'field_variable', + name: 'CHILD_MESH', variable: window.currentMesh, }, { - type: "input_value", - name: "X_OFFSET", - check: "Number", + type: 'input_value', + name: 'X_OFFSET', + check: 'Number', }, { - type: "input_value", - name: "Y_OFFSET", - check: "Number", + type: 'input_value', + name: 'Y_OFFSET', + check: 'Number', }, { - type: "input_value", - name: "Z_OFFSET", - check: "Number", + type: 'input_value', + name: 'Z_OFFSET', + check: 'Number', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("parent_child"), + tooltip: getTooltip('parent_child'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["remove_parent"] = { + Blockly.Blocks['remove_parent'] = { init: function () { this.jsonInit({ - type: "remove_parent", - message0: translate("remove_parent"), + type: 'remove_parent', + message0: translate('remove_parent'), args0: [ { - type: "field_variable", - name: "CHILD_MESH", + type: 'field_variable', + name: 'CHILD_MESH', variable: window.currentMesh, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("remove_parent"), + colour: categoryColours['Transform'], + tooltip: getTooltip('remove_parent'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["stop_follow"] = { + Blockly.Blocks['stop_follow'] = { init: function () { this.jsonInit({ - type: "stop_follow", - message0: translate("stop_follow"), + type: 'stop_follow', + message0: translate('stop_follow'), args0: [ { - type: "field_variable", - name: "FOLLOWER_MESH", + type: 'field_variable', + name: 'FOLLOWER_MESH', variable: window.currentMesh, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("stop_follow"), + colour: categoryColours['Transform'], + tooltip: getTooltip('stop_follow'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["hold"] = { + Blockly.Blocks['hold'] = { init: function () { this.jsonInit({ - type: "hold", - message0: translate("hold"), + type: 'hold', + message0: translate('hold'), args0: [ { - type: "field_variable", - name: "TARGET_MESH", - variable: "target", + type: 'field_variable', + name: 'TARGET_MESH', + variable: 'target', }, { - type: "field_variable", - name: "MESH_TO_ATTACH", - variable: "object", + type: 'field_variable', + name: 'MESH_TO_ATTACH', + variable: 'object', }, { - type: "input_value", - name: "X_OFFSET", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'X_OFFSET', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Y_OFFSET", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Y_OFFSET', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Z_OFFSET", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Z_OFFSET', + check: 'Number', + align: 'RIGHT', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("hold"), + tooltip: getTooltip('hold'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["attach"] = { + Blockly.Blocks['attach'] = { init: function () { this.jsonInit({ - type: "attach", - message0: translate("attach"), + type: 'attach', + message0: translate('attach'), args0: [ { - type: "field_variable", - name: "MESH_TO_ATTACH", - variable: "object", + type: 'field_variable', + name: 'MESH_TO_ATTACH', + variable: 'object', }, { - type: "field_variable", - name: "TARGET_MESH", - variable: "target", + type: 'field_variable', + name: 'TARGET_MESH', + variable: 'target', }, { - type: "field_dropdown", - name: "BONE_NAME", + type: 'field_dropdown', + name: 'BONE_NAME', options: getAttachNames(), }, { - type: "input_value", - name: "X_OFFSET", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'X_OFFSET', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Y_OFFSET", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Y_OFFSET', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Z_OFFSET", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Z_OFFSET', + check: 'Number', + align: 'RIGHT', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("attach"), + tooltip: getTooltip('attach'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["drop"] = { + Blockly.Blocks['drop'] = { init: function () { this.jsonInit({ - type: "drop", - message0: translate("drop"), + type: 'drop', + message0: translate('drop'), args0: [ { - type: "field_variable", - name: "MESH_TO_DETACH", - variable: "object", + type: 'field_variable', + name: 'MESH_TO_DETACH', + variable: 'object', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("drop"), + tooltip: getTooltip('drop'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["follow"] = { + Blockly.Blocks['follow'] = { init: function () { this.jsonInit({ - type: "follow", - message0: translate("follow"), + type: 'follow', + message0: translate('follow'), args0: [ { - type: "field_variable", - name: "FOLLOWER_MESH", - variable: "follower", + type: 'field_variable', + name: 'FOLLOWER_MESH', + variable: 'follower', }, { - type: "field_variable", - name: "TARGET_MESH", - variable: "target", + type: 'field_variable', + name: 'TARGET_MESH', + variable: 'target', }, { - type: "field_dropdown", - name: "FOLLOW_POSITION", + type: 'field_dropdown', + name: 'FOLLOW_POSITION', options: [ - getDropdownOption("TOP"), - getDropdownOption("CENTER"), - getDropdownOption("BOTTOM"), + getDropdownOption('TOP'), + getDropdownOption('CENTER'), + getDropdownOption('BOTTOM'), ], }, { - type: "input_value", - name: "X_OFFSET", - check: "Number", + type: 'input_value', + name: 'X_OFFSET', + check: 'Number', }, { - type: "input_value", - name: "Y_OFFSET", - check: "Number", + type: 'input_value', + name: 'Y_OFFSET', + check: 'Number', }, { - type: "input_value", - name: "Z_OFFSET", - check: "Number", + type: 'input_value', + name: 'Z_OFFSET', + check: 'Number', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("follow"), + tooltip: getTooltip('follow'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["export_mesh"] = { + Blockly.Blocks['export_mesh'] = { init: function () { this.jsonInit({ - type: "export_mesh", - message0: translate("export_mesh"), + type: 'export_mesh', + message0: translate('export_mesh'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "FORMAT", + type: 'field_dropdown', + name: 'FORMAT', options: [ - ["STL", "STL"], - ["OBJ", "OBJ"], - ["GLB", "GLB"], + ['STL', 'STL'], + ['OBJ', 'OBJ'], + ['GLB', 'GLB'], ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("export_mesh"), + colour: categoryColours['Scene'], + tooltip: getTooltip('export_mesh'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; } diff --git a/blocks/control.js b/blocks/control.js index a48f78b4..89c00f42 100644 --- a/blocks/control.js +++ b/blocks/control.js @@ -1,155 +1,152 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { getHelpUrlFor, applyInputAriaLabels } from "./blocks.js"; -import { translate, getTooltip } from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor, applyInputAriaLabels } from './blocks.js'; +import { translate, getTooltip } from '../main/translation.js'; export function defineControlBlocks() { - Blockly.Blocks["wait"] = { + Blockly.Blocks['wait'] = { init: function () { this.jsonInit({ - type: "wait", - message0: translate("wait"), + type: 'wait', + message0: translate('wait'), args0: [ { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Control"], - tooltip: getTooltip("wait"), + colour: categoryColours['Control'], + tooltip: getTooltip('wait'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("control_blocks"); + this.setStyle('control_blocks'); }, }; - Blockly.Blocks["wait_seconds"] = { + Blockly.Blocks['wait_seconds'] = { init: function () { this.jsonInit({ - type: "wait", - message0: translate("wait_seconds"), + type: 'wait', + message0: translate('wait_seconds'), args0: [ { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Control"], - tooltip: getTooltip("wait_seconds"), + colour: categoryColours['Control'], + tooltip: getTooltip('wait_seconds'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("control_blocks"); + this.setStyle('control_blocks'); }, }; - Blockly.Blocks["wait_until"] = { + Blockly.Blocks['wait_until'] = { init: function () { this.jsonInit({ - type: "wait_until", - message0: translate("wait_until"), + type: 'wait_until', + message0: translate('wait_until'), args0: [ { - type: "input_value", - name: "CONDITION", - check: "Boolean", + type: 'input_value', + name: 'CONDITION', + check: 'Boolean', }, ], previousStatement: null, nextStatement: null, //colour: categoryColours["Control"], - tooltip: getTooltip("wait_until"), + tooltip: getTooltip('wait_until'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("control_blocks"); + this.setStyle('control_blocks'); }, }; - Blockly.Blocks["local_variable"] = { + Blockly.Blocks['local_variable'] = { init: function () { this.jsonInit({ - type: "local_variable", - message0: translate("local_variable"), + type: 'local_variable', + message0: translate('local_variable'), args0: [ { - type: "field_variable", - name: "VAR", - variable: "item", // default variable name + type: 'field_variable', + name: 'VAR', + variable: 'item', // default variable name }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Control"], - tooltip: getTooltip("local_variable"), + colour: categoryColours['Control'], + tooltip: getTooltip('local_variable'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("control_blocks"); + this.setStyle('control_blocks'); }, }; - Blockly.Blocks["for_loop"] = { + Blockly.Blocks['for_loop'] = { init: function () { this.jsonInit({ - type: "for_loop", - message0: translate("for_loop"), + type: 'for_loop', + message0: translate('for_loop'), args0: [ { - type: "field_lexical_variable", - name: "VAR", - text: "count", // Default variable name is "count" - options: [["count", "count"]], + type: 'field_lexical_variable', + name: 'VAR', + text: 'count', // Default variable name is "count" + options: [['count', 'count']], }, { - type: "input_value", - name: "FROM", - check: "Number", + type: 'input_value', + name: 'FROM', + check: 'Number', }, { - type: "input_value", - name: "TO", - check: "Number", + type: 'input_value', + name: 'TO', + check: 'Number', }, { - type: "input_value", - name: "BY", - check: "Number", + type: 'input_value', + name: 'BY', + check: 'Number', }, { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', }, ], previousStatement: null, nextStatement: null, //colour: categoryColours["Control"], inputsInline: true, - tooltip: getTooltip("for_loop"), + tooltip: getTooltip('for_loop'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("control_blocks"); + this.setStyle('control_blocks'); }, // Returns an array of local variable names. getLexicalVariables: function () { - return [this.getFieldValue("VAR")]; + return [this.getFieldValue('VAR')]; }, // Update the variable name on this block. setLexicalVariable: function (newName) { - this.setFieldValue(String(newName), "VAR"); + this.setFieldValue(String(newName), 'VAR'); }, onchange: function (event) { - if ( - event.type === Blockly.Events.BLOCK_CREATE && - event.ids.includes(this.id) - ) { - const field = this.getField("VAR"); + if (event.type === Blockly.Events.BLOCK_CREATE && event.ids.includes(this.id)) { + const field = this.getField('VAR'); if (field && field.variableId_) { const oldId = field.variableId_; const newId = Blockly.utils.idGenerator.genUid(); @@ -157,17 +154,14 @@ export function defineControlBlocks() { // Recursively update nested getter blocks. const updateNestedBlocks = (block) => { - if ( - block.type === "get_lexical_variable" && - block.variableSourceId === oldId - ) { + if (block.type === 'get_lexical_variable' && block.variableSourceId === oldId) { block.variableSourceId = newId; } block.getChildren(false).forEach(updateNestedBlocks); }; // Update getter blocks inside the DO input. - const doConnection = this.getInput("DO")?.connection; + const doConnection = this.getInput('DO')?.connection; if (doConnection && doConnection.targetBlock()) { updateNestedBlocks(doConnection.targetBlock()); } @@ -177,31 +171,31 @@ export function defineControlBlocks() { // Save the current variable name and its unique id in a mutation. mutationToDom: function () { - const container = document.createElement("mutation"); - const field = this.getField("VAR"); + const container = document.createElement('mutation'); + const field = this.getField('VAR'); if (field && field.saveExtraState) { const extraState = field.saveExtraState(); - container.setAttribute("var", extraState.value); - container.setAttribute("variableid", extraState.variableId); + container.setAttribute('var', extraState.value); + container.setAttribute('variableid', extraState.variableId); } else { - container.setAttribute("var", this.getFieldValue("VAR")); + container.setAttribute('var', this.getFieldValue('VAR')); } return container; }, // Restore the variable name and unique id from the mutation. domToMutation: function (xmlElement) { - const varName = xmlElement.getAttribute("var"); - const variableId = xmlElement.getAttribute("variableid"); - const field = this.getField("VAR"); + const varName = xmlElement.getAttribute('var'); + const variableId = xmlElement.getAttribute('variableid'); + const field = this.getField('VAR'); if (field && field.loadExtraState) { field.loadExtraState({ value: varName, variableId: variableId, }); } else { - this.setFieldValue(varName, "VAR"); + this.setFieldValue(varName, 'VAR'); } }, }; @@ -209,8 +203,8 @@ export function defineControlBlocks() { class FieldLexicalVariable extends Blockly.FieldDropdown { constructor(opt_value, opt_validator) { // Force a default value of "count" if none is provided. - if (opt_value === null || opt_value === undefined || opt_value === "") { - opt_value = "count"; + if (opt_value === null || opt_value === undefined || opt_value === '') { + opt_value = 'count'; } super(opt_value, opt_validator); // Always use our custom options generator. @@ -222,7 +216,7 @@ export function defineControlBlocks() { // Ensure that the value is set (if somehow still null). if (!this.getValue()) { - super.setValue("count"); + super.setValue('count'); } } @@ -230,16 +224,16 @@ export function defineControlBlocks() { computeOptions() { // If the field's value is null-ish, force it to "count". let current = super.getValue(); - if (current === null || current === undefined || current === "") { - current = "count"; + if (current === null || current === undefined || current === '') { + current = 'count'; super.setValue(current); } else { current = String(current); } return [ [current, current], - ["Rename variable…", "__RENAME__"], - ["Get variable", "__GET__"], + ['Rename variable…', '__RENAME__'], + ['Get variable', '__GET__'], ]; } @@ -263,27 +257,24 @@ export function defineControlBlocks() { } getAriaTypeName() { - return "variable"; + return 'variable'; } setValue(value) { - if (value === "__RENAME__") { + if (value === '__RENAME__') { setTimeout(() => { const currentName = String(super.getValue()); - const newName = window.prompt("Rename variable", currentName); + const newName = window.prompt('Rename variable', currentName); if (newName && newName !== currentName) { - if ( - this.sourceBlock_ && - typeof this.sourceBlock_.setLexicalVariable === "function" - ) { + if (this.sourceBlock_ && typeof this.sourceBlock_.setLexicalVariable === 'function') { this.sourceBlock_.setLexicalVariable(String(newName)); } // Recompute and "lock in" our options with the new variable. this.cachedOptions_ = [ [String(newName), String(newName)], - ["Rename variable…", "__RENAME__"], - ["Get variable", "__GET__"], + ['Rename variable…', '__RENAME__'], + ['Get variable', '__GET__'], ]; // Force our generator to return the updated options. this.menuGenerator_ = () => this.cachedOptions_; @@ -293,13 +284,13 @@ export function defineControlBlocks() { const allBlocks = workspace.getAllBlocks(false); allBlocks.forEach((block) => { if ( - block.type === "get_lexical_variable" && + block.type === 'get_lexical_variable' && block.variableSourceId === this.variableId_ ) { - if (typeof block.updateVariable === "function") { + if (typeof block.updateVariable === 'function') { block.updateVariable(newName); } else { - block.setFieldValue(String(newName), "VAR"); + block.setFieldValue(String(newName), 'VAR'); } } }); @@ -312,14 +303,14 @@ export function defineControlBlocks() { } }, 0); return null; - } else if (value === "__GET__") { + } else if (value === '__GET__') { setTimeout(() => { const variableName = String(super.getValue()); const workspace = this.sourceBlock_.workspace; - const newBlock = workspace.newBlock("get_lexical_variable"); + const newBlock = workspace.newBlock('get_lexical_variable'); newBlock.initSvg(); newBlock.render(); - newBlock.setFieldValue(String(variableName), "VAR"); + newBlock.setFieldValue(String(variableName), 'VAR'); newBlock.variableSourceId = this.variableId_; const xy = this.sourceBlock_.getRelativeToSurfaceXY(); @@ -347,63 +338,60 @@ export function defineControlBlocks() { super.setValue(state.value); this.cachedOptions_ = [ [state.value, state.value], - ["Rename variable…", "__RENAME__"], - ["Get variable", "__GET__"], + ['Rename variable…', '__RENAME__'], + ['Get variable', '__GET__'], ]; this.menuGenerator_ = () => this.cachedOptions_; } } - Blockly.fieldRegistry.register( - "field_lexical_variable", - FieldLexicalVariable, - ); + Blockly.fieldRegistry.register('field_lexical_variable', FieldLexicalVariable); - Blockly.Blocks["get_lexical_variable"] = { + Blockly.Blocks['get_lexical_variable'] = { init: function () { this.jsonInit({ - message0: translate("get_lexical_variable"), + message0: translate('get_lexical_variable'), args0: [ { - type: "field_label", - name: "VAR", - text: "count", + type: 'field_label', + name: 'VAR', + text: 'count', }, ], output: null, - colour: categoryColours["Variables"], - tooltip: getTooltip("get_lexical_variable"), + colour: categoryColours['Variables'], + tooltip: getTooltip('get_lexical_variable'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("control_blocks"); + this.setStyle('control_blocks'); // Initialize with a null variable source ID. this.variableSourceId = null; }, updateVariable: function (newName) { - this.setFieldValue(String(newName), "VAR"); + this.setFieldValue(String(newName), 'VAR'); }, // Save the current variable name and source ID to the XML mutation. mutationToDom: function () { - const container = document.createElement("mutation"); - container.setAttribute("var", this.getFieldValue("VAR")); + const container = document.createElement('mutation'); + container.setAttribute('var', this.getFieldValue('VAR')); if (this.variableSourceId) { - container.setAttribute("sourceid", this.variableSourceId); + container.setAttribute('sourceid', this.variableSourceId); } return container; }, // Restore the variable name and source ID from the XML mutation. domToMutation: function (xmlElement) { - const variableName = xmlElement.getAttribute("var"); - this.setFieldValue(variableName, "VAR"); - const sourceId = xmlElement.getAttribute("sourceid"); + const variableName = xmlElement.getAttribute('var'); + this.setFieldValue(variableName, 'VAR'); + const sourceId = xmlElement.getAttribute('sourceid'); if (sourceId) { this.variableSourceId = sourceId; } }, }; - Blockly.Blocks["get_lexical_variable"].onchange = function (event) { + Blockly.Blocks['get_lexical_variable'].onchange = function (event) { // Only process if this is a move, create, or similar event that might affect scoping if ( event.type === Blockly.Events.BLOCK_MOVE || @@ -412,19 +400,19 @@ export function defineControlBlocks() { ) { if (!this.workspace) return; // Skip if no workspace - const variableName = this.getFieldValue("VAR"); + const variableName = this.getFieldValue('VAR'); let currentBlock = this; let found = false; // Traverse up the block hierarchy to find the closest for_loop with matching variable while ((currentBlock = currentBlock.getParent())) { - if (currentBlock.type === "for_loop") { - const loopVarName = currentBlock.getFieldValue("VAR"); - const field = currentBlock.getField("VAR"); + if (currentBlock.type === 'for_loop') { + const loopVarName = currentBlock.getFieldValue('VAR'); + const field = currentBlock.getField('VAR'); if (loopVarName === variableName && field) { // Found a matching for_loop parent - const variableId = field.variableId_ || ""; + const variableId = field.variableId_ || ''; // Update this getter's source ID this.variableSourceId = variableId; @@ -444,12 +432,12 @@ export function defineControlBlocks() { } }; - const MODE = { IF: "IF", ELSEIF: "ELSEIF", ELSE: "ELSE" }; - const INVALID_IF_STACK_REASON = "INVALID_IF_STACK"; + const MODE = { IF: 'IF', ELSEIF: 'ELSEIF', ELSE: 'ELSE' }; + const INVALID_IF_STACK_REASON = 'INVALID_IF_STACK'; - Blockly.Blocks["if_clause"] = { + Blockly.Blocks['if_clause'] = { init: function () { - this.setStyle("logic_blocks"); + this.setStyle('logic_blocks'); this.setPreviousStatement(true); this.setNextStatement(true); this.setInputsInline(true); @@ -459,29 +447,29 @@ export function defineControlBlocks() { // True while Blockly is constructing/rehydrating the block graph. this._isRestoring = true; - this.appendDummyInput("HEAD").appendField( + this.appendDummyInput('HEAD').appendField( new Blockly.FieldDropdown( [ - ["%{BKY_CONTROLS_IF_MSG_IF}", MODE.IF], - ["%{BKY_CONTROLS_IF_MSG_ELSEIF}", MODE.ELSEIF], - ["%{BKY_CONTROLS_IF_MSG_ELSE}", MODE.ELSE], + ['%{BKY_CONTROLS_IF_MSG_IF}', MODE.IF], + ['%{BKY_CONTROLS_IF_MSG_ELSEIF}', MODE.ELSEIF], + ['%{BKY_CONTROLS_IF_MSG_ELSE}', MODE.ELSE], ], (newValue) => { this.updateShape_(newValue); return newValue; - }, + } ), - "MODE", + 'MODE' ); - this.appendValueInput("COND").setCheck("Boolean"); - this.appendStatementInput("DO"); + this.appendValueInput('COND').setCheck('Boolean'); + this.appendStatementInput('DO'); - this.updateShape_(this.getFieldValue("MODE") ?? MODE.IF); + this.updateShape_(this.getFieldValue('MODE') ?? MODE.IF); // COND's context is the MODE dropdown (not a label field), so it can't be // auto-derived — give it an explicit ARIA label. - applyInputAriaLabels(this, { COND: "condition" }); + applyInputAriaLabels(this, { COND: 'condition' }); // Clear restore flag after Blockly has had a chance to reconnect blocks. setTimeout(() => { @@ -492,7 +480,7 @@ export function defineControlBlocks() { saveExtraState: function () { return { - mode: this.getFieldValue("MODE"), + mode: this.getFieldValue('MODE'), stashedCondState: this._stashedCondState ?? null, }; }, @@ -504,7 +492,7 @@ export function defineControlBlocks() { this._stashedCondState = state?.stashedCondState ?? null; const mode = state?.mode ?? MODE.IF; - this.setFieldValue(mode, "MODE"); + this.setFieldValue(mode, 'MODE'); // Only show/hide here; no dispose/restore while restoring. this.updateShape_(mode); @@ -518,7 +506,7 @@ export function defineControlBlocks() { updateShape_: function (mode) { const wantsCond = mode !== MODE.ELSE; - const condInput = this.getInput("COND"); + const condInput = this.getInput('COND'); if (!condInput) return; // During restore: never stash/dispose/append; only toggle visibility. @@ -560,7 +548,7 @@ export function defineControlBlocks() { const restored = Blockly.serialization.blocks.append( this._stashedCondState, this.workspace, - { recordUndo: false }, + { recordUndo: false } ); if (restored?.outputConnection && condInput.connection) { @@ -596,10 +584,7 @@ export function defineControlBlocks() { } // Ignore block-level disabled toggles to avoid event loops and undo noise. - if ( - event.type === Blockly.Events.BLOCK_CHANGE && - event.element === "disabled" - ) { + if (event.type === Blockly.Events.BLOCK_CHANGE && event.element === 'disabled') { return; } @@ -610,7 +595,7 @@ export function defineControlBlocks() { const maybeTarget = connection?.targetBlock?.(); if ( maybeTarget && - typeof maybeTarget.isInsertionMarker === "function" && + typeof maybeTarget.isInsertionMarker === 'function' && maybeTarget.isInsertionMarker() ) { return null; @@ -621,7 +606,7 @@ export function defineControlBlocks() { getIfClauseChainHead_: function () { let head = this; let prev = this.getRealTargetBlock_(head.previousConnection); - while (prev?.type === "if_clause") { + while (prev?.type === 'if_clause') { head = prev; prev = this.getRealTargetBlock_(head.previousConnection); } @@ -632,7 +617,7 @@ export function defineControlBlocks() { const chain = []; let current = head; - while (current?.type === "if_clause") { + while (current?.type === 'if_clause') { chain.push(current); current = this.getRealTargetBlock_(current.nextConnection); } @@ -655,7 +640,7 @@ export function defineControlBlocks() { try { for (const block of chain) { - const mode = block.getFieldValue("MODE"); + const mode = block.getFieldValue('MODE'); let isValid = false; if (mode === MODE.IF) { @@ -683,13 +668,13 @@ export function defineControlBlocks() { } const builtInControlBlocks = [ - "controls_if", - "controls_ifelse", - "controls_repeat_ext", - "controls_whileUntil", - "controls_for", - "controls_flow_statements", - "controls_forEach", + 'controls_if', + 'controls_ifelse', + 'controls_repeat_ext', + 'controls_whileUntil', + 'controls_for', + 'controls_flow_statements', + 'controls_forEach', ]; builtInControlBlocks.forEach((typeName) => { @@ -699,6 +684,6 @@ builtInControlBlocks.forEach((typeName) => { blockDef.init = function (...args) { originalInit.apply(this, args); - this.setStyle("control_blocks"); + this.setStyle('control_blocks'); }; }); diff --git a/blocks/effects.js b/blocks/effects.js index 3b19854c..2555e31a 100644 --- a/blocks/effects.js +++ b/blocks/effects.js @@ -1,114 +1,110 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { getHelpUrlFor } from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor } from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; export function defineEffectsBlocks() { - Blockly.Blocks["main_light"] = { + Blockly.Blocks['main_light'] = { init: function () { this.jsonInit({ - type: "main_light", - message0: translate("main_light"), + type: 'main_light', + message0: translate('main_light'), args0: [ { - type: "input_value", - name: "INTENSITY", - check: "Number", + type: 'input_value', + name: 'INTENSITY', + check: 'Number', }, { - type: "input_value", - name: "DIFFUSE", - check: "Colour", + type: 'input_value', + name: 'DIFFUSE', + check: 'Colour', }, { - type: "input_value", - name: "GROUND_COLOR", - check: "Colour", + type: 'input_value', + name: 'GROUND_COLOR', + check: 'Colour', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("main_light"), + colour: categoryColours['Scene'], + tooltip: getTooltip('main_light'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; - Blockly.Blocks["set_fog"] = { + Blockly.Blocks['set_fog'] = { init: function () { this.jsonInit({ - type: "set_fog", - message0: translate("set_fog"), + type: 'set_fog', + message0: translate('set_fog'), args0: [ { - type: "input_value", - name: "FOG_COLOR", - colour: "#ffffff", - check: "Colour", + type: 'input_value', + name: 'FOG_COLOR', + colour: '#ffffff', + check: 'Colour', }, { - type: "field_dropdown", - name: "FOG_MODE", + type: 'field_dropdown', + name: 'FOG_MODE', options: [ - getDropdownOption("LINEAR"), - getDropdownOption("NONE"), - getDropdownOption("EXP"), - getDropdownOption("EXP2"), + getDropdownOption('LINEAR'), + getDropdownOption('NONE'), + getDropdownOption('EXP'), + getDropdownOption('EXP2'), ], }, { - type: "input_value", - name: "DENSITY", - check: "Number", + type: 'input_value', + name: 'DENSITY', + check: 'Number', }, { - type: "input_value", - name: "START", - check: "Number", + type: 'input_value', + name: 'START', + check: 'Number', }, { - type: "input_value", - name: "END", - check: "Number", + type: 'input_value', + name: 'END', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("set_fog"), + colour: categoryColours['Scene'], + tooltip: getTooltip('set_fog'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; - Blockly.Blocks["get_light"] = { + Blockly.Blocks['get_light'] = { init: function () { this.jsonInit({ - type: "get_light", - message0: translate("get_light"), + type: 'get_light', + message0: translate('get_light'), args0: [ { - type: "field_variable", - name: "VAR", - variable: "light", // Default variable is 'camera' + type: 'field_variable', + name: 'VAR', + variable: 'light', // Default variable is 'camera' }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("get_light"), + colour: categoryColours['Scene'], + tooltip: getTooltip('get_light'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; } diff --git a/blocks/events.js b/blocks/events.js index 7984a5fe..e1a1f006 100644 --- a/blocks/events.js +++ b/blocks/events.js @@ -1,18 +1,13 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { getHelpUrlFor, addToggleButton, mutationToDom, domToMutation, updateShape, -} from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, - getOption, -} from "../main/translation.js"; +} from './blocks.js'; +import { translate, getTooltip, getDropdownOption, getOption } from '../main/translation.js'; import { makeStartIcon, makeRepeatIcon, @@ -26,70 +21,58 @@ import { BLOCK_ICON_FIELD_NAME, TOGGLE_BUTTON_FIELD_NAME, DecorativeFieldImage, -} from "./blockIcons.js"; +} from './blockIcons.js'; export function defineEventsBlocks() { - Blockly.Blocks["start"] = { + Blockly.Blocks['start'] = { init: function () { this.jsonInit({ - type: "start", - message0: translate("start"), - message1: "%1", + type: 'start', + message0: translate('start'), + message1: '%1', args1: [ { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', }, ], - colour: categoryColours["Events"], + colour: categoryColours['Events'], inputsInline: true, - tooltip: getTooltip("start"), + tooltip: getTooltip('start'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("events_blocks"); + this.setStyle('events_blocks'); this.inputList[0].insertFieldAt( 0, - new DecorativeFieldImage( - makeStartIcon(getCurrentIconColor()), - 18, - 18, - "", - null, - ), - BLOCK_ICON_FIELD_NAME, + new DecorativeFieldImage(makeStartIcon(getCurrentIconColor()), 18, 18, '', null), + BLOCK_ICON_FIELD_NAME ); }, }; // Define the forever block - Blockly.Blocks["forever"] = { + Blockly.Blocks['forever'] = { init: function () { this.jsonInit({ - type: "forever", - message0: translate("forever"), + type: 'forever', + message0: translate('forever'), args0: [ { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', check: null, }, ], - colour: categoryColours["Events"], - tooltip: getTooltip("forever"), + colour: categoryColours['Events'], + tooltip: getTooltip('forever'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("events_blocks"); + this.setStyle('events_blocks'); this.isInline = false; this.inputList[0].insertFieldAt( 0, - new DecorativeFieldImage( - makeRepeatIcon(getCurrentIconColor()), - 32, - 32, - "", - null, - ), - BLOCK_ICON_FIELD_NAME, + new DecorativeFieldImage(makeRepeatIcon(getCurrentIconColor()), 32, 32, '', null), + BLOCK_ICON_FIELD_NAME ); addToggleButton(this); }, @@ -112,37 +95,35 @@ export function defineEventsBlocks() { this.updateShape_(isInline); // Optional: Re-enable the block if it was disabled - if (this.hasDisabledReason && this.hasDisabledReason("ORPHANED_BLOCK")) { - this.setDisabledReason(false, "ORPHANED_BLOCK"); + if (this.hasDisabledReason && this.hasDisabledReason('ORPHANED_BLOCK')) { + this.setDisabledReason(false, 'ORPHANED_BLOCK'); } - Blockly.Events.fire( - new Blockly.Events.BlockChange(this, "mutation", null, "", ""), - ); + Blockly.Events.fire(new Blockly.Events.BlockChange(this, 'mutation', null, '', '')); Blockly.Events.fire(new Blockly.Events.BlockMove(this)); }, }; - Blockly.Blocks["when_clicked"] = { + Blockly.Blocks['when_clicked'] = { init: function () { this.jsonInit({ - type: "when_clicked", - message0: translate("when_clicked"), + type: 'when_clicked', + message0: translate('when_clicked'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "TRIGGER", + type: 'field_dropdown', + name: 'TRIGGER', options: [ - getDropdownOption("OnPickTrigger"), - getDropdownOption("OnLeftPickTrigger"), - getDropdownOption("OnDoublePickTrigger"), - getDropdownOption("OnPickDownTrigger"), - getDropdownOption("OnPickUpTrigger"), + getDropdownOption('OnPickTrigger'), + getDropdownOption('OnLeftPickTrigger'), + getDropdownOption('OnDoublePickTrigger'), + getDropdownOption('OnPickDownTrigger'), + getDropdownOption('OnPickUpTrigger'), ], }, /*{ @@ -155,44 +136,38 @@ export function defineEventsBlocks() { ], },*/ ], - message1: "%1", - implicitAlign1: "LEFT", + message1: '%1', + implicitAlign1: 'LEFT', args1: [ { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', }, ], - colour: categoryColours["Events"], - tooltip: getTooltip("when_clicked"), + colour: categoryColours['Events'], + tooltip: getTooltip('when_clicked'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("events_blocks"); + this.setStyle('events_blocks'); // Default to top-level mode this.isInline = false; this.setPreviousStatement(false); this.setNextStatement(false); this.inputList[0].insertFieldAt( 0, - new DecorativeFieldImage( - makeClickIcon(getCurrentIconColor()), - 22, - 22, - "", - null, - ), - BLOCK_ICON_FIELD_NAME, + new DecorativeFieldImage(makeClickIcon(getCurrentIconColor()), 22, 22, '', null), + BLOCK_ICON_FIELD_NAME ); // Add inline toggle button const toggleButton = new Blockly.FieldImage( - makeInlineIcon("white"), + makeInlineIcon('white'), 30, 30, - "toggle inline blocks", + 'toggle inline blocks', () => { this.toggleDoBlock(); - }, + } ); // Add toggle button to a separate input @@ -202,13 +177,13 @@ export function defineEventsBlocks() { }, mutationToDom: function () { - const container = document.createElement("mutation"); - container.setAttribute("inline", this.isInline); + const container = document.createElement('mutation'); + container.setAttribute('inline', this.isInline); return container; }, domToMutation: function (xmlElement) { - const isInline = xmlElement.getAttribute("inline") === "true" || false; + const isInline = xmlElement.getAttribute('inline') === 'true' || false; this.updateShape_(isInline); }, @@ -233,79 +208,71 @@ export function defineEventsBlocks() { this.updateShape_(isInline); - if (this.hasDisabledReason("ORPHANED_BLOCK")) { - this.setDisabledReason(false, "ORPHANED_BLOCK"); + if (this.hasDisabledReason('ORPHANED_BLOCK')) { + this.setDisabledReason(false, 'ORPHANED_BLOCK'); } - Blockly.Events.fire( - new Blockly.Events.BlockChange(this, "mutation", null, "", ""), - ); + Blockly.Events.fire(new Blockly.Events.BlockChange(this, 'mutation', null, '', '')); Blockly.Events.fire(new Blockly.Events.BlockMove(this)); }, }; - Blockly.Blocks["on_collision"] = { + Blockly.Blocks['on_collision'] = { init: function () { this.jsonInit({ - type: "when_touches", - message0: translate("on_collision"), + type: 'when_touches', + message0: translate('on_collision'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "TRIGGER", + type: 'field_dropdown', + name: 'TRIGGER', options: [ - getDropdownOption("OnIntersectionEnterTrigger"), - getDropdownOption("OnIntersectionExitTrigger"), + getDropdownOption('OnIntersectionEnterTrigger'), + getDropdownOption('OnIntersectionExitTrigger'), ], }, { - type: "field_variable", - name: "OTHER_MODEL_VAR", - variable: "object2", + type: 'field_variable', + name: 'OTHER_MODEL_VAR', + variable: 'object2', }, ], - message1: "%1", + message1: '%1', args1: [ { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', }, ], - colour: categoryColours["Events"], - tooltip: getTooltip("on_collision"), + colour: categoryColours['Events'], + tooltip: getTooltip('on_collision'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("events_blocks"); + this.setStyle('events_blocks'); // Set default state to top-level block this.isInline = false; this.callbackVar2Id = null; this.inputList[0].insertFieldAt( 0, - new DecorativeFieldImage( - makeCollisionIcon(getCurrentIconColor()), - 24, - 24, - "", - null, - ), - BLOCK_ICON_FIELD_NAME, + new DecorativeFieldImage(makeCollisionIcon(getCurrentIconColor()), 24, 24, '', null), + BLOCK_ICON_FIELD_NAME ); // Add the toggle button const toggleButton = new Blockly.FieldImage( - makeInlineIcon("white"), + makeInlineIcon('white'), 30, 30, - "toggle inline blocks", + 'toggle inline blocks', () => { this.toggleDoBlock(); - }, + } ); // Append the toggle button to the block @@ -316,8 +283,8 @@ export function defineEventsBlocks() { onchange: function () { if (!this.workspace || this.isInFlyout) return; - const modelVarId = this.getFieldValue("MODEL_VAR"); - const otherModelVarId = this.getFieldValue("OTHER_MODEL_VAR"); + const modelVarId = this.getFieldValue('MODEL_VAR'); + const otherModelVarId = this.getFieldValue('OTHER_MODEL_VAR'); if (!modelVarId || !otherModelVarId) return; @@ -338,17 +305,17 @@ export function defineEventsBlocks() { } }, mutationToDom: function () { - const container = document.createElement("mutation"); - container.setAttribute("inline", this.isInline); + const container = document.createElement('mutation'); + container.setAttribute('inline', this.isInline); if (this.callbackVar2Id) { - container.setAttribute("callback_var_2", this.callbackVar2Id); + container.setAttribute('callback_var_2', this.callbackVar2Id); } return container; }, domToMutation: function (xmlElement) { - const isInline = xmlElement.getAttribute("inline") === "true"; + const isInline = xmlElement.getAttribute('inline') === 'true'; this.updateShape_(isInline); - this.callbackVar2Id = xmlElement.getAttribute("callback_var_2") || null; + this.callbackVar2Id = xmlElement.getAttribute('callback_var_2') || null; }, updateShape_: function (isInline) { this.isInline = isInline; @@ -370,108 +337,97 @@ export function defineEventsBlocks() { this.updateShape_(isInline); // Optional: Re-enable the block if it was disabled - if (this.hasDisabledReason && this.hasDisabledReason("ORPHANED_BLOCK")) { - this.setDisabledReason(false, "ORPHANED_BLOCK"); + if (this.hasDisabledReason && this.hasDisabledReason('ORPHANED_BLOCK')) { + this.setDisabledReason(false, 'ORPHANED_BLOCK'); } - Blockly.Events.fire( - new Blockly.Events.BlockChange(this, "mutation", null, "", ""), - ); + Blockly.Events.fire(new Blockly.Events.BlockChange(this, 'mutation', null, '', '')); Blockly.Events.fire(new Blockly.Events.BlockMove(this)); }, }; // For backward compatibility - Blockly.Blocks["when_touches"] = Blockly.Blocks["on_collision"]; + Blockly.Blocks['when_touches'] = Blockly.Blocks['on_collision']; - Blockly.Blocks["when_key_event"] = { + Blockly.Blocks['when_key_event'] = { init: function () { this.jsonInit({ - type: "when_key_event", - message0: translate("when_key_event"), + type: 'when_key_event', + message0: translate('when_key_event'), args0: [ { - type: "field_grid_dropdown", - name: "KEY", + type: 'field_grid_dropdown', + name: 'KEY', columns: 10, options: [ - getDropdownOption("0"), - getDropdownOption("1"), - getDropdownOption("2"), - getDropdownOption("3"), - getDropdownOption("4"), - getDropdownOption("5"), - getDropdownOption("6"), - getDropdownOption("7"), - getDropdownOption("8"), - getDropdownOption("9"), - getDropdownOption("a"), - getDropdownOption("b"), - getDropdownOption("c"), - getDropdownOption("d"), - getDropdownOption("e"), - getDropdownOption("f"), - getDropdownOption("g"), - getDropdownOption("h"), - getDropdownOption("i"), - getDropdownOption("j"), - getDropdownOption("k"), - getDropdownOption("l"), - getDropdownOption("m"), - getDropdownOption("n"), - getDropdownOption("o"), - getDropdownOption("p"), - getDropdownOption("q"), - getDropdownOption("r"), - getDropdownOption("s"), - getDropdownOption("t"), - getDropdownOption("u"), - getDropdownOption("v"), - getDropdownOption("w"), - getDropdownOption("x"), - getDropdownOption("y"), - getDropdownOption("z"), - getDropdownOption(" "), - getDropdownOption(","), - getDropdownOption("."), - getDropdownOption("/"), - getDropdownOption("ArrowLeft"), - getDropdownOption("ArrowUp"), - getDropdownOption("ArrowRight"), - getDropdownOption("ArrowDown"), + getDropdownOption('0'), + getDropdownOption('1'), + getDropdownOption('2'), + getDropdownOption('3'), + getDropdownOption('4'), + getDropdownOption('5'), + getDropdownOption('6'), + getDropdownOption('7'), + getDropdownOption('8'), + getDropdownOption('9'), + getDropdownOption('a'), + getDropdownOption('b'), + getDropdownOption('c'), + getDropdownOption('d'), + getDropdownOption('e'), + getDropdownOption('f'), + getDropdownOption('g'), + getDropdownOption('h'), + getDropdownOption('i'), + getDropdownOption('j'), + getDropdownOption('k'), + getDropdownOption('l'), + getDropdownOption('m'), + getDropdownOption('n'), + getDropdownOption('o'), + getDropdownOption('p'), + getDropdownOption('q'), + getDropdownOption('r'), + getDropdownOption('s'), + getDropdownOption('t'), + getDropdownOption('u'), + getDropdownOption('v'), + getDropdownOption('w'), + getDropdownOption('x'), + getDropdownOption('y'), + getDropdownOption('z'), + getDropdownOption(' '), + getDropdownOption(','), + getDropdownOption('.'), + getDropdownOption('/'), + getDropdownOption('ArrowLeft'), + getDropdownOption('ArrowUp'), + getDropdownOption('ArrowRight'), + getDropdownOption('ArrowDown'), ], }, { - type: "field_dropdown", - name: "EVENT", - options: [ - getDropdownOption("pressed"), - getDropdownOption("released"), - ], + type: 'field_dropdown', + name: 'EVENT', + options: [getDropdownOption('pressed'), getDropdownOption('released')], }, ], - message1: "%1", + message1: '%1', args1: [ { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', }, ], - colour: categoryColours["Events"], - tooltip: getTooltip("when_key_event"), + colour: categoryColours['Events'], + tooltip: getTooltip('when_key_event'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("events_blocks"); + this.setStyle('events_blocks'); this.inputList[0].insertFieldAt( 0, - new DecorativeFieldImage( - makeKeyboardIcon(getCurrentIconColor()), - 36, - 36, - "", - null, - ), - BLOCK_ICON_FIELD_NAME, + new DecorativeFieldImage(makeKeyboardIcon(getCurrentIconColor()), 36, 36, '', null), + BLOCK_ICON_FIELD_NAME ); addToggleButton(this); }, @@ -494,68 +450,57 @@ export function defineEventsBlocks() { this.updateShape_(isInline); // Optional: Re-enable the block if it was disabled - if (this.hasDisabledReason && this.hasDisabledReason("ORPHANED_BLOCK")) { - this.setDisabledReason(false, "ORPHANED_BLOCK"); + if (this.hasDisabledReason && this.hasDisabledReason('ORPHANED_BLOCK')) { + this.setDisabledReason(false, 'ORPHANED_BLOCK'); } - Blockly.Events.fire( - new Blockly.Events.BlockChange(this, "mutation", null, "", ""), - ); + Blockly.Events.fire(new Blockly.Events.BlockChange(this, 'mutation', null, '', '')); Blockly.Events.fire(new Blockly.Events.BlockMove(this)); }, }; - Blockly.Blocks["when_action_event"] = { + Blockly.Blocks['when_action_event'] = { init: function () { this.jsonInit({ - type: "when_action_event", - message0: translate("when_action_event"), + type: 'when_action_event', + message0: translate('when_action_event'), args0: [ { - type: "field_dropdown", - name: "ACTION", + type: 'field_dropdown', + name: 'ACTION', options: [ - [getOption("ACTION_FORWARD"), "FORWARD"], - [getOption("ACTION_BACKWARD"), "BACKWARD"], - [getOption("ACTION_LEFT"), "LEFT"], - [getOption("ACTION_RIGHT"), "RIGHT"], - [getOption("ACTION_BUTTON1"), "BUTTON1"], - [getOption("ACTION_BUTTON2"), "BUTTON2"], - [getOption("ACTION_BUTTON3"), "BUTTON3"], - [getOption("ACTION_BUTTON4"), "BUTTON4"], + [getOption('ACTION_FORWARD'), 'FORWARD'], + [getOption('ACTION_BACKWARD'), 'BACKWARD'], + [getOption('ACTION_LEFT'), 'LEFT'], + [getOption('ACTION_RIGHT'), 'RIGHT'], + [getOption('ACTION_BUTTON1'), 'BUTTON1'], + [getOption('ACTION_BUTTON2'), 'BUTTON2'], + [getOption('ACTION_BUTTON3'), 'BUTTON3'], + [getOption('ACTION_BUTTON4'), 'BUTTON4'], ], }, { - type: "field_dropdown", - name: "EVENT", - options: [ - getDropdownOption("pressed"), - getDropdownOption("released"), - ], + type: 'field_dropdown', + name: 'EVENT', + options: [getDropdownOption('pressed'), getDropdownOption('released')], }, ], - message1: "%1", + message1: '%1', args1: [ { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', }, ], - colour: categoryColours["Events"], - tooltip: getTooltip("when_action_event"), + colour: categoryColours['Events'], + tooltip: getTooltip('when_action_event'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("events_blocks"); + this.setStyle('events_blocks'); this.inputList[0].insertFieldAt( 0, - new DecorativeFieldImage( - makePressIcon(getCurrentIconColor()), - 32, - 32, - "", - null, - ), - BLOCK_ICON_FIELD_NAME, + new DecorativeFieldImage(makePressIcon(getCurrentIconColor()), 32, 32, '', null), + BLOCK_ICON_FIELD_NAME ); addToggleButton(this); }, @@ -578,76 +523,67 @@ export function defineEventsBlocks() { this.updateShape_(isInline); // Optional: Re-enable the block if it was disabled - if (this.hasDisabledReason && this.hasDisabledReason("ORPHANED_BLOCK")) { - this.setDisabledReason(false, "ORPHANED_BLOCK"); + if (this.hasDisabledReason && this.hasDisabledReason('ORPHANED_BLOCK')) { + this.setDisabledReason(false, 'ORPHANED_BLOCK'); } - Blockly.Events.fire( - new Blockly.Events.BlockChange(this, "mutation", null, "", ""), - ); + Blockly.Events.fire(new Blockly.Events.BlockChange(this, 'mutation', null, '', '')); Blockly.Events.fire(new Blockly.Events.BlockMove(this)); }, }; function getEventNameValidationError(name) { - if (!name || typeof name !== "string") { - return "Event name must be a valid string."; + if (!name || typeof name !== 'string') { + return 'Event name must be a valid string.'; } if (name.length > 30) { - return "Event name must be 30 characters or fewer."; + return 'Event name must be 30 characters or fewer.'; } const lower = name.toLowerCase(); - const reservedPrefixes = [ - "_", - "on", - "system", - "internal", - "babylon", - "flock", - ]; + const reservedPrefixes = ['_', 'on', 'system', 'internal', 'babylon', 'flock']; if (reservedPrefixes.some((prefix) => lower.startsWith(prefix))) { return "Event name must not start with reserved words like 'on', 'system', or 'flock'."; } const disallowedChars = /[!@#$%^&*()+=[\]{};:'"\\|,<>?/\n\r\t]/; if (disallowedChars.test(name)) { - return "Event name must not include punctuation or special characters."; + return 'Event name must not include punctuation or special characters.'; } return null; // valid } - Blockly.Blocks["broadcast_event"] = { + Blockly.Blocks['broadcast_event'] = { init: function () { this.jsonInit({ - type: "broadcast_event", - message0: translate("broadcast_event"), + type: 'broadcast_event', + message0: translate('broadcast_event'), args0: [ { - type: "input_value", - name: "EVENT_NAME", - check: "String", + type: 'input_value', + name: 'EVENT_NAME', + check: 'String', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Events"], - tooltip: getTooltip("broadcast_event"), + colour: categoryColours['Events'], + tooltip: getTooltip('broadcast_event'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("events_blocks"); + this.setStyle('events_blocks'); }, /** Called whenever anything changes */ onchange: function () { if (!this.workspace || this.isInFlyout) return; - const inputBlock = this.getInputTargetBlock("EVENT_NAME"); + const inputBlock = this.getInputTargetBlock('EVENT_NAME'); - if (inputBlock && inputBlock.type === "text") { - const value = inputBlock.getFieldValue("TEXT"); + if (inputBlock && inputBlock.type === 'text') { + const value = inputBlock.getFieldValue('TEXT'); const error = getEventNameValidationError(value); if (error) { @@ -661,40 +597,34 @@ export function defineEventsBlocks() { }, }; - Blockly.Blocks["on_event"] = { + Blockly.Blocks['on_event'] = { init: function () { this.jsonInit({ - type: "on_event", - message0: translate("on_event"), + type: 'on_event', + message0: translate('on_event'), args0: [ { - type: "input_value", - name: "EVENT_NAME", - check: "String", + type: 'input_value', + name: 'EVENT_NAME', + check: 'String', }, ], - message1: "%1", + message1: '%1', args1: [ { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', }, ], - colour: categoryColours["Events"], - tooltip: getTooltip("on_event"), + colour: categoryColours['Events'], + tooltip: getTooltip('on_event'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("events_blocks"); + this.setStyle('events_blocks'); this.inputList[0].insertFieldAt( 0, - new DecorativeFieldImage( - makeOnEventIcon(getCurrentIconColor()), - 28, - 28, - "", - null, - ), - BLOCK_ICON_FIELD_NAME, + new DecorativeFieldImage(makeOnEventIcon(getCurrentIconColor()), 28, 28, '', null), + BLOCK_ICON_FIELD_NAME ); addToggleButton(this); }, @@ -717,22 +647,20 @@ export function defineEventsBlocks() { this.updateShape_(isInline); // Optional: Re-enable the block if it was disabled - if (this.hasDisabledReason && this.hasDisabledReason("ORPHANED_BLOCK")) { - this.setDisabledReason(false, "ORPHANED_BLOCK"); + if (this.hasDisabledReason && this.hasDisabledReason('ORPHANED_BLOCK')) { + this.setDisabledReason(false, 'ORPHANED_BLOCK'); } - Blockly.Events.fire( - new Blockly.Events.BlockChange(this, "mutation", null, "", ""), - ); + Blockly.Events.fire(new Blockly.Events.BlockChange(this, 'mutation', null, '', '')); Blockly.Events.fire(new Blockly.Events.BlockMove(this)); }, onchange: function () { if (!this.workspace || this.isInFlyout) return; - const inputBlock = this.getInputTargetBlock("EVENT_NAME"); + const inputBlock = this.getInputTargetBlock('EVENT_NAME'); - if (inputBlock && inputBlock.type === "text") { - const value = inputBlock.getFieldValue("TEXT"); + if (inputBlock && inputBlock.type === 'text') { + const value = inputBlock.getFieldValue('TEXT'); const error = getEventNameValidationError(value); if (error) { diff --git a/blocks/materials.js b/blocks/materials.js index 955ed02e..61ca9baf 100644 --- a/blocks/materials.js +++ b/blocks/materials.js @@ -1,9 +1,9 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { getHelpUrlFor } from "./blocks.js"; -import { materialNames } from "../config.js"; -import { flock } from "../flock.js"; -import { translate, getTooltip } from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor } from './blocks.js'; +import { materialNames } from '../config.js'; +import { flock } from '../flock.js'; +import { translate, getTooltip } from '../main/translation.js'; import { makeTouchesInputSubtree, wasBlockDeleted, @@ -12,48 +12,48 @@ import { promoteMaterialContainerFromShadow, respawnMaterialShadow, cacheMaterialState, -} from "./scene.js"; +} from './scene.js'; export function defineMaterialsBlocks() { - Blockly.Blocks["change_color"] = { + Blockly.Blocks['change_color'] = { init: function () { this.jsonInit({ - type: "change_color", - message0: translate("change_color"), + type: 'change_color', + message0: translate('change_color'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "COLOR", - check: ["Colour", "Array"], // Accepts either Colour or Array + type: 'input_value', + name: 'COLOR', + check: ['Colour', 'Array'], // Accepts either Colour or Array }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("change_color"), + colour: categoryColours['Materials'], + tooltip: getTooltip('change_color'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["change_material"] = { + Blockly.Blocks['change_material'] = { init: function () { this.jsonInit({ - message0: translate("change_material"), + message0: translate('change_material'), args0: [ { - type: "field_grid_dropdown", - name: "MATERIALS", + type: 'field_grid_dropdown', + name: 'MATERIALS', columns: 4, options: materialNames.map((name) => { - const baseName = name.replace(/\.[^/.]+$/, ""); + const baseName = name.replace(/\.[^/.]+$/, ''); return [ { src: `./textures/${baseName}.png`, @@ -66,532 +66,530 @@ export function defineMaterialsBlocks() { }), }, { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "COLOR", - check: ["Colour", "Array"], // Accepts either Colour or Array + type: 'input_value', + name: 'COLOR', + check: ['Colour', 'Array'], // Accepts either Colour or Array }, ], inputsInline: true, - colour: categoryColours["Materials"], - tooltip: getTooltip("change_material"), + colour: categoryColours['Materials'], + tooltip: getTooltip('change_material'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["text_material"] = { + Blockly.Blocks['text_material'] = { init: function () { this.jsonInit({ - type: "text_material", - message0: translate("text_material"), + type: 'text_material', + message0: translate('text_material'), args0: [ { - type: "field_variable", - name: "MATERIAL_VAR", - variable: "material", + type: 'field_variable', + name: 'MATERIAL_VAR', + variable: 'material', }, { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, { - type: "input_value", - name: "COLOR", - check: "Colour", + type: 'input_value', + name: 'COLOR', + check: 'Colour', }, { - type: "input_value", - name: "BACKGROUND_COLOR", - check: "Colour", + type: 'input_value', + name: 'BACKGROUND_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "WIDTH", - check: "Number", + type: 'input_value', + name: 'WIDTH', + check: 'Number', }, { - type: "input_value", - name: "HEIGHT", - check: "Number", + type: 'input_value', + name: 'HEIGHT', + check: 'Number', }, { - type: "input_value", - name: "TEXT_SIZE", - check: "Number", + type: 'input_value', + name: 'TEXT_SIZE', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("text_material"), + colour: categoryColours['Materials'], + tooltip: getTooltip('text_material'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["place_decal"] = { + Blockly.Blocks['place_decal'] = { init: function () { this.jsonInit({ - message0: translate("place_decal"), + message0: translate('place_decal'), args0: [ { - type: "field_variable", - name: "MATERIAL", - variable: "material", + type: 'field_variable', + name: 'MATERIAL', + variable: 'material', }, { - type: "input_value", - name: "ANGLE", - check: "Number", + type: 'input_value', + name: 'ANGLE', + check: 'Number', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("place_decal"), + colour: categoryColours['Materials'], + tooltip: getTooltip('place_decal'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["decal"] = { + Blockly.Blocks['decal'] = { init: function () { this.jsonInit({ - type: "decal", - message0: translate("decal"), + type: 'decal', + message0: translate('decal'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, }, { - type: "input_value", - name: "POSITION_X", - check: "Number", + type: 'input_value', + name: 'POSITION_X', + check: 'Number', }, { - type: "input_value", - name: "POSITION_Y", - check: "Number", + type: 'input_value', + name: 'POSITION_Y', + check: 'Number', }, { - type: "input_value", - name: "POSITION_Z", - check: "Number", + type: 'input_value', + name: 'POSITION_Z', + check: 'Number', }, { - type: "input_value", - name: "NORMAL_X", - check: "Number", + type: 'input_value', + name: 'NORMAL_X', + check: 'Number', }, { - type: "input_value", - name: "NORMAL_Y", - check: "Number", + type: 'input_value', + name: 'NORMAL_Y', + check: 'Number', }, { - type: "input_value", - name: "NORMAL_Z", - check: "Number", + type: 'input_value', + name: 'NORMAL_Z', + check: 'Number', }, { - type: "input_value", - name: "SIZE_X", - check: "Number", + type: 'input_value', + name: 'SIZE_X', + check: 'Number', }, { - type: "input_value", - name: "SIZE_Y", - check: "Number", + type: 'input_value', + name: 'SIZE_Y', + check: 'Number', }, { - type: "input_value", - name: "SIZE_Z", - check: "Number", + type: 'input_value', + name: 'SIZE_Z', + check: 'Number', }, { - type: "input_value", - name: "MATERIAL", + type: 'input_value', + name: 'MATERIAL', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("decal"), + colour: categoryColours['Materials'], + tooltip: getTooltip('decal'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["highlight"] = { + Blockly.Blocks['highlight'] = { init: function () { this.jsonInit({ - type: "highlight", - message0: translate("highlight"), + type: 'highlight', + message0: translate('highlight'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "COLOR", - colour: "#FFD700", - check: "Colour", + type: 'input_value', + name: 'COLOR', + colour: '#FFD700', + check: 'Colour', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("highlight"), + colour: categoryColours['Materials'], + tooltip: getTooltip('highlight'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["glow"] = { + Blockly.Blocks['glow'] = { init: function () { this.jsonInit({ - type: "glow", - message0: translate("glow"), + type: 'glow', + message0: translate('glow'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("glow"), + colour: categoryColours['Materials'], + tooltip: getTooltip('glow'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["tint"] = { + Blockly.Blocks['tint'] = { init: function () { this.jsonInit({ - type: "tint", - message0: translate("tint"), + type: 'tint', + message0: translate('tint'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "COLOR", - colour: "#AA336A", - check: "Colour", + type: 'input_value', + name: 'COLOR', + colour: '#AA336A', + check: 'Colour', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("tint"), + colour: categoryColours['Materials'], + tooltip: getTooltip('tint'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["set_alpha"] = { + Blockly.Blocks['set_alpha'] = { init: function () { this.jsonInit({ - type: "set_mesh_material_alpha", - message0: translate("set_alpha"), + type: 'set_mesh_material_alpha', + message0: translate('set_alpha'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, }, { - type: "input_value", - name: "ALPHA", - check: "Number", + type: 'input_value', + name: 'ALPHA', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("set_alpha"), + colour: categoryColours['Materials'], + tooltip: getTooltip('set_alpha'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["clear_effects"] = { + Blockly.Blocks['clear_effects'] = { init: function () { this.jsonInit({ - type: "clear_effects", - message0: translate("clear_effects"), + type: 'clear_effects', + message0: translate('clear_effects'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("clear_effects"), + colour: categoryColours['Materials'], + tooltip: getTooltip('clear_effects'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["colour"] = { + Blockly.Blocks['colour'] = { init: function () { this.jsonInit({ - type: "colour", - message0: translate("colour"), + type: 'colour', + message0: translate('colour'), args0: [ { - type: "field_colour", - name: "COLOR", - colour: "#9932CC", + type: 'field_colour', + name: 'COLOR', + colour: '#9932CC', }, ], - output: "Colour", - colour: categoryColours["Materials"], - tooltip: getTooltip("colour"), + output: 'Colour', + colour: categoryColours['Materials'], + tooltip: getTooltip('colour'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; // put this near your blocks file top const SKIN_HEXES = [ - "#2E1A1B", - "#37221D", - "#442B22", - "#6E473C", - "#926451", - "#966456", - "#D8A190", - "#E1BCB0", - "#EACDC7", - "#290A0E", - "#34130A", - "#602C14", - "#884B2C", - "#C27753", - "#E89878", - "#EEA98A", - "#FECCC0", - "#FCDACE", - "#2F2018", - "#41291C", - "#5A3C26", - "#845B40", - "#B98459", - "#D39E74", - "#E4BB9D", - "#EFCFB1", - "#ECD8C6", - "#231304", - "#3E1C0A", - "#572E15", - "#AC6534", - "#D8945D", - "#F4B077", - "#FCC292", - "#FFD2AA", - "#FEE0C7", - "#2D2113", - "#4F351F", - "#694831", - "#8F633E", - "#AD7D43", - "#C59B69", - "#DAB485", - "#E6CEAA", - "#E9D4B8", - "#251601", - "#3F1F03", - "#5D340F", - "#8D521E", - "#D2894A", - "#F2AF6C", - "#F1C387", - "#F4D0A4", - "#F5DDBE", - "#302418", - "#3C2D1B", - "#563E28", - "#7C6041", - "#957345", - "#9F7D4B", - "#CCB081", - "#DFCCA5", - "#E7DBC5", - "#271D07", - "#442E0F", - "#64421B", - "#976E2F", - "#C69045", - "#D9A756", - "#EBC57E", - "#F4DBA6", - "#FBE6C1", + '#2E1A1B', + '#37221D', + '#442B22', + '#6E473C', + '#926451', + '#966456', + '#D8A190', + '#E1BCB0', + '#EACDC7', + '#290A0E', + '#34130A', + '#602C14', + '#884B2C', + '#C27753', + '#E89878', + '#EEA98A', + '#FECCC0', + '#FCDACE', + '#2F2018', + '#41291C', + '#5A3C26', + '#845B40', + '#B98459', + '#D39E74', + '#E4BB9D', + '#EFCFB1', + '#ECD8C6', + '#231304', + '#3E1C0A', + '#572E15', + '#AC6534', + '#D8945D', + '#F4B077', + '#FCC292', + '#FFD2AA', + '#FEE0C7', + '#2D2113', + '#4F351F', + '#694831', + '#8F633E', + '#AD7D43', + '#C59B69', + '#DAB485', + '#E6CEAA', + '#E9D4B8', + '#251601', + '#3F1F03', + '#5D340F', + '#8D521E', + '#D2894A', + '#F2AF6C', + '#F1C387', + '#F4D0A4', + '#F5DDBE', + '#302418', + '#3C2D1B', + '#563E28', + '#7C6041', + '#957345', + '#9F7D4B', + '#CCB081', + '#DFCCA5', + '#E7DBC5', + '#271D07', + '#442E0F', + '#64421B', + '#976E2F', + '#C69045', + '#D9A756', + '#EBC57E', + '#F4DBA6', + '#FBE6C1', ]; // Use the hex strings themselves as the hover titles const SKIN_TITLES = SKIN_HEXES.map((h) => h); - Blockly.Blocks["skin_colour"] = { + Blockly.Blocks['skin_colour'] = { init: function () { this.jsonInit({ - type: "skin_colour", - message0: translate("skin_colour"), + type: 'skin_colour', + message0: translate('skin_colour'), args0: [ { - type: "field_colour", - name: "COLOR", - colour: "#FFE0BD", + type: 'field_colour', + name: 'COLOR', + colour: '#FFE0BD', colourOptions: SKIN_HEXES, colourTitles: SKIN_TITLES, columns: 9, }, ], - output: "Colour", - colour: categoryColours["Materials"], - tooltip: getTooltip("skin_colour"), + output: 'Colour', + colour: categoryColours['Materials'], + tooltip: getTooltip('skin_colour'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["greyscale_colour"] = { + Blockly.Blocks['greyscale_colour'] = { init: function () { this.jsonInit({ - type: "greyscale_colour", - message0: translate("greyscale_colour"), + type: 'greyscale_colour', + message0: translate('greyscale_colour'), args0: [ { - type: "field_colour", - name: "COLOR", - colour: "#808080", // A neutral starting color (grey) + type: 'field_colour', + name: 'COLOR', + colour: '#808080', // A neutral starting color (grey) colourOptions: [ - "#000000", // Black - "#1a1a1a", - "#333333", - "#4d4d4d", - "#666666", - "#808080", // Middle grey - "#999999", - "#b3b3b3", - "#cccccc", - "#e6e6e6", - "#ffffff", // White + '#000000', // Black + '#1a1a1a', + '#333333', + '#4d4d4d', + '#666666', + '#808080', // Middle grey + '#999999', + '#b3b3b3', + '#cccccc', + '#e6e6e6', + '#ffffff', // White ], colourTitles: [ - "Black", - "Dark Grey 1", - "Dark Grey 2", - "Dark Grey 3", - "Grey 1", - "Middle Grey", - "Grey 2", - "Light Grey 1", - "Light Grey 2", - "Light Grey 3", - "White", + 'Black', + 'Dark Grey 1', + 'Dark Grey 2', + 'Dark Grey 3', + 'Grey 1', + 'Middle Grey', + 'Grey 2', + 'Light Grey 1', + 'Light Grey 2', + 'Light Grey 3', + 'White', ], columns: 4, }, ], - output: "Colour", - colour: categoryColours["Materials"], // You can set this to any colour category you prefer - tooltip: getTooltip("greyscale_colour"), + output: 'Colour', + colour: categoryColours['Materials'], // You can set this to any colour category you prefer + tooltip: getTooltip('greyscale_colour'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["colour_from_string"] = { + Blockly.Blocks['colour_from_string'] = { init: function () { this.jsonInit({ - message0: "%1 %2", + message0: '%1 %2', args0: [ { - type: "field_label_serializable", - name: "HASH_PREFIX", - text: "#", + type: 'field_label_serializable', + name: 'HASH_PREFIX', + text: '#', }, { - type: "field_input", - name: "COLOR", - text: "800080", + type: 'field_input', + name: 'COLOR', + text: '800080', }, ], - output: "Colour", + output: 'Colour', }); - const colorField = this.getField("COLOR"); - const hashPrefixField = this.getField("HASH_PREFIX"); + const colorField = this.getField('COLOR'); + const hashPrefixField = this.getField('HASH_PREFIX'); const updateHashPrefixContrast = (hexColor) => { if (!hashPrefixField?.getSvgRoot) return; const svgRoot = hashPrefixField.getSvgRoot(); if (!svgRoot?.style) return; - const rgb = flock.hexToRgb(hexColor || "#000000"); + const rgb = flock.hexToRgb(hexColor || '#000000'); const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255; - const textColor = luminance > 0.6 ? "#000000" : "#ffffff"; - const textEl = svgRoot.querySelector("text"); + const textColor = luminance > 0.6 ? '#000000' : '#ffffff'; + const textEl = svgRoot.querySelector('text'); if (textEl) { - textEl.setAttribute("fill", textColor); - textEl.style.setProperty("fill", textColor, "important"); + textEl.setAttribute('fill', textColor); + textEl.style.setProperty('fill', textColor, 'important'); } else { - svgRoot.style.setProperty("fill", textColor, "important"); + svgRoot.style.setProperty('fill', textColor, 'important'); } }; colorField.setValidator(function (newVal) { - const normalizedInput = - typeof newVal === "string" ? newVal.trim().replace(/^#/, "") : ""; + const normalizedInput = typeof newVal === 'string' ? newVal.trim().replace(/^#/, '') : ''; try { - const validatedVal = - flock.getColorFromString(normalizedInput) || "#000000"; + const validatedVal = flock.getColorFromString(normalizedInput) || '#000000'; this.sourceBlock_.setColour(validatedVal); updateHashPrefixContrast(validatedVal); return normalizedInput; } catch (error) { - console.warn("Failed to validate colour field value:", error); - this.sourceBlock_.setColour("#000000"); - updateHashPrefixContrast("#000000"); + console.warn('Failed to validate colour field value:', error); + this.sourceBlock_.setColour('#000000'); + updateHashPrefixContrast('#000000'); return normalizedInput; } }); @@ -599,44 +597,44 @@ export function defineMaterialsBlocks() { // Set initial block color based on the field's initial value try { const initialVal = colorField.getValue(); - const validatedVal = flock.getColorFromString(initialVal) || "#000000"; + const validatedVal = flock.getColorFromString(initialVal) || '#000000'; this.setColour(validatedVal); updateHashPrefixContrast(validatedVal); } catch (error) { - console.warn("Failed to initialize colour block value:", error); - this.setColour("#000000"); - updateHashPrefixContrast("#000000"); + console.warn('Failed to initialize colour block value:', error); + this.setColour('#000000'); + updateHashPrefixContrast('#000000'); } }, }; - Blockly.Blocks["random_colour"] = { + Blockly.Blocks['random_colour'] = { init: function () { this.jsonInit({ - type: "random_colour_block", - message0: translate("random_colour"), - output: "Colour", - colour: categoryColours["Materials"], - tooltip: getTooltip("random_colour"), + type: 'random_colour_block', + message0: translate('random_colour'), + output: 'Colour', + colour: categoryColours['Materials'], + tooltip: getTooltip('random_colour'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["material"] = { + Blockly.Blocks['material'] = { init: function () { this.jsonInit({ - type: "material", - message0: translate("material"), + type: 'material', + message0: translate('material'), args0: [ { - type: "field_grid_dropdown", - name: "TEXTURE_SET", + type: 'field_grid_dropdown', + name: 'TEXTURE_SET', columns: 4, options: materialNames.map((name) => { - const baseName = name.replace(/\.[^/.]+$/, ""); + const baseName = name.replace(/\.[^/.]+$/, ''); return [ { src: `./textures/${baseName}.png`, @@ -649,63 +647,63 @@ export function defineMaterialsBlocks() { }), }, { - type: "input_value", - name: "BASE_COLOR", - colour: "#ffffff", // Default to white + type: 'input_value', + name: 'BASE_COLOR', + colour: '#ffffff', // Default to white }, { - type: "input_value", - name: "ALPHA", + type: 'input_value', + name: 'ALPHA', value: 1, min: 0, max: 1, precision: 0.01, }, ], - output: "Material", + output: 'Material', inputsInline: true, - colour: categoryColours["Materials"], - tooltip: getTooltip("material"), + colour: categoryColours['Materials'], + tooltip: getTooltip('material'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; - Blockly.Blocks["gradient_material"] = { + Blockly.Blocks['gradient_material'] = { init: function () { this.jsonInit({ - type: "material", - message0: translate("gradient_material"), + type: 'material', + message0: translate('gradient_material'), args0: [ { - type: "input_value", - name: "COLOR", - colour: "#6495ED", - check: ["Colour", "Array"], + type: 'input_value', + name: 'COLOR', + colour: '#6495ED', + check: ['Colour', 'Array'], }, { - type: "input_value", - name: "ALPHA", + type: 'input_value', + name: 'ALPHA', value: 1, min: 0, max: 1, precision: 0.01, }, ], - output: "Material", + output: 'Material', inputsInline: true, - colour: categoryColours["Materials"], - tooltip: getTooltip("gradient_material"), + colour: categoryColours['Materials'], + tooltip: getTooltip('gradient_material'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); }, }; function attachSetMaterialOnChange(block) { const ws = block.workspace; - const touches = makeTouchesInputSubtree(block, ws, "MATERIAL"); + const touches = makeTouchesInputSubtree(block, ws, 'MATERIAL'); block.setOnChange((changeEvent) => { const eventTypes = [ @@ -720,11 +718,10 @@ export function defineMaterialsBlocks() { const relevant = wasBlockDeleted(changeEvent, block.id) || changeEventHitsTouches(changeEvent, touches) || - (changeEvent.type === Blockly.Events.BLOCK_CREATE && - changeEvent.blockId === block.id) || + (changeEvent.type === Blockly.Events.BLOCK_CREATE && changeEvent.blockId === block.id) || (changeEvent.type === Blockly.Events.BLOCK_MOVE && changeEvent.oldParentId === block.id && - changeEvent.oldInputName === "MATERIAL"); + changeEvent.oldInputName === 'MATERIAL'); if (!relevant) return; @@ -732,7 +729,7 @@ export function defineMaterialsBlocks() { promoteMaterialContainerFromShadow(block); - if (!block.getInputTargetBlock("MATERIAL")) { + if (!block.getInputTargetBlock('MATERIAL')) { respawnMaterialShadow(block); return; } @@ -741,32 +738,32 @@ export function defineMaterialsBlocks() { }); } - Blockly.Blocks["set_material"] = { + Blockly.Blocks['set_material'] = { init: function () { this.jsonInit({ - type: "set_material", - message0: translate("set_material"), + type: 'set_material', + message0: translate('set_material'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, }, { - type: "input_value", - name: "MATERIAL", - check: ["Material", "Array"], + type: 'input_value', + name: 'MATERIAL', + check: ['Material', 'Array'], }, ], previousStatement: null, nextStatement: null, inputsInline: true, - colour: categoryColours["Materials"], - tooltip: getTooltip("set_material"), + colour: categoryColours['Materials'], + tooltip: getTooltip('set_material'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("materials_blocks"); + this.setStyle('materials_blocks'); attachSetMaterialOnChange(this); }, }; diff --git a/blocks/models.js b/blocks/models.js index ea0fb1c5..ce7f8385 100644 --- a/blocks/models.js +++ b/blocks/models.js @@ -1,5 +1,5 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { nextVariableIndexes, handleBlockChange, @@ -10,7 +10,7 @@ import { handleParentLinkedUpdate, getHelpUrlFor, registerBlockHandler, -} from "./blocks.js"; +} from './blocks.js'; import { characterNames, objectNames, @@ -18,30 +18,29 @@ import { objectColours, modelNames, getModelDisplayName, -} from "../config.js"; -import { flock } from "../flock.js"; -import { translate, getTooltip } from "../main/translation.js"; +} from '../config.js'; +import { flock } from '../flock.js'; +import { translate, getTooltip } from '../main/translation.js'; export function defineModelBlocks() { - Blockly.Blocks["load_character"] = { + Blockly.Blocks['load_character'] = { init: function () { - const variableNamePrefix = "character"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'character'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - message0: translate("load_character"), + message0: translate('load_character'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "field_grid_dropdown", - name: "MODELS", + type: 'field_grid_dropdown', + name: 'MODELS', columns: 6, options: characterNames.map((name) => { - const baseName = name.replace(/\.[^/.]+$/, ""); + const baseName = name.replace(/\.[^/.]+$/, ''); return [ { src: `${flock.imagePath}${baseName}.png`, @@ -54,73 +53,68 @@ export function defineModelBlocks() { }), }, { - type: "input_value", - name: "SCALE", - check: "Number", + type: 'input_value', + name: 'SCALE', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, { - type: "input_value", - name: "HAIR_COLOR", - check: "Colour", + type: 'input_value', + name: 'HAIR_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "SKIN_COLOR", - check: "Colour", + type: 'input_value', + name: 'SKIN_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "EYES_COLOR", - check: "Colour", + type: 'input_value', + name: 'EYES_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "TSHIRT_COLOR", - check: "Colour", + type: 'input_value', + name: 'TSHIRT_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "SHORTS_COLOR", - check: "Colour", + type: 'input_value', + name: 'SHORTS_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "SLEEVES_COLOR", - check: "Colour", + type: 'input_value', + name: 'SLEEVES_COLOR', + check: 'Colour', }, ], inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("load_character"), + colour: categoryColours['Scene'], + tooltip: getTooltip('load_character'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); registerBlockHandler(this, (changeEvent) => { // Always handle variable naming first (even if mesh is skipped) - handleBlockCreateEvent( - this, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ); + handleBlockCreateEvent(this, changeEvent, variableNamePrefix, nextVariableIndexes); // Mesh lifecycle events on this block directly (e.g. enable/disable, move), // or when this block is part of a snippet creation (its id is in changeEvent.ids). @@ -135,7 +129,7 @@ export function defineModelBlocks() { // Linked children like MODELS or color inputs if (handleParentLinkedUpdate(this, changeEvent)) { // 🔹 Additional side-effect unique to this block type - window.updateCurrentMeshName(this, "ID_VAR"); + window.updateCurrentMeshName(this, 'ID_VAR'); return; } @@ -148,32 +142,31 @@ export function defineModelBlocks() { }, }; - Blockly.Blocks["load_object"] = { + Blockly.Blocks['load_object'] = { init: function () { - const defaultObject = "Star.glb"; + const defaultObject = 'Star.glb'; const defaultColours = objectColours[defaultObject]; const defaultColour = Array.isArray(defaultColours) ? defaultColours[0] - : defaultColours || "#FFD700"; - const variableNamePrefix = "item"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + : defaultColours || '#FFD700'; + const variableNamePrefix = 'item'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; // Add the main inputs of the block this.jsonInit({ - message0: translate("load_object"), + message0: translate('load_object'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "field_grid_dropdown", - name: "MODELS", + type: 'field_grid_dropdown', + name: 'MODELS', columns: 6, options: objectNames.map((name) => { - const baseName = name.replace(/\.[^/.]+$/, ""); + const baseName = name.replace(/\.[^/.]+$/, ''); return [ { src: `${flock.imagePath}${baseName}.png`, @@ -186,51 +179,51 @@ export function defineModelBlocks() { }), }, { - type: "input_value", - name: "COLOR", - check: ["Colour", "Array", "Material"], + type: 'input_value', + name: 'COLOR', + check: ['Colour', 'Array', 'Material'], }, { - type: "input_value", - name: "SCALE", - check: "Number", + type: 'input_value', + name: 'SCALE', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("load_object"), + colour: categoryColours['Scene'], + tooltip: getTooltip('load_object'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); // Function to update the COLOR field based on the selected model const updateColorField = () => { - const selectedObject = this.getFieldValue("MODELS"); + const selectedObject = this.getFieldValue('MODELS'); const configColors = objectColours[selectedObject]; const colour = Array.isArray(configColors) ? configColors[0] : configColors || defaultColour; - const colorInput = this.getInput("COLOR"); + const colorInput = this.getInput('COLOR'); const colorField = colorInput.connection.targetBlock(); - if (colorField?.getField?.("COLOR")) { - colorField.setFieldValue(colour, "COLOR"); + if (colorField?.getField?.('COLOR')) { + colorField.setFieldValue(colour, 'COLOR'); } }; @@ -239,8 +232,8 @@ export function defineModelBlocks() { registerBlockHandler(this, (changeEvent) => { if ( changeEvent.type === Blockly.Events.BLOCK_CHANGE && - changeEvent.element === "field" && - changeEvent.name === "MODELS" && + changeEvent.element === 'field' && + changeEvent.name === 'MODELS' && changeEvent.blockId === this.id ) { updateColorField(); @@ -248,10 +241,7 @@ export function defineModelBlocks() { handleBlockChange(this, changeEvent, variableNamePrefix); - if ( - this.id !== changeEvent.blockId && - changeEvent.type !== Blockly.Events.BLOCK_CHANGE - ) + if (this.id !== changeEvent.blockId && changeEvent.type !== Blockly.Events.BLOCK_CHANGE) return; if (handleMeshLifecycleChange(this, changeEvent)) return; // if (handleFieldOrChildChange(this, changeEvent)) return; @@ -261,26 +251,25 @@ export function defineModelBlocks() { }, }; - Blockly.Blocks["load_multi_object"] = { + Blockly.Blocks['load_multi_object'] = { init: function () { - const variableNamePrefix = "object"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'object'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - message0: translate("load_multi_object"), + message0: translate('load_multi_object'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "field_grid_dropdown", - name: "MODELS", + type: 'field_grid_dropdown', + name: 'MODELS', columns: 6, options: multiObjectNames.map((name) => { - const baseName = name.replace(/\.[^/.]+$/, ""); + const baseName = name.replace(/\.[^/.]+$/, ''); return [ { src: `${flock.imagePath}${baseName}.png`, @@ -293,63 +282,59 @@ export function defineModelBlocks() { }), }, { - type: "input_value", - name: "SCALE", - check: "Number", + type: 'input_value', + name: 'SCALE', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, { - type: "input_value", - name: "COLORS", - check: "Array", + type: 'input_value', + name: 'COLORS', + check: 'Array', }, ], inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("load_multi_object"), + colour: categoryColours['Scene'], + tooltip: getTooltip('load_multi_object'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); // Change from a local constant to a method on the block prototype - Blockly.Blocks["load_multi_object"].updateColorsField = function () { - const selectedObject = this.getFieldValue("MODELS"); - const colours = objectColours[selectedObject] || [ - "#000000", - "#FFFFFF", - "#CCCCCC", - ]; + Blockly.Blocks['load_multi_object'].updateColorsField = function () { + const selectedObject = this.getFieldValue('MODELS'); + const colours = objectColours[selectedObject] || ['#000000', '#FFFFFF', '#CCCCCC']; const requiredItemCount = colours.length; - const colorsInput = this.getInput("COLORS"); + const colorsInput = this.getInput('COLORS'); let listBlock = colorsInput.connection?.targetBlock(); // Create a mutation element with the correct number of items. - const mutation = document.createElement("mutation"); - mutation.setAttribute("items", requiredItemCount); + const mutation = document.createElement('mutation'); + mutation.setAttribute('items', requiredItemCount); - if (listBlock && listBlock.type === "lists_create_with") { + if (listBlock && listBlock.type === 'lists_create_with') { // Apply the mutation to update the block's inputs. listBlock.domToMutation(mutation); // Remove any extra inputs beyond the required count. listBlock.inputList - .filter((input) => input.name && input.name.startsWith("ADD")) + .filter((input) => input.name && input.name.startsWith('ADD')) .forEach((input) => { const index = parseInt(input.name.substring(3)); if (index >= requiredItemCount) { @@ -359,46 +344,45 @@ export function defineModelBlocks() { // For each required input, update or create its shadow colour block. for (let i = 0; i < requiredItemCount; i++) { - let input = listBlock.getInput("ADD" + i); + let input = listBlock.getInput('ADD' + i); if (!input) { - input = listBlock.appendValueInput("ADD" + i).setCheck("Colour"); + input = listBlock.appendValueInput('ADD' + i).setCheck('Colour'); } let shadowBlock = input.connection?.targetBlock(); if (!shadowBlock || !shadowBlock.isShadow()) { - shadowBlock = listBlock.workspace.newBlock("colour"); - shadowBlock.setFieldValue(colours[i] || "#000000", "COLOR"); + shadowBlock = listBlock.workspace.newBlock('colour'); + shadowBlock.setFieldValue(colours[i] || '#000000', 'COLOR'); shadowBlock.setShadow(true); shadowBlock.initSvg(); input.connection.connect(shadowBlock.outputConnection); } else { - shadowBlock.setFieldValue(colours[i] || "#000000", "COLOR"); + shadowBlock.setFieldValue(colours[i] || '#000000', 'COLOR'); } } listBlock.initSvg(); listBlock.render(); } else if (!listBlock) { // Create a new list block. - listBlock = this.workspace.newBlock("lists_create_with"); + listBlock = this.workspace.newBlock('lists_create_with'); listBlock.setShadow(true); listBlock.domToMutation(mutation); for (let i = 0; i < requiredItemCount; i++) { - let input = listBlock.getInput("ADD" + i); + let input = listBlock.getInput('ADD' + i); if (!input) { - input = listBlock.appendValueInput("ADD" + i).setCheck("Colour"); + input = listBlock.appendValueInput('ADD' + i).setCheck('Colour'); } - const shadowBlock = listBlock.workspace.newBlock("colour"); - shadowBlock.setFieldValue(colours[i] || "#000000", "COLOR"); + const shadowBlock = listBlock.workspace.newBlock('colour'); + shadowBlock.setFieldValue(colours[i] || '#000000', 'COLOR'); shadowBlock.setShadow(true); shadowBlock.initSvg(); input.connection.connect(shadowBlock.outputConnection); } listBlock.setInputsInline(true); listBlock.setTooltip( - Blockly.Msg["LISTS_CREATE_WITH_TOOLTIP"] || - "Create a list of colours.", + Blockly.Msg['LISTS_CREATE_WITH_TOOLTIP'] || 'Create a list of colours.' ); listBlock.setHelpUrl( - "https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks", + 'https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks' ); listBlock.initSvg(); @@ -407,21 +391,18 @@ export function defineModelBlocks() { } }; - Blockly.Blocks["load_multi_object"].updateColorAtIndex = function ( - colour, - colourIndex, - ) { - const colorsInput = this.getInput("COLORS"); + Blockly.Blocks['load_multi_object'].updateColorAtIndex = function (colour, colourIndex) { + const colorsInput = this.getInput('COLORS'); if (!colorsInput || !colorsInput.connection) { return; } const listBlock = colorsInput.connection.targetBlock(); - if (!listBlock || listBlock.type !== "lists_create_with") { - console.log("List block not found or of incorrect type."); + if (!listBlock || listBlock.type !== 'lists_create_with') { + console.log('List block not found or of incorrect type.'); return; } - const inputName = "ADD" + colourIndex; + const inputName = 'ADD' + colourIndex; let input = listBlock.getInput(inputName); if (!input) { //input = listBlock.appendValueInput(inputName).setCheck("Colour"); @@ -430,13 +411,13 @@ export function defineModelBlocks() { let shadowBlock = input.connection?.targetBlock(); if (!shadowBlock || !shadowBlock.isShadow()) { - shadowBlock = listBlock.workspace.newBlock("colour"); + shadowBlock = listBlock.workspace.newBlock('colour'); shadowBlock.setShadow(true); shadowBlock.initSvg(); input.connection.connect(shadowBlock.outputConnection); } - shadowBlock.setFieldValue(colour, "COLOR"); + shadowBlock.setFieldValue(colour, 'COLOR'); shadowBlock.render(); listBlock.render(); }; @@ -445,25 +426,18 @@ export function defineModelBlocks() { // PRIORITY: Handle MODELS field change first (before other handlers can return early) if ( changeEvent.type === Blockly.Events.BLOCK_CHANGE && - changeEvent.element === "field" && - changeEvent.name === "MODELS" && + changeEvent.element === 'field' && + changeEvent.name === 'MODELS' && changeEvent.blockId === this.id ) { - const blockInWorkspace = Blockly.getMainWorkspace().getBlockById( - this.id, - ); + const blockInWorkspace = Blockly.getMainWorkspace().getBlockById(this.id); if (blockInWorkspace) { this.updateColorsField(); } // Don't return here - let other handlers process this event too } - handleBlockCreateEvent( - this, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ); + handleBlockCreateEvent(this, changeEvent, variableNamePrefix, nextVariableIndexes); // Always handle mesh lifecycle if the event targets this block, // or when this block is part of a snippet creation (its id is in changeEvent.ids). @@ -488,26 +462,25 @@ export function defineModelBlocks() { }, }; - Blockly.Blocks["load_model"] = { + Blockly.Blocks['load_model'] = { init: function () { - const variableNamePrefix = "model"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; // Start with "model1" + const variableNamePrefix = 'model'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; // Start with "model1" this.jsonInit({ - message0: translate("load_model"), + message0: translate('load_model'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "field_grid_dropdown", - name: "MODELS", + type: 'field_grid_dropdown', + name: 'MODELS', columns: 6, options: modelNames.map((name) => { - const baseName = name.replace(/\.[^/.]+$/, ""); + const baseName = name.replace(/\.[^/.]+$/, ''); return [ { src: `${flock.imagePath}${baseName}.png`, @@ -520,43 +493,38 @@ export function defineModelBlocks() { }), }, { - type: "input_value", - name: "SCALE", - check: "Number", + type: 'input_value', + name: 'SCALE', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("load_model"), + colour: categoryColours['Scene'], + tooltip: getTooltip('load_model'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); registerBlockHandler(this, (changeEvent) => { - handleBlockCreateEvent( - this, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ); + handleBlockCreateEvent(this, changeEvent, variableNamePrefix, nextVariableIndexes); const isThisBlockCreated = changeEvent.type === Blockly.Events.BLOCK_CREATE && diff --git a/blocks/physics.js b/blocks/physics.js index 294e1499..1ea4fb7e 100644 --- a/blocks/physics.js +++ b/blocks/physics.js @@ -1,168 +1,164 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { getHelpUrlFor } from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor } from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; export function definePhysicsBlocks() { - Blockly.Blocks["add_physics"] = { + Blockly.Blocks['add_physics'] = { init: function () { this.jsonInit({ - type: "add_physics", - message0: translate("add_physics"), + type: 'add_physics', + message0: translate('add_physics'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "PHYSICS_TYPE", + type: 'field_dropdown', + name: 'PHYSICS_TYPE', options: [ - getDropdownOption("DYNAMIC"), - getDropdownOption("ANIMATED"), - getDropdownOption("STATIC"), - getDropdownOption("NONE"), + getDropdownOption('DYNAMIC'), + getDropdownOption('ANIMATED'), + getDropdownOption('STATIC'), + getDropdownOption('NONE'), ], - default: "DYNAMIC", + default: 'DYNAMIC', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("add_physics"), + colour: categoryColours['Transform'], + tooltip: getTooltip('add_physics'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["add_physics_shape"] = { + Blockly.Blocks['add_physics_shape'] = { init: function () { this.jsonInit({ - type: "add_physics_shape", - message0: translate("add_physics_shape"), + type: 'add_physics_shape', + message0: translate('add_physics_shape'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "SHAPE_TYPE", - options: [getDropdownOption("MESH"), getDropdownOption("CAPSULE")], - default: "MESH", + type: 'field_dropdown', + name: 'SHAPE_TYPE', + options: [getDropdownOption('MESH'), getDropdownOption('CAPSULE')], + default: 'MESH', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("add_physics_shape"), + colour: categoryColours['Transform'], + tooltip: getTooltip('add_physics_shape'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["apply_force"] = { + Blockly.Blocks['apply_force'] = { init: function () { this.jsonInit({ - type: "apply_force", - message0: translate("apply_force"), + type: 'apply_force', + message0: translate('apply_force'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("apply_force"), + colour: categoryColours['Transform'], + tooltip: getTooltip('apply_force'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["show_physics"] = { + Blockly.Blocks['show_physics'] = { init: function () { this.jsonInit({ - type: "show_physics", - message0: translate("show_physics"), + type: 'show_physics', + message0: translate('show_physics'), args0: [ { - type: "field_checkbox", - name: "SHOW", + type: 'field_checkbox', + name: 'SHOW', checked: true, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("show_physics"), + colour: categoryColours['Transform'], + tooltip: getTooltip('show_physics'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["move_forward"] = { + Blockly.Blocks['move_forward'] = { init: function () { this.jsonInit({ - type: "move", - message0: translate("move_forward"), + type: 'move', + message0: translate('move_forward'), args0: [ { - type: "field_variable", - name: "MODEL", + type: 'field_variable', + name: 'MODEL', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "DIRECTION", + type: 'field_dropdown', + name: 'DIRECTION', options: [ - getDropdownOption("forward"), - getDropdownOption("sideways"), - getDropdownOption("strafe"), + getDropdownOption('forward'), + getDropdownOption('sideways'), + getDropdownOption('strafe'), ], }, { - type: "input_value", - name: "SPEED", - check: "Number", + type: 'input_value', + name: 'SPEED', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("move_forward"), + colour: categoryColours['Transform'], + tooltip: getTooltip('move_forward'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; } diff --git a/blocks/scene.js b/blocks/scene.js index aafcd94c..162090bd 100644 --- a/blocks/scene.js +++ b/blocks/scene.js @@ -1,5 +1,5 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { nextVariableIndexes, handleBlockCreateEvent, @@ -8,10 +8,10 @@ import { addDoMutatorWithToggleBehavior, getHelpUrlFor, registerBlockHandler, -} from "./blocks.js"; -import { mapNames } from "../config.js"; -import { updateOrCreateMeshFromBlock } from "../ui/blockmesh.js"; -import { translate, getTooltip, getOption } from "../main/translation.js"; +} from './blocks.js'; +import { mapNames } from '../config.js'; +import { updateOrCreateMeshFromBlock } from '../ui/blockmesh.js'; +import { translate, getTooltip, getOption } from '../main/translation.js'; function initSceneJsonBlock(block, { type, args0, inputsInline = true }) { block.jsonInit({ @@ -21,26 +21,26 @@ function initSceneJsonBlock(block, { type, args0, inputsInline = true }) { previousStatement: null, nextStatement: null, inputsInline, - colour: categoryColours["Scene"], + colour: categoryColours['Scene'], tooltip: getTooltip(type), }); block.setHelpUrl(getHelpUrlFor(block.type)); - block.setStyle("scene_blocks"); + block.setStyle('scene_blocks'); } function buildMapNameDropdownArgs() { return [ { - type: "field_dropdown", - name: "MAP_NAME", - options: [[getOption("FLAT"), "NONE"]].concat(mapNames()), + type: 'field_dropdown', + name: 'MAP_NAME', + options: [[getOption('FLAT'), 'NONE']].concat(mapNames()), }, ]; } function buildInputValueArg({ name, check, colour }) { - const arg = { type: "input_value", name }; + const arg = { type: 'input_value', name }; if (check) arg.check = check; if (colour) arg.colour = colour; return arg; @@ -68,9 +68,7 @@ export function makeTouchesInputSubtree(block, ws, inputName) { } function isDragStop(changeEvent) { - return ( - changeEvent.type === Blockly.Events.UI && changeEvent.element === "dragStop" - ); + return changeEvent.type === Blockly.Events.UI && changeEvent.element === 'dragStop'; } export function changeEventHitsTouches(changeEvent, touches) { @@ -128,7 +126,7 @@ function attachMeshSyncOnChange(block, config) { } function initSceneColourLikeBlock(block, cfg) { - const inputName = cfg.inputName || "COLOR"; + const inputName = cfg.inputName || 'COLOR'; const args0 = []; if (cfg.hasDropdown) args0.push(...buildMapNameDropdownArgs()); @@ -136,8 +134,8 @@ function initSceneColourLikeBlock(block, cfg) { buildInputValueArg({ name: inputName, colour: cfg.inputColor, - check: cfg.check || ["Colour", "Array", "Material"], - }), + check: cfg.check || ['Colour', 'Array', 'Material'], + }) ); initSceneJsonBlock(block, { type: cfg.type, args0 }); @@ -151,7 +149,7 @@ function initSceneColourLikeBlock(block, cfg) { const input = block.getInput(inputName); if (input?.connection) { const shadowDom = Blockly.utils.xml.textToDom( - `${cfg.inputColor}`, + `${cfg.inputColor}` ); input.connection.setShadowDom(shadowDom); input.connection.respawnShadow_(); @@ -160,8 +158,8 @@ function initSceneColourLikeBlock(block, cfg) { } export function cacheMaterialState(mapBlock) { - const mat = mapBlock.getInputTargetBlock("MATERIAL"); - if (!mat || mat.type !== "material") return; + const mat = mapBlock.getInputTargetBlock('MATERIAL'); + if (!mat || mat.type !== 'material') return; if (mat.isShadow?.()) return; mapBlock._cachedMaterialState = Blockly.serialization.blocks.save(mat); @@ -171,7 +169,7 @@ function refillMaterialFromCache(mapBlock) { const ws = mapBlock.workspace; if (!ws || ws.isFlyout) return false; - const input = mapBlock.getInput("MATERIAL"); + const input = mapBlock.getInput('MATERIAL'); const conn = input?.connection; if (!conn || conn.isConnected()) return false; @@ -192,11 +190,11 @@ export function replaceShadowMaterialWithCache(mapBlock) { const state = mapBlock._cachedMaterialState; if (!state) return false; - const mat = mapBlock.getInputTargetBlock("MATERIAL"); - if (!mat || mat.type !== "material") return false; + const mat = mapBlock.getInputTargetBlock('MATERIAL'); + if (!mat || mat.type !== 'material') return false; if (!mat.isShadow?.()) return false; - const input = mapBlock.getInput("MATERIAL"); + const input = mapBlock.getInput('MATERIAL'); const conn = input?.connection; if (!conn) return false; @@ -213,8 +211,8 @@ export function promoteMaterialContainerFromShadow(mapBlock) { const ws = mapBlock.workspace; if (!ws || ws.isFlyout) return; - const mat = mapBlock.getInputTargetBlock("MATERIAL"); - if (!mat || mat.type !== "material") return; + const mat = mapBlock.getInputTargetBlock('MATERIAL'); + if (!mat || mat.type !== 'material') return; if (!mat.isShadow?.()) return; @@ -229,7 +227,7 @@ export function promoteMaterialContainerFromShadow(mapBlock) { } delete state.shadow; - const input = mapBlock.getInput("MATERIAL"); + const input = mapBlock.getInput('MATERIAL'); const conn = input?.connection; if (!conn) return; @@ -245,16 +243,16 @@ export function respawnMaterialShadow(mapBlock) { const ws = mapBlock.workspace; if (!ws || ws.isFlyout) return; - const input = mapBlock.getInput("MATERIAL"); + const input = mapBlock.getInput('MATERIAL'); const conn = input?.connection; if (!conn || conn.isConnected()) return; const restored = refillMaterialFromCache(mapBlock); if (restored) return; - const block = ws.newBlock("material"); - if (typeof block.initSvg === "function") block.initSvg(); - if (typeof block.render === "function") block.render(); + const block = ws.newBlock('material'); + if (typeof block.initSvg === 'function') block.initSvg(); + if (typeof block.render === 'function') block.render(); if (block?.outputConnection) { block.outputConnection.connect(conn); @@ -263,7 +261,7 @@ export function respawnMaterialShadow(mapBlock) { function attachCreateMapOnChange(block) { const ws = block.workspace; - const touches = makeTouchesInputSubtree(block, ws, "MATERIAL"); + const touches = makeTouchesInputSubtree(block, ws, 'MATERIAL'); block.setOnChange((changeEvent) => { const eventTypes = [ @@ -278,11 +276,10 @@ function attachCreateMapOnChange(block) { const relevant = wasBlockDeleted(changeEvent, block.id) || changeEventHitsTouches(changeEvent, touches) || - (changeEvent.type === Blockly.Events.BLOCK_CREATE && - changeEvent.blockId === block.id) || + (changeEvent.type === Blockly.Events.BLOCK_CREATE && changeEvent.blockId === block.id) || (changeEvent.type === Blockly.Events.BLOCK_MOVE && changeEvent.oldParentId === block.id && - changeEvent.oldInputName === "MATERIAL"); + changeEvent.oldInputName === 'MATERIAL'); if (!relevant) return; @@ -290,7 +287,7 @@ function attachCreateMapOnChange(block) { promoteMaterialContainerFromShadow(block); - if (!block.getInputTargetBlock("MATERIAL")) { + if (!block.getInputTargetBlock('MATERIAL')) { respawnMaterialShadow(block); return; } @@ -305,66 +302,60 @@ function attachCreateMapOnChange(block) { } export function defineSceneBlocks() { - Blockly.Blocks["set_sky_color"] = { + Blockly.Blocks['set_sky_color'] = { init: function () { initSceneColourLikeBlock(this, { - type: "set_sky_color", - inputColor: "#6495ED", + type: 'set_sky_color', + inputColor: '#6495ED', listenToMove: true, }); }, }; - Blockly.Blocks["create_ground"] = { + Blockly.Blocks['create_ground'] = { init: function () { initSceneColourLikeBlock(this, { - type: "create_ground", - inputColor: "#71BC78", + type: 'create_ground', + inputColor: '#71BC78', listenToMove: true, }); }, }; - Blockly.Blocks["set_background_color"] = { + Blockly.Blocks['set_background_color'] = { init: function () { initSceneColourLikeBlock(this, { - type: "set_background_color", - inputColor: "#6495ED", - check: ["Colour"], + type: 'set_background_color', + inputColor: '#6495ED', + check: ['Colour'], listenToMove: true, }); }, }; - Blockly.Blocks["create_map"] = { + Blockly.Blocks['create_map'] = { init: function () { const args0 = [ ...buildMapNameDropdownArgs(), buildInputValueArg({ - name: "MATERIAL", - check: [ - "Material", - "Array", - "Colour", - "material_like", - "colour_array", - ], + name: 'MATERIAL', + check: ['Material', 'Array', 'Colour', 'material_like', 'colour_array'], }), ]; - initSceneJsonBlock(this, { type: "create_map", args0 }); + initSceneJsonBlock(this, { type: 'create_map', args0 }); attachCreateMapOnChange(this); }, }; - Blockly.Blocks["show"] = { + Blockly.Blocks['show'] = { init: function () { initSceneJsonBlock(this, { - type: "show", + type: 'show', args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, ], @@ -373,14 +364,14 @@ export function defineSceneBlocks() { }, }; - Blockly.Blocks["hide"] = { + Blockly.Blocks['hide'] = { init: function () { initSceneJsonBlock(this, { - type: "hide", + type: 'hide', args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, ], @@ -389,14 +380,14 @@ export function defineSceneBlocks() { }, }; - Blockly.Blocks["dispose"] = { + Blockly.Blocks['dispose'] = { init: function () { initSceneJsonBlock(this, { - type: "dispose", + type: 'dispose', args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, ], @@ -404,45 +395,39 @@ export function defineSceneBlocks() { }, }; - Blockly.Blocks["clone_mesh"] = { + Blockly.Blocks['clone_mesh'] = { init: function () { - const variableNamePrefix = "clone"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'clone'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - message0: translate("clone_mesh"), + message0: translate('clone_mesh'), args0: [ { - type: "field_variable", - name: "CLONE_VAR", + type: 'field_variable', + name: 'CLONE_VAR', variable: nextVariableName, }, { - type: "field_variable", - name: "SOURCE_MESH", + type: 'field_variable', + name: 'SOURCE_MESH', variable: window.currentMesh, }, ], inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("clone_mesh"), - helpUrl: "", + colour: categoryColours['Scene'], + tooltip: getTooltip('clone_mesh'), + helpUrl: '', previousStatement: null, nextStatement: null, }); registerBlockHandler(this, (changeEvent) => { - handleBlockCreateEvent( - this, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ); + handleBlockCreateEvent(this, changeEvent, variableNamePrefix, nextVariableIndexes); }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); addDoMutatorWithToggleBehavior(this); }, }; diff --git a/blocks/sensing.js b/blocks/sensing.js index e310e89e..4c11f4c7 100644 --- a/blocks/sensing.js +++ b/blocks/sensing.js @@ -1,5 +1,5 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { getHelpUrlFor, addToggleButton, @@ -9,501 +9,493 @@ import { handleBlockCreateEvent, updateShape, registerBlockHandler, -} from "./blocks.js"; -import { - translate, - getTooltip, - getOption, - getDropdownOption, -} from "../main/translation.js"; -import { ACTIONS } from "../input/bindings.js"; +} from './blocks.js'; +import { translate, getTooltip, getOption, getDropdownOption } from '../main/translation.js'; +import { ACTIONS } from '../input/bindings.js'; export function defineSensingBlocks() { - Blockly.Blocks["key_pressed"] = { + Blockly.Blocks['key_pressed'] = { init: function () { this.jsonInit({ - type: "key_pressed", - message0: translate("key_pressed"), + type: 'key_pressed', + message0: translate('key_pressed'), args0: [ { - type: "field_dropdown", - name: "KEY", + type: 'field_dropdown', + name: 'KEY', options: [ - getDropdownOption("ANY"), - getDropdownOption("NONE"), - getDropdownOption("w"), - getDropdownOption("a"), - getDropdownOption("s"), - getDropdownOption("d"), - [getOption("space_infinity"), " "], - [getOption("q_icon"), "q"], - [getOption("e_icon"), "e"], - [getOption("f_icon"), "f"], + getDropdownOption('ANY'), + getDropdownOption('NONE'), + getDropdownOption('w'), + getDropdownOption('a'), + getDropdownOption('s'), + getDropdownOption('d'), + [getOption('space_infinity'), ' '], + [getOption('q_icon'), 'q'], + [getOption('e_icon'), 'e'], + [getOption('f_icon'), 'f'], ], }, ], - output: "Boolean", - colour: categoryColours["Sensing"], - tooltip: getTooltip("key_pressed"), + output: 'Boolean', + colour: categoryColours['Sensing'], + tooltip: getTooltip('key_pressed'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["action_pressed"] = { + Blockly.Blocks['action_pressed'] = { init: function () { this.jsonInit({ - type: "action_pressed", - message0: translate("action_pressed"), + type: 'action_pressed', + message0: translate('action_pressed'), args0: [ { - type: "field_dropdown", - name: "ACTION", + type: 'field_dropdown', + name: 'ACTION', options: ACTIONS.map((a) => [getOption(`ACTION_${a}`), a]), }, ], - output: "Boolean", - colour: categoryColours["Sensing"], - tooltip: getTooltip("action_pressed"), + output: 'Boolean', + colour: categoryColours['Sensing'], + tooltip: getTooltip('action_pressed'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["set_action_key"] = { + Blockly.Blocks['set_action_key'] = { init: function () { this.jsonInit({ - type: "set_action_key", - message0: translate("set_action_key"), + type: 'set_action_key', + message0: translate('set_action_key'), args0: [ { - type: "field_dropdown", - name: "ACTION", + type: 'field_dropdown', + name: 'ACTION', options: ACTIONS.map((a) => [getOption(`ACTION_${a}`), a]), }, { - type: "field_grid_dropdown", - name: "KEY", + type: 'field_grid_dropdown', + name: 'KEY', columns: 10, options: [ - getDropdownOption("0"), - getDropdownOption("1"), - getDropdownOption("2"), - getDropdownOption("3"), - getDropdownOption("4"), - getDropdownOption("5"), - getDropdownOption("6"), - getDropdownOption("7"), - getDropdownOption("8"), - getDropdownOption("9"), - getDropdownOption("a"), - getDropdownOption("b"), - getDropdownOption("c"), - getDropdownOption("d"), - getDropdownOption("e"), - getDropdownOption("f"), - getDropdownOption("g"), - getDropdownOption("h"), - getDropdownOption("i"), - getDropdownOption("j"), - getDropdownOption("k"), - getDropdownOption("l"), - getDropdownOption("m"), - getDropdownOption("n"), - getDropdownOption("o"), - getDropdownOption("p"), - getDropdownOption("q"), - getDropdownOption("r"), - getDropdownOption("s"), - getDropdownOption("t"), - getDropdownOption("u"), - getDropdownOption("v"), - getDropdownOption("w"), - getDropdownOption("x"), - getDropdownOption("y"), - getDropdownOption("z"), - getDropdownOption(" "), - getDropdownOption(","), - getDropdownOption("."), - getDropdownOption("/"), - getDropdownOption("ArrowLeft"), - getDropdownOption("ArrowUp"), - getDropdownOption("ArrowRight"), - getDropdownOption("ArrowDown"), + getDropdownOption('0'), + getDropdownOption('1'), + getDropdownOption('2'), + getDropdownOption('3'), + getDropdownOption('4'), + getDropdownOption('5'), + getDropdownOption('6'), + getDropdownOption('7'), + getDropdownOption('8'), + getDropdownOption('9'), + getDropdownOption('a'), + getDropdownOption('b'), + getDropdownOption('c'), + getDropdownOption('d'), + getDropdownOption('e'), + getDropdownOption('f'), + getDropdownOption('g'), + getDropdownOption('h'), + getDropdownOption('i'), + getDropdownOption('j'), + getDropdownOption('k'), + getDropdownOption('l'), + getDropdownOption('m'), + getDropdownOption('n'), + getDropdownOption('o'), + getDropdownOption('p'), + getDropdownOption('q'), + getDropdownOption('r'), + getDropdownOption('s'), + getDropdownOption('t'), + getDropdownOption('u'), + getDropdownOption('v'), + getDropdownOption('w'), + getDropdownOption('x'), + getDropdownOption('y'), + getDropdownOption('z'), + getDropdownOption(' '), + getDropdownOption(','), + getDropdownOption('.'), + getDropdownOption('/'), + getDropdownOption('ArrowLeft'), + getDropdownOption('ArrowUp'), + getDropdownOption('ArrowRight'), + getDropdownOption('ArrowDown'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Sensing"], - tooltip: getTooltip("set_action_key"), + colour: categoryColours['Sensing'], + tooltip: getTooltip('set_action_key'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["meshes_touching"] = { + Blockly.Blocks['meshes_touching'] = { init: function () { this.jsonInit({ - type: "meshes_are_touching", - message0: translate("meshes_touching"), + type: 'meshes_are_touching', + message0: translate('meshes_touching'), args0: [ { - type: "field_variable", - name: "MESH1", + type: 'field_variable', + name: 'MESH1', variable: window.currentMesh, }, { - type: "field_variable", - name: "MESH2", - variable: "object2", + type: 'field_variable', + name: 'MESH2', + variable: 'object2', }, ], - output: "Boolean", - colour: categoryColours["Sensing"], - tooltip: getTooltip("meshes_touching"), + output: 'Boolean', + colour: categoryColours['Sensing'], + tooltip: getTooltip('meshes_touching'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["time"] = { + Blockly.Blocks['time'] = { init: function () { this.jsonInit({ - type: "time", - message0: translate("time"), + type: 'time', + message0: translate('time'), args0: [ { - type: "field_dropdown", - name: "UNIT", + type: 'field_dropdown', + name: 'UNIT', options: [ - [getOption("seconds"), "seconds"], - [getOption("milliseconds"), "milliseconds"], - [getOption("minutes"), "minutes"], + [getOption('seconds'), 'seconds'], + [getOption('milliseconds'), 'milliseconds'], + [getOption('minutes'), 'minutes'], ], }, ], - output: "Number", - colour: categoryColours["Sensing"], + output: 'Number', + colour: categoryColours['Sensing'], inputsInline: true, - tooltip: getTooltip("time"), + tooltip: getTooltip('time'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["ground_level"] = { + Blockly.Blocks['ground_level'] = { init: function () { this.jsonInit({ - type: "ground_level", - message0: translate("ground_level"), - output: "Number", - colour: categoryColours["Sensing"], - tooltip: getTooltip("ground_level"), + type: 'ground_level', + message0: translate('ground_level'), + output: 'Number', + colour: categoryColours['Sensing'], + tooltip: getTooltip('ground_level'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["distance_to"] = { + Blockly.Blocks['distance_to'] = { init: function () { this.jsonInit({ - type: "distance_to", - message0: translate("distance_to"), + type: 'distance_to', + message0: translate('distance_to'), args0: [ { - type: "field_variable", - name: "MODEL1", + type: 'field_variable', + name: 'MODEL1', variable: window.currentMesh, }, { - type: "field_variable", - name: "MODEL2", - variable: "object2", + type: 'field_variable', + name: 'MODEL2', + variable: 'object2', }, ], - output: "Number", - colour: categoryColours["Sensing"], + output: 'Number', + colour: categoryColours['Sensing'], inputsInline: true, - tooltip: getTooltip("distance_to"), + tooltip: getTooltip('distance_to'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["touching_surface"] = { + Blockly.Blocks['touching_surface'] = { init: function () { this.jsonInit({ - type: "touching_surface", - message0: translate("touching_surface"), + type: 'touching_surface', + message0: translate('touching_surface'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, ], - output: "Boolean", - colour: categoryColours["Sensing"], - tooltip: getTooltip("touching_surface"), + output: 'Boolean', + colour: categoryColours['Sensing'], + tooltip: getTooltip('touching_surface'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["mesh_exists"] = { + Blockly.Blocks['mesh_exists'] = { init: function () { this.jsonInit({ - type: "mesh_exists", - message0: translate("mesh_exists"), + type: 'mesh_exists', + message0: translate('mesh_exists'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, ], - output: "Boolean", - colour: categoryColours["Sensing"], - tooltip: getTooltip("mesh_exists"), + output: 'Boolean', + colour: categoryColours['Sensing'], + tooltip: getTooltip('mesh_exists'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["get_property"] = { + Blockly.Blocks['get_property'] = { init: function () { this.jsonInit({ - type: "get_property", - message0: translate("get_property"), + type: 'get_property', + message0: translate('get_property'), args0: [ { - type: "field_dropdown", - name: "PROPERTY", + type: 'field_dropdown', + name: 'PROPERTY', options: [ - getDropdownOption("POSITION_X"), - getDropdownOption("POSITION_Y"), - getDropdownOption("POSITION_Z"), - getDropdownOption("ROTATION_X"), - getDropdownOption("ROTATION_Y"), - getDropdownOption("ROTATION_Z"), - getDropdownOption("MIN_X"), - getDropdownOption("MAX_X"), - getDropdownOption("MIN_Y"), - getDropdownOption("MAX_Y"), - getDropdownOption("MIN_Z"), - getDropdownOption("MAX_Z"), - getDropdownOption("SCALE_X"), - getDropdownOption("SCALE_Y"), - getDropdownOption("SCALE_Z"), - getDropdownOption("SIZE_X"), - getDropdownOption("SIZE_Y"), - getDropdownOption("SIZE_Z"), - getDropdownOption("VISIBLE"), - getDropdownOption("ALPHA"), - getDropdownOption("COLOUR"), - getDropdownOption("DESCRIPTION"), + getDropdownOption('POSITION_X'), + getDropdownOption('POSITION_Y'), + getDropdownOption('POSITION_Z'), + getDropdownOption('ROTATION_X'), + getDropdownOption('ROTATION_Y'), + getDropdownOption('ROTATION_Z'), + getDropdownOption('MIN_X'), + getDropdownOption('MAX_X'), + getDropdownOption('MIN_Y'), + getDropdownOption('MAX_Y'), + getDropdownOption('MIN_Z'), + getDropdownOption('MAX_Z'), + getDropdownOption('SCALE_X'), + getDropdownOption('SCALE_Y'), + getDropdownOption('SCALE_Z'), + getDropdownOption('SIZE_X'), + getDropdownOption('SIZE_Y'), + getDropdownOption('SIZE_Z'), + getDropdownOption('VISIBLE'), + getDropdownOption('ALPHA'), + getDropdownOption('COLOUR'), + getDropdownOption('DESCRIPTION'), ], }, { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, }, ], output: null, - colour: categoryColours["Sensing"], - tooltip: getTooltip("get_property"), + colour: categoryColours['Sensing'], + tooltip: getTooltip('get_property'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["canvas_controls"] = { + Blockly.Blocks['canvas_controls'] = { init: function () { this.jsonInit({ - type: "canvas_controls", - message0: translate("canvas_controls"), + type: 'canvas_controls', + message0: translate('canvas_controls'), args0: [ { - type: "field_checkbox", - name: "CONTROLS", + type: 'field_checkbox', + name: 'CONTROLS', checked: true, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Sensing"], - tooltip: getTooltip("canvas_controls"), + colour: categoryColours['Sensing'], + tooltip: getTooltip('canvas_controls'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["interact_indicator"] = { + Blockly.Blocks['interact_indicator'] = { init: function () { this.jsonInit({ - type: "interact_indicator", - message0: translate("interact_indicator"), + type: 'interact_indicator', + message0: translate('interact_indicator'), args0: [ { - type: "field_checkbox", - name: "ENABLED", + type: 'field_checkbox', + name: 'ENABLED', checked: true, }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Sensing"], - tooltip: getTooltip("interact_indicator"), + colour: categoryColours['Sensing'], + tooltip: getTooltip('interact_indicator'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["button_controls"] = { + Blockly.Blocks['button_controls'] = { init: function () { this.jsonInit({ - type: "button_controls", - message0: translate("button_controls"), + type: 'button_controls', + message0: translate('button_controls'), args0: [ { - type: "field_dropdown", - name: "CONTROL", + type: 'field_dropdown', + name: 'CONTROL', options: [ - getDropdownOption("BOTH"), - getDropdownOption("ARROWS"), - getDropdownOption("ACTIONS"), + getDropdownOption('BOTH'), + getDropdownOption('ARROWS'), + getDropdownOption('ACTIONS'), ], }, { - type: "field_dropdown", - name: "ENABLED", + type: 'field_dropdown', + name: 'ENABLED', options: [ - getDropdownOption("AUTO"), - getDropdownOption("ENABLED"), - getDropdownOption("DISABLED"), + getDropdownOption('AUTO'), + getDropdownOption('ENABLED'), + getDropdownOption('DISABLED'), ], }, { - type: "input_value", - name: "COLOR", - check: "Colour", + type: 'input_value', + name: 'COLOR', + check: 'Colour', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Sensing"], - tooltip: getTooltip("button_controls"), + colour: categoryColours['Sensing'], + tooltip: getTooltip('button_controls'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["on_screen_controls"] = { + Blockly.Blocks['on_screen_controls'] = { init: function () { this.jsonInit({ - type: "on_screen_controls", - message0: translate("on_screen_controls"), + type: 'on_screen_controls', + message0: translate('on_screen_controls'), args0: [ { - type: "field_dropdown", - name: "MOVEMENT", + type: 'field_dropdown', + name: 'MOVEMENT', options: [ - getDropdownOption("ARROWS"), - getDropdownOption("JOYSTICK"), - getDropdownOption("NONE"), + getDropdownOption('ARROWS'), + getDropdownOption('JOYSTICK'), + getDropdownOption('NONE'), ], }, { - type: "field_dropdown", - name: "ACTIONS", - options: [ - getDropdownOption("YES"), - getDropdownOption("NO"), - ], + type: 'field_dropdown', + name: 'ACTIONS', + options: [getDropdownOption('YES'), getDropdownOption('NO')], }, { - type: "field_dropdown", - name: "ENABLED", + type: 'field_dropdown', + name: 'ENABLED', options: [ - getDropdownOption("AUTO"), - getDropdownOption("ENABLED"), - getDropdownOption("DISABLED"), + getDropdownOption('AUTO'), + getDropdownOption('ENABLED'), + getDropdownOption('DISABLED'), ], }, { - type: "input_value", - name: "COLOR", - check: "Colour", + type: 'input_value', + name: 'COLOR', + check: 'Colour', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Sensing"], - tooltip: getTooltip("on_screen_controls"), + colour: categoryColours['Sensing'], + tooltip: getTooltip('on_screen_controls'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); }, }; - Blockly.Blocks["microbit_input"] = { + Blockly.Blocks['microbit_input'] = { init: function () { this.jsonInit({ - type: "microbit_input", - message0: translate("microbit_input"), + type: 'microbit_input', + message0: translate('microbit_input'), args0: [ { - type: "field_dropdown", - name: "EVENT", + type: 'field_dropdown', + name: 'EVENT', options: [ - [getOption("pin_0"), "0"], - [getOption("pin_1"), "1"], - [getOption("pin_2"), "2"], - [getOption("pin_l"), "l"], - [getOption("pin_j"), "j"], - [getOption("pin_h"), "h"], - [getOption("pin_k"), "k"], - [getOption("pin_space"), " "], - [getOption("pin_q"), "q"], - [getOption("pin_r"), "r"], - [getOption("pin_t"), "t"], - [getOption("pin_o"), "o"], - [getOption("pin_p"), "p"], - [getOption("pin_a"), "a"], - [getOption("pin_d"), "d"], - [getOption("pin_y"), "y"], - [getOption("pin_g"), "g"], - [getOption("pin_i"), "i"], + [getOption('pin_0'), '0'], + [getOption('pin_1'), '1'], + [getOption('pin_2'), '2'], + [getOption('pin_l'), 'l'], + [getOption('pin_j'), 'j'], + [getOption('pin_h'), 'h'], + [getOption('pin_k'), 'k'], + [getOption('pin_space'), ' '], + [getOption('pin_q'), 'q'], + [getOption('pin_r'), 'r'], + [getOption('pin_t'), 't'], + [getOption('pin_o'), 'o'], + [getOption('pin_p'), 'p'], + [getOption('pin_a'), 'a'], + [getOption('pin_d'), 'd'], + [getOption('pin_y'), 'y'], + [getOption('pin_g'), 'g'], + [getOption('pin_i'), 'i'], ], }, ], - message1: "%1", + message1: '%1', args1: [ { - type: "input_statement", - name: "DO", + type: 'input_statement', + name: 'DO', }, ], - colour: categoryColours["Sensing"], - tooltip: getTooltip("microbit_input"), + colour: categoryColours['Sensing'], + tooltip: getTooltip('microbit_input'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sensing_blocks"); + this.setStyle('sensing_blocks'); addToggleButton(this); }, @@ -520,82 +512,81 @@ export function defineSensingBlocks() { this.updateShape_(!this.isInline); }, }; - Blockly.Blocks["ui_slider"] = { + Blockly.Blocks['ui_slider'] = { init: function () { - const variableNamePrefix = "slider"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'slider'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "ui_slider", - message0: translate("ui_slider"), + type: 'ui_slider', + message0: translate('ui_slider'), args0: [ { - type: "field_variable", - name: "SLIDER_VAR", + type: 'field_variable', + name: 'SLIDER_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "MIN", - check: "Number", + type: 'input_value', + name: 'MIN', + check: 'Number', }, { - type: "input_value", - name: "MAX", - check: "Number", + type: 'input_value', + name: 'MAX', + check: 'Number', }, { - type: "input_value", - name: "VALUE", - check: "Number", + type: 'input_value', + name: 'VALUE', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "COLOR", - check: "Colour", + type: 'input_value', + name: 'COLOR', + check: 'Colour', }, { - type: "input_value", - name: "BACKGROUND", - check: "Colour", + type: 'input_value', + name: 'BACKGROUND', + check: 'Colour', }, { - type: "field_dropdown", - name: "SIZE", + type: 'field_dropdown', + name: 'SIZE', options: [ - getDropdownOption("SMALL"), - getDropdownOption("MEDIUM"), - getDropdownOption("LARGE"), + getDropdownOption('SMALL'), + getDropdownOption('MEDIUM'), + getDropdownOption('LARGE'), ], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Text"], - tooltip: getTooltip("ui_slider"), + colour: categoryColours['Text'], + tooltip: getTooltip('ui_slider'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); registerBlockHandler(this, (changeEvent) => handleBlockCreateEvent( this, changeEvent, variableNamePrefix, nextVariableIndexes, - "SLIDER_VAR", - ), + 'SLIDER_VAR' + ) ); }, }; diff --git a/blocks/shapes.js b/blocks/shapes.js index 7eedb111..831ac196 100644 --- a/blocks/shapes.js +++ b/blocks/shapes.js @@ -1,5 +1,5 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { nextVariableIndexes, handleBlockChange, @@ -7,933 +7,918 @@ import { handleBlockCreateEvent, getHelpUrlFor, registerBlockHandler, -} from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; +} from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; export function defineShapeBlocks() { // Define the particle effect block. - Blockly.Blocks["create_particle_effect"] = { + Blockly.Blocks['create_particle_effect'] = { init: function () { - const variableNamePrefix = "particleEffect"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'particleEffect'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - message0: translate("create_particle_effect"), + message0: translate('create_particle_effect'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "field_variable", - name: "EMITTER_MESH", + type: 'field_variable', + name: 'EMITTER_MESH', variable: window.currentMesh, }, { - type: "field_grid_dropdown", - name: "SHAPE", + type: 'field_grid_dropdown', + name: 'SHAPE', options: [ [ { - src: "./textures/circle_texture.png", + src: './textures/circle_texture.png', width: 32, height: 32, - alt: "Circle", + alt: 'Circle', }, - "circle_texture.png", + 'circle_texture.png', ], [ { - src: "./textures/balloon_texture.png", + src: './textures/balloon_texture.png', width: 32, height: 32, - alt: "Balloon", + alt: 'Balloon', }, - "balloon_texture.png", + 'balloon_texture.png', ], [ { - src: "./textures/bee_texture.png", + src: './textures/bee_texture.png', width: 32, height: 32, - alt: "Bee", + alt: 'Bee', }, - "bee_texture.png", + 'bee_texture.png', ], [ { - src: "./textures/bird_texture.png", + src: './textures/bird_texture.png', width: 32, height: 32, - alt: "Bird", + alt: 'Bird', }, - "bird_texture.png", + 'bird_texture.png', ], [ { - src: "./textures/blast_texture.png", + src: './textures/blast_texture.png', width: 32, height: 32, - alt: "Blast", + alt: 'Blast', }, - "blast_texture.png", + 'blast_texture.png', ], [ { - src: "./textures/bubble_texture.png", + src: './textures/bubble_texture.png', width: 32, height: 32, - alt: "Bubble", + alt: 'Bubble', }, - "bubble_texture.png", + 'bubble_texture.png', ], [ { - src: "./textures/burst_texture.png", + src: './textures/burst_texture.png', width: 32, height: 32, - alt: "Burst", + alt: 'Burst', }, - "burst_texture.png", + 'burst_texture.png', ], [ { - src: "./textures/chevron_texture.png", + src: './textures/chevron_texture.png', width: 32, height: 32, - alt: "Cheveron", + alt: 'Cheveron', }, - "chevron_texture.png", + 'chevron_texture.png', ], [ { - src: "./textures/comet_texture.png", + src: './textures/comet_texture.png', width: 32, height: 32, - alt: "Comet", + alt: 'Comet', }, - "comet_texture.png", + 'comet_texture.png', ], [ { - src: "./textures/confetti_texture.png", + src: './textures/confetti_texture.png', width: 32, height: 32, - alt: "Confetti", + alt: 'Confetti', }, - "confetti_texture.png", + 'confetti_texture.png', ], [ { - src: "./textures/exclaim_texture.png", + src: './textures/exclaim_texture.png', width: 32, height: 32, - alt: "Exclaim", + alt: 'Exclaim', }, - "exclaim_texture.png", + 'exclaim_texture.png', ], [ { - src: "./textures/flock_texture.png", + src: './textures/flock_texture.png', width: 32, height: 32, - alt: "Flock", + alt: 'Flock', }, - "flock_texture.png", + 'flock_texture.png', ], [ { - src: "./textures/fish_texture.png", + src: './textures/fish_texture.png', width: 32, height: 32, - alt: "Fish", + alt: 'Fish', }, - "fish_texture.png", + 'fish_texture.png', ], [ { - src: "./textures/fragments_texture.png", + src: './textures/fragments_texture.png', width: 32, height: 32, - alt: "Fragments", + alt: 'Fragments', }, - "fragments_texture.png", + 'fragments_texture.png', ], [ { - src: "./textures/gem_texture.png", + src: './textures/gem_texture.png', width: 32, height: 32, - alt: "Gem", + alt: 'Gem', }, - "gem_texture.png", + 'gem_texture.png', ], [ { - src: "./textures/ghost_texture.png", + src: './textures/ghost_texture.png', width: 32, height: 32, - alt: "Ghost", + alt: 'Ghost', }, - "ghost_texture.png", + 'ghost_texture.png', ], [ { - src: "./textures/heart_texture.png", + src: './textures/heart_texture.png', width: 32, height: 32, - alt: "Heart", + alt: 'Heart', }, - "heart_texture.png", + 'heart_texture.png', ], [ { - src: "./textures/leaf_texture.png", + src: './textures/leaf_texture.png', width: 32, height: 32, - alt: "Leaf", + alt: 'Leaf', }, - "leaf_texture.png", + 'leaf_texture.png', ], [ { - src: "./textures/leaf2_texture.png", + src: './textures/leaf2_texture.png', width: 32, height: 32, - alt: "Leaf", + alt: 'Leaf', }, - "leaf2_texture.png", + 'leaf2_texture.png', ], [ { - src: "./textures/mic_texture.png", + src: './textures/mic_texture.png', width: 32, height: 32, - alt: "Mic", + alt: 'Mic', }, - "mic_texture.png", + 'mic_texture.png', ], [ { - src: "./textures/money_texture.png", + src: './textures/money_texture.png', width: 32, height: 32, - alt: "Money", + alt: 'Money', }, - "money_texture.png", + 'money_texture.png', ], [ { - src: "./textures/music_texture.png", + src: './textures/music_texture.png', width: 32, height: 32, - alt: "Music", + alt: 'Music', }, - "music_texture.png", + 'music_texture.png', ], [ { - src: "./textures/paw_texture.png", + src: './textures/paw_texture.png', width: 32, height: 32, - alt: "Paw", + alt: 'Paw', }, - "paw_texture.png", + 'paw_texture.png', ], [ { - src: "./textures/rays_texture.png", + src: './textures/rays_texture.png', width: 32, height: 32, - alt: "Rays", + alt: 'Rays', }, - "rays_texture.png", + 'rays_texture.png', ], [ { - src: "./textures/ripple_texture.png", + src: './textures/ripple_texture.png', width: 32, height: 32, - alt: "Ripple", + alt: 'Ripple', }, - "ripple_texture.png", + 'ripple_texture.png', ], [ { - src: "./textures/rocket_texture.png", + src: './textures/rocket_texture.png', width: 32, height: 32, - alt: "Rocket", + alt: 'Rocket', }, - "rocket_texture.png", + 'rocket_texture.png', ], [ { - src: "./textures/sleep_texture.png", + src: './textures/sleep_texture.png', width: 32, height: 32, - alt: "Sleep", + alt: 'Sleep', }, - "sleep_texture.png", + 'sleep_texture.png', ], [ { - src: "./textures/speaking_texture.png", + src: './textures/speaking_texture.png', width: 32, height: 32, - alt: "Speaking", + alt: 'Speaking', }, - "speaking_texture.png", + 'speaking_texture.png', ], [ { - src: "./textures/splash_texture.png", + src: './textures/splash_texture.png', width: 32, height: 32, - alt: "Splash", + alt: 'Splash', }, - "splash_texture.png", + 'splash_texture.png', ], [ { - src: "./textures/splat_texture.png", + src: './textures/splat_texture.png', width: 32, height: 32, - alt: "Splat", + alt: 'Splat', }, - "splat_texture.png", + 'splat_texture.png', ], [ { - src: "./textures/star_texture.png", + src: './textures/star_texture.png', width: 32, height: 32, - alt: "Star", + alt: 'Star', }, - "star_texture.png", + 'star_texture.png', ], [ { - src: "./textures/sweet_texture.png", + src: './textures/sweet_texture.png', width: 32, height: 32, - alt: "Sweet", + alt: 'Sweet', }, - "sweet_texture.png", + 'sweet_texture.png', ], [ { - src: "./textures/butterfly_texture.png", + src: './textures/butterfly_texture.png', width: 32, height: 32, - alt: "Butterfly", + alt: 'Butterfly', }, - "butterfly_texture.png", + 'butterfly_texture.png', ], [ { - src: "./textures/flower_texture.png", + src: './textures/flower_texture.png', width: 32, height: 32, - alt: "Flower", + alt: 'Flower', }, - "flower_texture.png", + 'flower_texture.png', ], [ { - src: "./textures/flame_texture.png", + src: './textures/flame_texture.png', width: 32, height: 32, - alt: "Flame", + alt: 'Flame', }, - "flame_texture.png", + 'flame_texture.png', ], [ { - src: "./textures/smoke_texture.png", + src: './textures/smoke_texture.png', width: 32, height: 32, - alt: "Smoke", + alt: 'Smoke', }, - "smoke_texture.png", + 'smoke_texture.png', ], [ { - src: "./textures/snowflake_texture.png", + src: './textures/snowflake_texture.png', width: 32, height: 32, - alt: "Snowflake", + alt: 'Snowflake', }, - "snowflake_texture.png", + 'snowflake_texture.png', ], [ { - src: "./textures/swirl_texture.png", + src: './textures/swirl_texture.png', width: 32, height: 32, - alt: "Swirl", + alt: 'Swirl', }, - "swirl_texture.png", + 'swirl_texture.png', ], [ { - src: "./textures/wave_texture.png", + src: './textures/wave_texture.png', width: 32, height: 32, - alt: "Wave", + alt: 'Wave', }, - "wave_texture.png", + 'wave_texture.png', ], [ { - src: "./textures/wind_texture.png", + src: './textures/wind_texture.png', width: 32, height: 32, - alt: "Wind", + alt: 'Wind', }, - "wind_texture.png", + 'wind_texture.png', ], [ { - src: "./textures/strip_texture.png", + src: './textures/strip_texture.png', width: 32, height: 32, - alt: "Strip", + alt: 'Strip', }, - "strip_texture.png", + 'strip_texture.png', ], [ { - src: "./textures/crescent_texture.png", + src: './textures/crescent_texture.png', width: 32, height: 32, - alt: "Crescent", + alt: 'Crescent', }, - "crescent_texture.png", + 'crescent_texture.png', ], [ { - src: "./textures/lightning_texture.png", + src: './textures/lightning_texture.png', width: 32, height: 32, - alt: "Lightning bolt", + alt: 'Lightning bolt', }, - "lightning_texture.png", + 'lightning_texture.png', ], [ { - src: "./textures/droplet_texture.png", + src: './textures/droplet_texture.png', width: 32, height: 32, - alt: "Droplet", + alt: 'Droplet', }, - "droplet_texture.png", + 'droplet_texture.png', ], [ { - src: "./textures/shard_texture.png", + src: './textures/shard_texture.png', width: 32, height: 32, - alt: "Shard", + alt: 'Shard', }, - "shard_texture.png", + 'shard_texture.png', ], [ { - src: "./textures/square_texture.png", + src: './textures/square_texture.png', width: 32, height: 32, - alt: "Square", + alt: 'Square', }, - "square_texture.png", + 'square_texture.png', ], [ { - src: "./textures/arrow_texture.png", + src: './textures/arrow_texture.png', width: 32, height: 32, - alt: "Arrow", + alt: 'Arrow', }, - "arrow_texture.png", + 'arrow_texture.png', ], ], }, { - type: "input_value", - name: "START_COLOR", - check: "Colour", + type: 'input_value', + name: 'START_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "END_COLOR", - check: "Colour", + type: 'input_value', + name: 'END_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "START_ALPHA", - check: "Number", + type: 'input_value', + name: 'START_ALPHA', + check: 'Number', }, { - type: "input_value", - name: "END_ALPHA", - check: "Number", + type: 'input_value', + name: 'END_ALPHA', + check: 'Number', }, { - type: "input_value", - name: "RATE", - check: "Number", + type: 'input_value', + name: 'RATE', + check: 'Number', }, { - type: "input_value", - name: "MIN_SIZE", - check: "Number", + type: 'input_value', + name: 'MIN_SIZE', + check: 'Number', }, { - type: "input_value", - name: "MAX_SIZE", - check: "Number", + type: 'input_value', + name: 'MAX_SIZE', + check: 'Number', }, { - type: "input_value", - name: "MIN_LIFETIME", - check: "Number", + type: 'input_value', + name: 'MIN_LIFETIME', + check: 'Number', }, { - type: "input_value", - name: "MAX_LIFETIME", - check: "Number", + type: 'input_value', + name: 'MAX_LIFETIME', + check: 'Number', }, { - type: "field_checkbox", - name: "GRAVITY", + type: 'field_checkbox', + name: 'GRAVITY', checked: false, }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, { - type: "input_value", - name: "MIN_ANGULAR_SPEED", - check: "Number", + type: 'input_value', + name: 'MIN_ANGULAR_SPEED', + check: 'Number', }, { - type: "input_value", - name: "MAX_ANGULAR_SPEED", - check: "Number", + type: 'input_value', + name: 'MAX_ANGULAR_SPEED', + check: 'Number', }, { - type: "input_value", - name: "MIN_INITIAL_ROTATION", - check: "Number", + type: 'input_value', + name: 'MIN_INITIAL_ROTATION', + check: 'Number', }, { - type: "input_value", - name: "MAX_INITIAL_ROTATION", - check: "Number", + type: 'input_value', + name: 'MAX_INITIAL_ROTATION', + check: 'Number', }, ], inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("create_particle_effect"), + colour: categoryColours['Scene'], + tooltip: getTooltip('create_particle_effect'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); registerBlockHandler(this, (changeEvent) => - handleBlockCreateEvent( - this, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ), + handleBlockCreateEvent(this, changeEvent, variableNamePrefix, nextVariableIndexes) ); }, }; - Blockly.Blocks["create_box"] = { + Blockly.Blocks['create_box'] = { init: function () { - const variableNamePrefix = "box"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'box'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "create_box", - message0: translate("create_box"), + type: 'create_box', + message0: translate('create_box'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "COLOR", - check: ["Colour", "Material"], + type: 'input_value', + name: 'COLOR', + check: ['Colour', 'Material'], }, { - type: "input_value", - name: "WIDTH", - check: "Number", + type: 'input_value', + name: 'WIDTH', + check: 'Number', }, { - type: "input_value", - name: "HEIGHT", - check: "Number", + type: 'input_value', + name: 'HEIGHT', + check: 'Number', }, { - type: "input_value", - name: "DEPTH", - check: "Number", + type: 'input_value', + name: 'DEPTH', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], previousStatement: null, nextStatement: null, inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("create_box"), + colour: categoryColours['Scene'], + tooltip: getTooltip('create_box'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); // Set up the change handler. registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent, variableNamePrefix), + handleBlockChange(this, changeEvent, variableNamePrefix) ); // Add the mutator with toggle behaviour. addDoMutatorWithToggleBehavior(this); }, }; - Blockly.Blocks["create_sphere"] = { + Blockly.Blocks['create_sphere'] = { init: function () { - const variableNamePrefix = "sphere"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'sphere'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "create_sphere", - message0: translate("create_sphere"), + type: 'create_sphere', + message0: translate('create_sphere'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "COLOR", - check: ["Colour", "Material"], + type: 'input_value', + name: 'COLOR', + check: ['Colour', 'Material'], }, { - type: "input_value", - name: "DIAMETER_X", - check: "Number", + type: 'input_value', + name: 'DIAMETER_X', + check: 'Number', }, { - type: "input_value", - name: "DIAMETER_Y", - check: "Number", + type: 'input_value', + name: 'DIAMETER_Y', + check: 'Number', }, { - type: "input_value", - name: "DIAMETER_Z", - check: "Number", + type: 'input_value', + name: 'DIAMETER_Z', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], previousStatement: null, nextStatement: null, inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("create_sphere"), + colour: categoryColours['Scene'], + tooltip: getTooltip('create_sphere'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); // Set up the change handler. registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent, variableNamePrefix), + handleBlockChange(this, changeEvent, variableNamePrefix) ); // Add the mutator with toggle behaviour. addDoMutatorWithToggleBehavior(this); }, }; - Blockly.Blocks["create_cylinder"] = { + Blockly.Blocks['create_cylinder'] = { init: function () { - const variableNamePrefix = "cylinder"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'cylinder'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "create_cylinder", - message0: translate("create_cylinder"), + type: 'create_cylinder', + message0: translate('create_cylinder'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "COLOR", - check: ["Colour", "Material"], + type: 'input_value', + name: 'COLOR', + check: ['Colour', 'Material'], }, { - type: "input_value", - name: "HEIGHT", - check: "Number", + type: 'input_value', + name: 'HEIGHT', + check: 'Number', }, { - type: "input_value", - name: "DIAMETER_TOP", - check: "Number", + type: 'input_value', + name: 'DIAMETER_TOP', + check: 'Number', }, { - type: "input_value", - name: "DIAMETER_BOTTOM", - check: "Number", + type: 'input_value', + name: 'DIAMETER_BOTTOM', + check: 'Number', }, { - type: "input_value", - name: "TESSELLATIONS", - check: "Number", + type: 'input_value', + name: 'TESSELLATIONS', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], previousStatement: null, nextStatement: null, inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("create_cylinder"), + colour: categoryColours['Scene'], + tooltip: getTooltip('create_cylinder'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); // Set up the change handler. registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent, variableNamePrefix), + handleBlockChange(this, changeEvent, variableNamePrefix) ); // Add the mutator with toggle behaviour. addDoMutatorWithToggleBehavior(this); }, }; - Blockly.Blocks["create_capsule"] = { + Blockly.Blocks['create_capsule'] = { init: function () { - const variableNamePrefix = "capsule"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'capsule'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "create_capsule", - message0: translate("create_capsule"), + type: 'create_capsule', + message0: translate('create_capsule'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "COLOR", - check: ["Colour", "Material"], + type: 'input_value', + name: 'COLOR', + check: ['Colour', 'Material'], }, { - type: "input_value", - name: "DIAMETER", - check: "Number", + type: 'input_value', + name: 'DIAMETER', + check: 'Number', }, { - type: "input_value", - name: "HEIGHT", - check: "Number", + type: 'input_value', + name: 'HEIGHT', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], previousStatement: null, nextStatement: null, inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("create_capsule"), + colour: categoryColours['Scene'], + tooltip: getTooltip('create_capsule'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); // Set up the change handler. registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent, variableNamePrefix), + handleBlockChange(this, changeEvent, variableNamePrefix) ); // Add the mutator with toggle behaviour. addDoMutatorWithToggleBehavior(this); }, }; - Blockly.Blocks["create_plane"] = { + Blockly.Blocks['create_plane'] = { init: function () { - const variableNamePrefix = "plane"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'plane'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "create_plane", - message0: translate("create_plane"), + type: 'create_plane', + message0: translate('create_plane'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "COLOR", - check: ["Colour", "Material"], + type: 'input_value', + name: 'COLOR', + check: ['Colour', 'Material'], }, { - type: "input_value", - name: "WIDTH", - check: "Number", + type: 'input_value', + name: 'WIDTH', + check: 'Number', }, { - type: "input_value", - name: "HEIGHT", - check: "Number", + type: 'input_value', + name: 'HEIGHT', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], previousStatement: null, nextStatement: null, inputsInline: true, - colour: categoryColours["Scene"], - tooltip: getTooltip("create_plane"), + colour: categoryColours['Scene'], + tooltip: getTooltip('create_plane'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); // Set up the change handler. registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent, variableNamePrefix), + handleBlockChange(this, changeEvent, variableNamePrefix) ); // Add the mutator with toggle behaviour. addDoMutatorWithToggleBehavior(this); }, }; - Blockly.Blocks["control_particle_system"] = { + Blockly.Blocks['control_particle_system'] = { init: function () { this.jsonInit({ - type: "particle_system_control", - message0: translate("control_particle_system"), + type: 'particle_system_control', + message0: translate('control_particle_system'), args0: [ { - type: "field_variable", - name: "SYSTEM_NAME", + type: 'field_variable', + name: 'SYSTEM_NAME', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "ACTION", + type: 'field_dropdown', + name: 'ACTION', options: [ - getDropdownOption("start"), - getDropdownOption("stop"), - getDropdownOption("reset"), + getDropdownOption('start'), + getDropdownOption('stop'), + getDropdownOption('reset'), ], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("control_particle_system"), - helpUrl: "", + colour: categoryColours['Scene'], + tooltip: getTooltip('control_particle_system'), + helpUrl: '', }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; } diff --git a/blocks/sound.js b/blocks/sound.js index bb2a9e04..36a3c7cf 100644 --- a/blocks/sound.js +++ b/blocks/sound.js @@ -1,23 +1,14 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { nextVariableIndexes, handleBlockCreateEvent, getHelpUrlFor, registerBlockHandler, -} from "./blocks.js"; -import { - audioNames, - themeNames, - getSoundDisplayName, - getThemeDisplayName, -} from "../config.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; -import { announceToScreenReader } from "../main/input.js"; +} from './blocks.js'; +import { audioNames, themeNames, getSoundDisplayName, getThemeDisplayName } from '../config.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; +import { announceToScreenReader } from '../main/input.js'; // --------------------------------------------------------------------------- // Custom multi-line text field for ABC notation input @@ -25,60 +16,59 @@ import { announceToScreenReader } from "../main/input.js"; class FieldAbcInput extends Blockly.FieldTextInput { showEditor_() { - const current = this.getValue() || ""; + const current = this.getValue() || ''; // Build modal entirely with DOM APIs — no innerHTML, no user content in // attribute values — so there is no XSS surface. - const overlay = document.createElement("div"); + const overlay = document.createElement('div'); overlay.style.cssText = - "position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;" + - "display:flex;align-items:center;justify-content:center"; + 'position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;' + + 'display:flex;align-items:center;justify-content:center'; - const dialog = document.createElement("div"); - dialog.setAttribute("role", "dialog"); - dialog.setAttribute("aria-modal", "true"); - dialog.setAttribute("aria-labelledby", "abc-dlg-title"); + const dialog = document.createElement('div'); + dialog.setAttribute('role', 'dialog'); + dialog.setAttribute('aria-modal', 'true'); + dialog.setAttribute('aria-labelledby', 'abc-dlg-title'); dialog.style.cssText = - "background:#fff;border-radius:8px;padding:20px;display:flex;" + - "flex-direction:column;gap:12px;width:520px;max-width:90vw;" + - "box-shadow:0 8px 32px rgba(0,0,0,.3)"; + 'background:#fff;border-radius:8px;padding:20px;display:flex;' + + 'flex-direction:column;gap:12px;width:520px;max-width:90vw;' + + 'box-shadow:0 8px 32px rgba(0,0,0,.3)'; - const titleEl = document.createElement("h2"); - titleEl.id = "abc-dlg-title"; - titleEl.textContent = "ABC notation"; - titleEl.style.cssText = "margin:0;font:bold 15px/1 sans-serif;color:#333"; + const titleEl = document.createElement('h2'); + titleEl.id = 'abc-dlg-title'; + titleEl.textContent = 'ABC notation'; + titleEl.style.cssText = 'margin:0;font:bold 15px/1 sans-serif;color:#333'; - const hint = document.createElement("p"); - hint.textContent = - "Paste or type ABC notation. Ctrl+Enter to import, Escape to cancel."; - hint.style.cssText = "margin:0;font:12px/1.4 sans-serif;color:#666"; + const hint = document.createElement('p'); + hint.textContent = 'Paste or type ABC notation. Ctrl+Enter to import, Escape to cancel.'; + hint.style.cssText = 'margin:0;font:12px/1.4 sans-serif;color:#666'; - const textarea = document.createElement("textarea"); + const textarea = document.createElement('textarea'); textarea.rows = 14; textarea.spellcheck = false; - textarea.setAttribute("aria-label", "ABC notation"); + textarea.setAttribute('aria-label', 'ABC notation'); textarea.style.cssText = - "width:100%;box-sizing:border-box;font:13px/1.5 monospace;" + - "resize:vertical;border:1px solid #ccc;border-radius:4px;padding:8px"; + 'width:100%;box-sizing:border-box;font:13px/1.5 monospace;' + + 'resize:vertical;border:1px solid #ccc;border-radius:4px;padding:8px'; // Set via .value (textarea property), never via innerHTML textarea.value = current; - const buttons = document.createElement("div"); - buttons.style.cssText = "display:flex;justify-content:flex-end;gap:8px"; + const buttons = document.createElement('div'); + buttons.style.cssText = 'display:flex;justify-content:flex-end;gap:8px'; - const cancelBtn = document.createElement("button"); - cancelBtn.type = "button"; - cancelBtn.textContent = "Cancel"; + const cancelBtn = document.createElement('button'); + cancelBtn.type = 'button'; + cancelBtn.textContent = 'Cancel'; cancelBtn.style.cssText = - "padding:7px 18px;border:1px solid #ccc;border-radius:4px;" + - "cursor:pointer;background:#f5f5f5;font-size:13px"; + 'padding:7px 18px;border:1px solid #ccc;border-radius:4px;' + + 'cursor:pointer;background:#f5f5f5;font-size:13px'; - const importBtn = document.createElement("button"); - importBtn.type = "button"; - importBtn.textContent = "Import tune"; + const importBtn = document.createElement('button'); + importBtn.type = 'button'; + importBtn.textContent = 'Import tune'; importBtn.style.cssText = - "padding:7px 18px;border:none;border-radius:4px;cursor:pointer;" + - "background:#4a90d9;color:#fff;font-size:13px"; + 'padding:7px 18px;border:none;border-radius:4px;cursor:pointer;' + + 'background:#4a90d9;color:#fff;font-size:13px'; buttons.append(cancelBtn, importBtn); dialog.append(titleEl, hint, textarea, buttons); @@ -86,7 +76,7 @@ class FieldAbcInput extends Blockly.FieldTextInput { const close = () => { overlay.remove(); - document.removeEventListener("keydown", onKeyDown); + document.removeEventListener('keydown', onKeyDown); }; const confirm = () => { @@ -95,23 +85,23 @@ class FieldAbcInput extends Blockly.FieldTextInput { close(); }; - cancelBtn.addEventListener("click", close); - importBtn.addEventListener("click", confirm); + cancelBtn.addEventListener('click', close); + importBtn.addEventListener('click', confirm); // Clicking the dimmed backdrop dismisses without importing - overlay.addEventListener("click", (e) => { + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); const onKeyDown = (e) => { - if (e.key === "Escape") { + if (e.key === 'Escape') { e.preventDefault(); close(); - } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { + } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); confirm(); } }; - document.addEventListener("keydown", onKeyDown); + document.addEventListener('keydown', onKeyDown); document.body.append(overlay); // Defer focus so the overlay is in the DOM before the browser moves focus @@ -120,15 +110,15 @@ class FieldAbcInput extends Blockly.FieldTextInput { getText_() { const val = this.getValue(); - if (!val) return "click to enter ABC…"; + if (!val) return 'click to enter ABC…'; const titleLine = String(val) - .split("\n") - .find((l) => l.trim().startsWith("T:")); - return titleLine ? titleLine.trim().slice(2).trim() : "ABC"; + .split('\n') + .find((l) => l.trim().startsWith('T:')); + return titleLine ? titleLine.trim().slice(2).trim() : 'ABC'; } getAriaTypeName() { - return "ABC notation"; + return 'ABC notation'; } } @@ -139,33 +129,45 @@ class FieldAbcInput extends Blockly.FieldTextInput { const ABC_BASE_MIDI = { C: 60, D: 62, E: 64, F: 65, G: 67, A: 69, B: 71 }; const ABC_KEY_ACCIDENTALS = { - C: {}, Am: {}, - G: { F: 1 }, Em: { F: 1 }, - D: { F: 1, C: 1 }, Bm: { F: 1, C: 1 }, - A: { F: 1, C: 1, G: 1 }, "F#m": { F: 1, C: 1, G: 1 }, - E: { F: 1, C: 1, G: 1, D: 1 }, "C#m": { F: 1, C: 1, G: 1, D: 1 }, - B: { F: 1, C: 1, G: 1, D: 1, A: 1 }, "G#m": { F: 1, C: 1, G: 1, D: 1, A: 1 }, - "F#": { F: 1, C: 1, G: 1, D: 1, A: 1, E: 1 }, "D#m": { F: 1, C: 1, G: 1, D: 1, A: 1, E: 1 }, - "C#": { F: 1, C: 1, G: 1, D: 1, A: 1, E: 1, B: 1 }, - F: { B: -1 }, Dm: { B: -1 }, - Bb: { B: -1, E: -1 }, Gm: { B: -1, E: -1 }, - Eb: { B: -1, E: -1, A: -1 }, Cm: { B: -1, E: -1, A: -1 }, - Ab: { B: -1, E: -1, A: -1, D: -1 }, Fm: { B: -1, E: -1, A: -1, D: -1 }, - Db: { B: -1, E: -1, A: -1, D: -1, G: -1 }, Bbm: { B: -1, E: -1, A: -1, D: -1, G: -1 }, + C: {}, + Am: {}, + G: { F: 1 }, + Em: { F: 1 }, + D: { F: 1, C: 1 }, + Bm: { F: 1, C: 1 }, + A: { F: 1, C: 1, G: 1 }, + 'F#m': { F: 1, C: 1, G: 1 }, + E: { F: 1, C: 1, G: 1, D: 1 }, + 'C#m': { F: 1, C: 1, G: 1, D: 1 }, + B: { F: 1, C: 1, G: 1, D: 1, A: 1 }, + 'G#m': { F: 1, C: 1, G: 1, D: 1, A: 1 }, + 'F#': { F: 1, C: 1, G: 1, D: 1, A: 1, E: 1 }, + 'D#m': { F: 1, C: 1, G: 1, D: 1, A: 1, E: 1 }, + 'C#': { F: 1, C: 1, G: 1, D: 1, A: 1, E: 1, B: 1 }, + F: { B: -1 }, + Dm: { B: -1 }, + Bb: { B: -1, E: -1 }, + Gm: { B: -1, E: -1 }, + Eb: { B: -1, E: -1, A: -1 }, + Cm: { B: -1, E: -1, A: -1 }, + Ab: { B: -1, E: -1, A: -1, D: -1 }, + Fm: { B: -1, E: -1, A: -1, D: -1 }, + Db: { B: -1, E: -1, A: -1, D: -1, G: -1 }, + Bbm: { B: -1, E: -1, A: -1, D: -1, G: -1 }, Gb: { B: -1, E: -1, A: -1, D: -1, G: -1, C: -1 }, Cb: { B: -1, E: -1, A: -1, D: -1, G: -1, C: -1, F: -1 }, }; function abcParseFraction(str) { - const parts = str.split("/"); + const parts = str.split('/'); if (parts.length === 2) return parseInt(parts[0]) / parseInt(parts[1]); return parseFloat(str) || 1; } function parseAbc(abcText) { - const lines = abcText.split("\n"); - let title = ""; - let composer = ""; + const lines = abcText.split('\n'); + let title = ''; + let composer = ''; let lFraction = 0.125; let bpm = 120; let keyAcc = {}; @@ -178,14 +180,14 @@ function parseAbc(abcText) { if (hm) { const [, key, value] = hm; const v = value.trim(); - if (key === "T") title = v; - else if (key === "C") composer = v; - else if (key === "L") lFraction = abcParseFraction(v); - else if (key === "Q") { + if (key === 'T') title = v; + else if (key === 'C') composer = v; + else if (key === 'L') lFraction = abcParseFraction(v); + else if (key === 'Q') { const qm = v.match(/(\d+)$/); if (qm) bpm = parseInt(qm[1]); - } else if (key === "K") { - keyAcc = ABC_KEY_ACCIDENTALS[v.split(" ")[0]] || {}; + } else if (key === 'K') { + keyAcc = ABC_KEY_ACCIDENTALS[v.split(' ')[0]] || {}; } } else { musicLines.push(t); @@ -199,40 +201,67 @@ function parseAbc(abcText) { } function abcParseSections(musicLines, noteBase, keyAcc) { - const text = musicLines.join(" ").replace(/%[^\n]*/g, ""); + const text = musicLines.join(' ').replace(/%[^\n]*/g, ''); const tokens = []; let i = 0; while (i < text.length) { const c = text[i]; - if (/\s/.test(c)) { i++; continue; } + if (/\s/.test(c)) { + i++; + continue; + } - if (c === "|") { + if (c === '|') { const n = text[i + 1]; - if (n === ":") { tokens.push({ t: "rs" }); i += 2; } - else if (n === "]") { tokens.push({ t: "bar" }); i += 2; } - else if (n === "1") { tokens.push({ t: "v1" }); i += 2; } - else if (n === "2") { tokens.push({ t: "v2" }); i += 2; } - else if (n === "|") { tokens.push({ t: "bar" }); i += 2; } - else { tokens.push({ t: "bar" }); i++; } - } else if (c === ":") { - if (text[i + 1] === "|") { - tokens.push({ t: "re" }); + if (n === ':') { + tokens.push({ t: 'rs' }); + i += 2; + } else if (n === ']') { + tokens.push({ t: 'bar' }); + i += 2; + } else if (n === '1') { + tokens.push({ t: 'v1' }); + i += 2; + } else if (n === '2') { + tokens.push({ t: 'v2' }); + i += 2; + } else if (n === '|') { + tokens.push({ t: 'bar' }); + i += 2; + } else { + tokens.push({ t: 'bar' }); + i++; + } + } else if (c === ':') { + if (text[i + 1] === '|') { + tokens.push({ t: 're' }); i += 2; // Look ahead for volta markers (e.g. :|[1 or :|1) while (i < text.length && /\s/.test(text[i])) i++; - if (text[i] === "[" && (text[i + 1] === "1" || text[i + 1] === "2")) { - tokens.push({ t: text[i + 1] === "1" ? "v1" : "v2" }); i += 2; - } else if (text[i] === "1") { - tokens.push({ t: "v1" }); i++; - } else if (text[i] === "2") { - tokens.push({ t: "v2" }); i++; + if (text[i] === '[' && (text[i + 1] === '1' || text[i + 1] === '2')) { + tokens.push({ t: text[i + 1] === '1' ? 'v1' : 'v2' }); + i += 2; + } else if (text[i] === '1') { + tokens.push({ t: 'v1' }); + i++; + } else if (text[i] === '2') { + tokens.push({ t: 'v2' }); + i++; } - } else { i++; } - } else if (c === "[") { - if (text[i + 1] === "1") { tokens.push({ t: "v1" }); i += 2; } - else if (text[i + 1] === "2") { tokens.push({ t: "v2" }); i += 2; } - else { i++; } + } else { + i++; + } + } else if (c === '[') { + if (text[i + 1] === '1') { + tokens.push({ t: 'v1' }); + i += 2; + } else if (text[i + 1] === '2') { + tokens.push({ t: 'v2' }); + i += 2; + } else { + i++; + } } else { const sub = text.slice(i); const m = sub.match(/^([_^=]*)([A-Ga-gz])([',]*)(\d*)(\/\d*)?/); @@ -240,32 +269,34 @@ function abcParseSections(musicLines, noteBase, keyAcc) { const [full, accStr, letter, octStr, numStr, denomStr] = m; const num = numStr ? parseInt(numStr) : 1; let denom = 1; - if (denomStr) denom = denomStr === "/" ? 2 : parseInt(denomStr.slice(1)) || 2; + if (denomStr) denom = denomStr === '/' ? 2 : parseInt(denomStr.slice(1)) || 2; const beats = noteBase * (num / denom); const lo = letter.toLowerCase(); - if (lo === "z" || lo === "x") { - tokens.push({ t: "rest", beats }); + if (lo === 'z' || lo === 'x') { + tokens.push({ t: 'rest', beats }); } else { const base = letter.toUpperCase(); let midi = ABC_BASE_MIDI[base] + (letter !== letter.toUpperCase() ? 12 : 0); for (const oc of octStr) { if (oc === "'") midi += 12; - else if (oc === ",") midi -= 12; + else if (oc === ',') midi -= 12; } let acc = 0; let hasAcc = false; if (accStr) { hasAcc = true; for (const a of accStr) { - if (a === "^") acc++; - else if (a === "_") acc--; + if (a === '^') acc++; + else if (a === '_') acc--; } } - tokens.push({ t: "note", base, midi, beats, hasAcc, acc }); + tokens.push({ t: 'note', base, midi, beats, hasAcc, acc }); } i += full.length; - } else { i++; } + } else { + i++; + } } } @@ -274,7 +305,7 @@ function abcParseSections(musicLines, noteBase, keyAcc) { let currentBars = []; let currentBar = []; let barAcc = {}; - let state = "plain"; + let state = 'plain'; let repeatBars = null; let volta1Bars = null; @@ -302,15 +333,15 @@ function abcParseSections(musicLines, noteBase, keyAcc) { for (let tokIdx = 0; tokIdx < tokens.length; tokIdx++) { const tok = tokens[tokIdx]; switch (tok.t) { - case "bar": + case 'bar': flushBar(); break; - case "rs": + case 'rs': flushBar(); - if (state === "volta" && repeatBars !== null) { + if (state === 'volta' && repeatBars !== null) { // Finalize the preceding volta before starting a new repeat section sections.push({ - type: "volta", + type: 'volta', commonBars: repeatBars, firstBars: volta1Bars || [], secondBars: [...currentBars], @@ -318,29 +349,29 @@ function abcParseSections(musicLines, noteBase, keyAcc) { repeatBars = null; volta1Bars = null; } else if (currentBars.length > 0) { - sections.push({ type: "plain", bars: [...currentBars] }); + sections.push({ type: 'plain', bars: [...currentBars] }); } currentBars = []; - state = "repeat"; + state = 'repeat'; break; - case "re": + case 're': flushBar(); - if (state === "repeat" || state === "plain") { + if (state === 'repeat' || state === 'plain') { const nextTok = tokens[tokIdx + 1]; - if (nextTok?.t === "v1" || nextTok?.t === "v2") { + if (nextTok?.t === 'v1' || nextTok?.t === 'v2') { // :| immediately followed by [1 or [2 — the body so far is the common part repeatBars = [...currentBars]; volta1Bars = []; currentBars = []; - state = "volta"; + state = 'volta'; } else if (currentBars.length > 0) { - sections.push({ type: "repeat", bars: [...currentBars] }); + sections.push({ type: 'repeat', bars: [...currentBars] }); currentBars = []; - state = "plain"; + state = 'plain'; } - } else if (state === "volta") { + } else if (state === 'volta') { const nextTok = tokens[tokIdx + 1]; - if (nextTok?.t === "v1" || nextTok?.t === "v2") { + if (nextTok?.t === 'v1' || nextTok?.t === 'v2') { // Transitioning between endings — accumulate into firstBars volta1Bars = [...(volta1Bars || []), ...currentBars]; currentBars = []; @@ -348,99 +379,100 @@ function abcParseSections(musicLines, noteBase, keyAcc) { // Closing :| of last ending — leave currentBars for secondBars } break; - case "v1": + case 'v1': flushBar(); - if (state !== "volta") { + if (state !== 'volta') { // v1 appearing without a preceding :| — current bars are the common part repeatBars = [...currentBars]; volta1Bars = []; currentBars = []; - state = "volta"; + state = 'volta'; } // already in volta (set by :| lookahead above) — leave repeatBars alone break; - case "v2": + case 'v2': flushBar(); - if (state !== "volta") { + if (state !== 'volta') { volta1Bars = [...currentBars]; repeatBars = repeatBars || []; - state = "volta"; + state = 'volta'; } currentBars = []; break; - case "note": + case 'note': addNote(tok); break; - case "rest": + case 'rest': currentBar.push({ midi: null, beats: tok.beats }); break; } } flushBar(); - if (state === "volta" && repeatBars !== null) { + if (state === 'volta' && repeatBars !== null) { sections.push({ - type: "volta", + type: 'volta', commonBars: repeatBars, firstBars: volta1Bars || [], secondBars: [...currentBars], }); } else if (currentBars.length > 0) { - sections.push({ type: "plain", bars: currentBars }); + sections.push({ type: 'plain', bars: currentBars }); } return sections; } - function buildPlayBlock(ws, bars, meshName) { const validBars = (bars || []).filter((b) => b && b.length > 0); if (validBars.length === 0) return null; - const outerList = ws.newBlock("lists_create_with"); + const outerList = ws.newBlock('lists_create_with'); outerList.loadExtraState({ itemCount: validBars.length }); outerList.initSvg(); outerList.render(); validBars.forEach((bar, barIdx) => { - const barBlock = ws.newBlock("lists_create_with"); + const barBlock = ws.newBlock('lists_create_with'); barBlock.loadExtraState({ itemCount: bar.length }); barBlock.setInputsInline(true); barBlock.initSvg(); barBlock.render(); bar.forEach((note, noteIdx) => { - const noteBlock = ws.newBlock("note"); + const noteBlock = ws.newBlock('note'); noteBlock.initSvg(); noteBlock.render(); if (note.midi === null) { - const restBlock = ws.newBlock("rest"); + const restBlock = ws.newBlock('rest'); restBlock.initSvg(); restBlock.render(); - noteBlock.getInput("PITCH").connection.connect(restBlock.outputConnection); + noteBlock.getInput('PITCH').connection.connect(restBlock.outputConnection); } else { - noteBlock.getInput("PITCH").connection.connect(makeNumBlock(ws, note.midi).outputConnection); + noteBlock + .getInput('PITCH') + .connection.connect(makeNumBlock(ws, note.midi).outputConnection); } const dur = Math.round(note.beats * 1000) / 1000; - noteBlock.getInput("DURATION").connection.connect(makeNumBlock(ws, dur).outputConnection); + noteBlock.getInput('DURATION').connection.connect(makeNumBlock(ws, dur).outputConnection); barBlock.getInput(`ADD${noteIdx}`).connection.connect(noteBlock.outputConnection); }); outerList.getInput(`ADD${barIdx}`).connection.connect(barBlock.outputConnection); }); - const playBlock = ws.newBlock("play_tune_notes"); + const playBlock = ws.newBlock('play_tune_notes'); playBlock.initSvg(); playBlock.render(); - const instBlock = ws.newBlock("instrument"); - instBlock.setFieldValue("default", "INSTRUMENT_TYPE"); + const instBlock = ws.newBlock('instrument'); + instBlock.setFieldValue('default', 'INSTRUMENT_TYPE'); instBlock.initSvg(); instBlock.render(); - if (meshName) playBlock.setFieldValue(meshName, "MESH_NAME"); - playBlock.getInput("INSTRUMENT").connection.connect(instBlock.outputConnection); - playBlock.getInput("NOTES").connection.connect(outerList.outputConnection); + if (meshName) playBlock.setFieldValue(meshName, 'MESH_NAME'); + playBlock.getInput('INSTRUMENT').connection.connect(instBlock.outputConnection); + playBlock.getInput('NOTES').connection.connect(outerList.outputConnection); return playBlock; } @@ -451,8 +483,8 @@ function buildPlayBlock(ws, bars, meshName) { // value inputs connect to blocks with outputConnection, not previousConnection. function propagateMesh(block, meshName) { while (block) { - if (block.getField("MESH_NAME")) { - block.setFieldValue(meshName, "MESH_NAME"); + if (block.getField('MESH_NAME')) { + block.setFieldValue(meshName, 'MESH_NAME'); } for (const input of block.inputList) { const child = input.connection?.targetBlock(); @@ -467,8 +499,8 @@ function propagateInstrument(block, instrumentState) { delete state.id; delete state.shadow; while (block) { - if (block.type === "play_tune_notes") { - const instrConn = block.getInput("INSTRUMENT")?.connection; + if (block.type === 'play_tune_notes') { + const instrConn = block.getInput('INSTRUMENT')?.connection; if (instrConn) { const existing = instrConn.targetBlock(); if (existing && !existing.isShadow()) existing.dispose(false); @@ -485,8 +517,8 @@ function propagateInstrument(block, instrumentState) { } function makeNumBlock(ws, value) { - const b = ws.newBlock("math_number"); - b.setFieldValue(String(value), "NUM"); + const b = ws.newBlock('math_number'); + b.setFieldValue(String(value), 'NUM'); b.initSvg(); b.render(); return b; @@ -494,84 +526,84 @@ function makeNumBlock(ws, value) { function abcBuildDoBlocks(ws, doInput, sections, bpm, meshName) { const hasBars = sections.some((s) => - s.type === "plain" ? s.bars?.length > 0 : - s.type === "repeat" ? s.bars?.length > 0 : - (s.commonBars?.length > 0 || s.firstBars?.length > 0 || s.secondBars?.length > 0) + s.type === 'plain' + ? s.bars?.length > 0 + : s.type === 'repeat' + ? s.bars?.length > 0 + : s.commonBars?.length > 0 || s.firstBars?.length > 0 || s.secondBars?.length > 0 ); if (!hasBars) { - console.warn("[play_tune] no bars found in ABC"); + console.warn('[play_tune] no bars found in ABC'); return; } const speed = Math.round((bpm / 60) * 1000) / 1000; - const speedBlock = ws.newBlock("set_music_speed"); + const speedBlock = ws.newBlock('set_music_speed'); speedBlock.initSvg(); speedBlock.render(); - speedBlock.getInput("SPEED").connection.connect(makeNumBlock(ws, speed).outputConnection); + speedBlock.getInput('SPEED').connection.connect(makeNumBlock(ws, speed).outputConnection); doInput.connection.connect(speedBlock.previousConnection); let prevNext = speedBlock.nextConnection; for (const section of sections) { - if (section.type === "plain") { + if (section.type === 'plain') { const playBlock = buildPlayBlock(ws, section.bars, meshName); if (!playBlock) continue; prevNext.connect(playBlock.previousConnection); prevNext = playBlock.nextConnection; - - } else if (section.type === "repeat") { - const repeatBlock = ws.newBlock("controls_repeat_ext"); + } else if (section.type === 'repeat') { + const repeatBlock = ws.newBlock('controls_repeat_ext'); repeatBlock.initSvg(); repeatBlock.render(); - repeatBlock.getInput("TIMES").connection.connect(makeNumBlock(ws, 2).outputConnection); + repeatBlock.getInput('TIMES').connection.connect(makeNumBlock(ws, 2).outputConnection); const playBlock = buildPlayBlock(ws, section.bars, meshName); if (playBlock) { - repeatBlock.getInput("DO").connection.connect(playBlock.previousConnection); + repeatBlock.getInput('DO').connection.connect(playBlock.previousConnection); } prevNext.connect(repeatBlock.previousConnection); prevNext = repeatBlock.nextConnection; - - } else if (section.type === "volta") { + } else if (section.type === 'volta') { const varModel = - ws.getVariableMap().getVariable("_repeat", "") || ws.createVariable("_repeat"); + ws.getVariableMap().getVariable('_repeat', '') || ws.createVariable('_repeat'); - const forBlock = ws.newBlock("controls_for"); + const forBlock = ws.newBlock('controls_for'); forBlock.initSvg(); forBlock.render(); - forBlock.getField("VAR").setValue(varModel.getId()); - forBlock.getInput("FROM").connection.connect(makeNumBlock(ws, 1).outputConnection); - forBlock.getInput("TO").connection.connect(makeNumBlock(ws, 2).outputConnection); - forBlock.getInput("BY").connection.connect(makeNumBlock(ws, 1).outputConnection); + forBlock.getField('VAR').setValue(varModel.getId()); + forBlock.getInput('FROM').connection.connect(makeNumBlock(ws, 1).outputConnection); + forBlock.getInput('TO').connection.connect(makeNumBlock(ws, 2).outputConnection); + forBlock.getInput('BY').connection.connect(makeNumBlock(ws, 1).outputConnection); - const ifBlock = ws.newBlock("controls_if"); + const ifBlock = ws.newBlock('controls_if'); ifBlock.loadExtraState({ hasElse: true }); ifBlock.initSvg(); ifBlock.render(); - const compareBlock = ws.newBlock("logic_compare"); - compareBlock.setFieldValue("EQ", "OP"); + const compareBlock = ws.newBlock('logic_compare'); + compareBlock.setFieldValue('EQ', 'OP'); compareBlock.initSvg(); compareBlock.render(); - const varGetBlock = ws.newBlock("variables_get"); - varGetBlock.getField("VAR").setValue(varModel.getId()); + const varGetBlock = ws.newBlock('variables_get'); + varGetBlock.getField('VAR').setValue(varModel.getId()); varGetBlock.initSvg(); varGetBlock.render(); - compareBlock.getInput("A").connection.connect(varGetBlock.outputConnection); - compareBlock.getInput("B").connection.connect(makeNumBlock(ws, 1).outputConnection); - ifBlock.getInput("IF0").connection.connect(compareBlock.outputConnection); + compareBlock.getInput('A').connection.connect(varGetBlock.outputConnection); + compareBlock.getInput('B').connection.connect(makeNumBlock(ws, 1).outputConnection); + ifBlock.getInput('IF0').connection.connect(compareBlock.outputConnection); const firstPlay = buildPlayBlock(ws, section.firstBars, meshName); if (firstPlay) { - ifBlock.getInput("DO0").connection.connect(firstPlay.previousConnection); + ifBlock.getInput('DO0').connection.connect(firstPlay.previousConnection); } const secondPlay = buildPlayBlock(ws, section.secondBars, meshName); if (secondPlay) { - ifBlock.getInput("ELSE").connection.connect(secondPlay.previousConnection); + ifBlock.getInput('ELSE').connection.connect(secondPlay.previousConnection); } const commonPlay = buildPlayBlock(ws, section.commonBars, meshName); @@ -579,7 +611,7 @@ function abcBuildDoBlocks(ws, doInput, sections, bpm, meshName) { if (commonPlay) { commonPlay.nextConnection.connect(ifBlock.previousConnection); } - forBlock.getInput("DO").connection.connect(doFirst.previousConnection); + forBlock.getInput('DO').connection.connect(doFirst.previousConnection); prevNext.connect(forBlock.previousConnection); prevNext = forBlock.nextConnection; @@ -588,305 +620,289 @@ function abcBuildDoBlocks(ws, doInput, sections, bpm, meshName) { } export function defineSoundBlocks() { - Blockly.Blocks["play_theme"] = { + Blockly.Blocks['play_theme'] = { init: function () { - const variableNamePrefix = "sound"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'sound'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "play_theme", - message0: translate("play_theme"), + type: 'play_theme', + message0: translate('play_theme'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "field_dropdown", - name: "THEME_NAME", + type: 'field_dropdown', + name: 'THEME_NAME', options: function () { return themeNames.map((name) => [getThemeDisplayName(name), name]); }, }, { - type: "input_dummy", - name: "MESH_INPUT", + type: 'input_dummy', + name: 'MESH_INPUT', }, { - type: "input_value", - name: "SPEED", + type: 'input_value', + name: 'SPEED', value: 1, min: 0.1, max: 3, precision: 0.1, }, { - type: "input_value", - name: "VOLUME", + type: 'input_value', + name: 'VOLUME', value: 1, min: 0, max: 1, precision: 0.1, }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("ONCE"), getDropdownOption("LOOP")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('ONCE'), getDropdownOption('LOOP')], }, { - type: "field_dropdown", - name: "ASYNC", - options: [getDropdownOption("START"), getDropdownOption("AWAIT")], + type: 'field_dropdown', + name: 'ASYNC', + options: [getDropdownOption('START'), getDropdownOption('AWAIT')], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("play_theme"), - extensions: ["dynamic_mesh_dropdown"], + colour: categoryColours['Sound'], + tooltip: getTooltip('play_theme'), + extensions: ['dynamic_mesh_dropdown'], }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); registerBlockHandler(this, (changeEvent) => { - handleBlockCreateEvent( - this, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ); + handleBlockCreateEvent(this, changeEvent, variableNamePrefix, nextVariableIndexes); }); }, }; - Blockly.Blocks["play_sound"] = { + Blockly.Blocks['play_sound'] = { init: function () { - const variableNamePrefix = "sound"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'sound'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "play_sound", - message0: translate("play_sound"), + type: 'play_sound', + message0: translate('play_sound'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "field_dropdown", - name: "SOUND_NAME", + type: 'field_dropdown', + name: 'SOUND_NAME', options: function () { return audioNames.map((name) => [getSoundDisplayName(name), name]); }, }, { - type: "input_dummy", - name: "MESH_INPUT", // Dummy input for the dropdown + type: 'input_dummy', + name: 'MESH_INPUT', // Dummy input for the dropdown }, { - type: "input_value", - name: "SPEED", + type: 'input_value', + name: 'SPEED', value: 1, min: 0.1, max: 3, precision: 0.1, }, { - type: "input_value", - name: "VOLUME", + type: 'input_value', + name: 'VOLUME', value: 1, min: 0, max: 1, precision: 0.1, }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("ONCE"), getDropdownOption("LOOP")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('ONCE'), getDropdownOption('LOOP')], }, { - type: "field_dropdown", - name: "ASYNC", - options: [getDropdownOption("START"), getDropdownOption("AWAIT")], + type: 'field_dropdown', + name: 'ASYNC', + options: [getDropdownOption('START'), getDropdownOption('AWAIT')], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("play_sound"), - extensions: ["dynamic_mesh_dropdown"], // Attach the extension + colour: categoryColours['Sound'], + tooltip: getTooltip('play_sound'), + extensions: ['dynamic_mesh_dropdown'], // Attach the extension }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); registerBlockHandler(this, (changeEvent) => { - handleBlockCreateEvent( - this, - changeEvent, - variableNamePrefix, - nextVariableIndexes, - ); + handleBlockCreateEvent(this, changeEvent, variableNamePrefix, nextVariableIndexes); }); }, }; - Blockly.Blocks["stop_all_sounds"] = { + Blockly.Blocks['stop_all_sounds'] = { init: function () { this.jsonInit({ - message0: translate("stop_all_sounds"), + message0: translate('stop_all_sounds'), previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("stop_all_sounds"), + colour: categoryColours['Sound'], + tooltip: getTooltip('stop_all_sounds'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; - Blockly.Blocks["midi_note"] = { + Blockly.Blocks['midi_note'] = { init: function () { this.jsonInit({ - type: "midi_note", - message0: translate("midi_note"), + type: 'midi_note', + message0: translate('midi_note'), args0: [ { - type: "input_value", - name: "NOTE", - check: "Number", + type: 'input_value', + name: 'NOTE', + check: 'Number', }, ], inputsInline: true, - output: "Number", - colour: categoryColours["Sound"], - tooltip: getTooltip("midi_note"), + output: 'Number', + colour: categoryColours['Sound'], + tooltip: getTooltip('midi_note'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; - Blockly.Blocks["note"] = { + Blockly.Blocks['note'] = { init: function () { this.jsonInit({ - type: "note", - message0: translate("note"), + type: 'note', + message0: translate('note'), args0: [ { - type: "input_value", - name: "PITCH", - check: "Number", + type: 'input_value', + name: 'PITCH', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, ], inputsInline: true, - output: "NoteEvent", - colour: categoryColours["Sound"], - tooltip: getTooltip("note"), + output: 'NoteEvent', + colour: categoryColours['Sound'], + tooltip: getTooltip('note'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); - this.getInput("PITCH").setAriaLabelProvider("pitch"); - this.getInput("DURATION").setAriaLabelProvider("duration"); + this.setStyle('sound_blocks'); + this.getInput('PITCH').setAriaLabelProvider('pitch'); + this.getInput('DURATION').setAriaLabelProvider('duration'); }, }; - Blockly.Blocks["rest"] = { + Blockly.Blocks['rest'] = { init: function () { this.jsonInit({ - type: "rest", - message0: translate("rest"), + type: 'rest', + message0: translate('rest'), inputsInline: true, - output: "Number", - colour: categoryColours["Sound"], - tooltip: getTooltip("rest"), + output: 'Number', + colour: categoryColours['Sound'], + tooltip: getTooltip('rest'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; - Blockly.Blocks["play_tune_notes"] = { + Blockly.Blocks['play_tune_notes'] = { init: function () { this.jsonInit({ - type: "play_tune_notes", - message0: translate("play_tune_notes"), + type: 'play_tune_notes', + message0: translate('play_tune_notes'), args0: [ { - type: "input_dummy", - name: "MESH_INPUT", + type: 'input_dummy', + name: 'MESH_INPUT', }, { - type: "input_value", - name: "INSTRUMENT", - check: "Instrument", + type: 'input_value', + name: 'INSTRUMENT', + check: 'Instrument', }, { - type: "input_value", - name: "NOTES", - check: "Array", + type: 'input_value', + name: 'NOTES', + check: 'Array', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("play_tune_notes"), - extensions: ["dynamic_mesh_dropdown"], + colour: categoryColours['Sound'], + tooltip: getTooltip('play_tune_notes'), + extensions: ['dynamic_mesh_dropdown'], }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); this.setOnChange(function (changeEvent) { if (this.workspace?.isFlyout) return; if (changeEvent.type !== Blockly.Events.BLOCK_MOVE) return; // When a bar (inner list) is connected to the outer tune list, make it inline. - const outerList = this.getInputTargetBlock("NOTES"); + const outerList = this.getInputTargetBlock('NOTES'); if (outerList && changeEvent.newParentId === outerList.id) { const movedBlock = this.workspace.getBlockById(changeEvent.blockId); - if (movedBlock?.type === "lists_create_with") { + if (movedBlock?.type === 'lists_create_with') { movedBlock.setInputsInline(true); } } // Restore the default bar structure when NOTES is disconnected. - if ( - changeEvent.oldParentId !== this.id || - changeEvent.oldInputName !== "NOTES" - ) - return; - if (this.getInputTargetBlock("NOTES")) return; + if (changeEvent.oldParentId !== this.id || changeEvent.oldInputName !== 'NOTES') return; + if (this.getInputTargetBlock('NOTES')) return; const ws = this.workspace; const listBlock = Blockly.serialization.blocks.append( { - type: "lists_create_with", + type: 'lists_create_with', extraState: { itemCount: 1 }, inputs: { ADD0: { block: { - type: "lists_create_with", + type: 'lists_create_with', extraState: { itemCount: 1 }, inline: true, inputs: { ADD0: { block: { - type: "note", + type: 'note', inputs: { PITCH: { - shadow: { type: "math_number", fields: { NUM: 60 } }, + shadow: { type: 'math_number', fields: { NUM: 60 } }, }, DURATION: { - shadow: { type: "math_number", fields: { NUM: 0.5 } }, + shadow: { type: 'math_number', fields: { NUM: 0.5 } }, }, }, }, @@ -896,217 +912,215 @@ export function defineSoundBlocks() { }, }, }, - ws, + ws ); - const notesConn = this.getInput("NOTES")?.connection; - if (notesConn && listBlock?.outputConnection) - notesConn.connect(listBlock.outputConnection); + const notesConn = this.getInput('NOTES')?.connection; + if (notesConn && listBlock?.outputConnection) notesConn.connect(listBlock.outputConnection); }); }, }; - Blockly.Blocks["set_music_speed"] = { + Blockly.Blocks['set_music_speed'] = { init: function () { this.jsonInit({ - type: "set_music_speed", - message0: translate("set_music_speed"), + type: 'set_music_speed', + message0: translate('set_music_speed'), args0: [ { - type: "input_dummy", - name: "MESH_INPUT", + type: 'input_dummy', + name: 'MESH_INPUT', }, { - type: "input_value", - name: "SPEED", - check: "Number", + type: 'input_value', + name: 'SPEED', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("set_music_speed"), - extensions: ["dynamic_mesh_dropdown"], + colour: categoryColours['Sound'], + tooltip: getTooltip('set_music_speed'), + extensions: ['dynamic_mesh_dropdown'], }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; - Blockly.Blocks["play_notes"] = { + Blockly.Blocks['play_notes'] = { init: function () { this.jsonInit({ - type: "play_notes", - message0: translate("play_notes"), + type: 'play_notes', + message0: translate('play_notes'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, }, { - type: "input_value", - name: "NOTES", - check: "Array", + type: 'input_value', + name: 'NOTES', + check: 'Array', }, { - type: "input_value", - name: "DURATIONS", - check: "Array", + type: 'input_value', + name: 'DURATIONS', + check: 'Array', }, { - type: "input_value", - name: "INSTRUMENT", - check: "Instrument", + type: 'input_value', + name: 'INSTRUMENT', + check: 'Instrument', }, { - type: "field_dropdown", - name: "ASYNC", - options: [getDropdownOption("START"), getDropdownOption("AWAIT")], + type: 'field_dropdown', + name: 'ASYNC', + options: [getDropdownOption('START'), getDropdownOption('AWAIT')], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("play_notes"), + colour: categoryColours['Sound'], + tooltip: getTooltip('play_notes'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; - Blockly.Blocks["set_scene_bpm"] = { + Blockly.Blocks['set_scene_bpm'] = { init: function () { this.jsonInit({ - type: "set_scene_bpm", - message0: translate("set_scene_bpm"), + type: 'set_scene_bpm', + message0: translate('set_scene_bpm'), args0: [ { - type: "input_value", - name: "BPM", - check: "Number", + type: 'input_value', + name: 'BPM', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], // Assuming "Sound" category - tooltip: getTooltip("set_scene_bpm"), + colour: categoryColours['Sound'], // Assuming "Sound" category + tooltip: getTooltip('set_scene_bpm'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; - Blockly.Blocks["set_mesh_bpm"] = { + Blockly.Blocks['set_mesh_bpm'] = { init: function () { this.jsonInit({ - type: "set_mesh_bpm", - message0: translate("set_mesh_bpm"), + type: 'set_mesh_bpm', + message0: translate('set_mesh_bpm'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, }, { - type: "input_value", - name: "BPM", - check: "Number", + type: 'input_value', + name: 'BPM', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], // Assuming "Sound" category - tooltip: getTooltip("set_mesh_bpm"), + colour: categoryColours['Sound'], // Assuming "Sound" category + tooltip: getTooltip('set_mesh_bpm'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; - Blockly.Blocks["create_instrument"] = { + Blockly.Blocks['create_instrument'] = { init: function () { - const variableNamePrefix = "instrument"; - let nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'instrument'; + let nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "create_instrument", - message0: translate("create_instrument"), + type: 'create_instrument', + message0: translate('create_instrument'), args0: [ { - type: "field_variable", - name: "INSTRUMENT", + type: 'field_variable', + name: 'INSTRUMENT', variable: nextVariableName, }, { - type: "field_dropdown", - name: "TYPE", + type: 'field_dropdown', + name: 'TYPE', options: [ - getDropdownOption("sine"), - getDropdownOption("square"), - getDropdownOption("sawtooth"), - getDropdownOption("triangle"), + getDropdownOption('sine'), + getDropdownOption('square'), + getDropdownOption('sawtooth'), + getDropdownOption('triangle'), ], }, { - type: "input_value", - name: "VOLUME", - check: "Number", + type: 'input_value', + name: 'VOLUME', + check: 'Number', }, { - type: "field_dropdown", - name: "EFFECT", + type: 'field_dropdown', + name: 'EFFECT', options: [ - getDropdownOption("none"), - getDropdownOption("tremolo"), - getDropdownOption("vibrato"), - getDropdownOption("warble"), - getDropdownOption("robot"), + getDropdownOption('none'), + getDropdownOption('tremolo'), + getDropdownOption('vibrato'), + getDropdownOption('warble'), + getDropdownOption('robot'), ], }, { - type: "input_value", - name: "EFFECT_RATE", - check: "Number", + type: 'input_value', + name: 'EFFECT_RATE', + check: 'Number', }, { - type: "input_value", - name: "EFFECT_DEPTH", - check: "Number", + type: 'input_value', + name: 'EFFECT_DEPTH', + check: 'Number', }, { - type: "input_value", - name: "ATTACK", - check: "Number", + type: 'input_value', + name: 'ATTACK', + check: 'Number', }, { - type: "input_value", - name: "DECAY", - check: "Number", + type: 'input_value', + name: 'DECAY', + check: 'Number', }, { - type: "input_value", - name: "SUSTAIN", - check: "Number", + type: 'input_value', + name: 'SUSTAIN', + check: 'Number', }, { - type: "input_value", - name: "RELEASE", - check: "Number", + type: 'input_value', + name: 'RELEASE', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("create_instrument"), + colour: categoryColours['Sound'], + tooltip: getTooltip('create_instrument'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); registerBlockHandler(this, (changeEvent) => { handleBlockCreateEvent( @@ -1114,61 +1128,61 @@ export function defineSoundBlocks() { changeEvent, variableNamePrefix, nextVariableIndexes, - "INSTRUMENT", + 'INSTRUMENT' ); }); }, }; - Blockly.Blocks["instrument"] = { + Blockly.Blocks['instrument'] = { init: function () { this.jsonInit({ - type: "instrument", - message0: translate("instrument"), + type: 'instrument', + message0: translate('instrument'), args0: [ { - type: "field_dropdown", - name: "INSTRUMENT_TYPE", + type: 'field_dropdown', + name: 'INSTRUMENT_TYPE', options: [ - getDropdownOption("default"), - getDropdownOption("piano"), - getDropdownOption("guitar"), - getDropdownOption("violin"), + getDropdownOption('default'), + getDropdownOption('piano'), + getDropdownOption('guitar'), + getDropdownOption('violin'), ], }, ], - output: "Instrument", - colour: categoryColours["Sound"], - tooltip: getTooltip("instrument"), + output: 'Instrument', + colour: categoryColours['Sound'], + tooltip: getTooltip('instrument'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; - Blockly.Blocks["play_tune"] = { + Blockly.Blocks['play_tune'] = { init: function () { this.hasImported_ = false; - this.appendDummyInput("ABC_INPUT") - .appendField(translate("play_tune")) - .appendField(new FieldAbcInput(""), "ABC_TEXT"); + this.appendDummyInput('ABC_INPUT') + .appendField(translate('play_tune')) + .appendField(new FieldAbcInput(''), 'ABC_TEXT'); this.setPreviousStatement(true, null); this.setNextStatement(true, null); - this.setColour(categoryColours["Sound"]); - this.setStyle("sound_blocks"); - this.setTooltip(getTooltip("play_tune")); + this.setColour(categoryColours['Sound']); + this.setStyle('sound_blocks'); + this.setTooltip(getTooltip('play_tune')); this.setHelpUrl(getHelpUrlFor(this.type)); setTimeout(() => { if (this.isDisposed()) return; - const f = this.getField("ABC_TEXT"); + const f = this.getField('ABC_TEXT'); if (f && f.getSvgRoot && f.getSvgRoot()) { Blockly.utils.aria.setState( f.getSvgRoot(), Blockly.utils.aria.State.LABEL, - "ABC notation, click away to import", + 'ABC notation, click away to import' ); } }, 0); @@ -1178,21 +1192,21 @@ export function defineSoundBlocks() { if (ev.type === Blockly.Events.BLOCK_CHANGE && ev.blockId === this.id) { if (!this.hasImported_) { - const value = this.getFieldValue("ABC_TEXT"); + const value = this.getFieldValue('ABC_TEXT'); if (!value) return; clearTimeout(this._abcTimer); this._abcTimer = setTimeout(() => { if (!this.isDisposed() && !this.hasImported_) this.importAbc(value); }, 150); - } else if (ev.name === "MESH_NAME") { - const meshName = this.getFieldValue("MESH_NAME"); - const doStart = this.getInputTargetBlock("DO"); + } else if (ev.name === 'MESH_NAME') { + const meshName = this.getFieldValue('MESH_NAME'); + const doStart = this.getInputTargetBlock('DO'); if (doStart && meshName) propagateMesh(doStart, meshName); } } else if (this.hasImported_ && ev.type === Blockly.Events.BLOCK_CHANGE) { - const instrTarget = this.getInput("INSTRUMENT")?.connection?.targetBlock(); + const instrTarget = this.getInput('INSTRUMENT')?.connection?.targetBlock(); if (instrTarget && ev.blockId === instrTarget.id) { - const doStart = this.getInputTargetBlock("DO"); + const doStart = this.getInputTargetBlock('DO'); if (doStart) { const instrState = Blockly.serialization.blocks.save(instrTarget); Blockly.Events.setGroup(true); @@ -1207,10 +1221,10 @@ export function defineSoundBlocks() { this.hasImported_ && ev.type === Blockly.Events.BLOCK_MOVE && ev.newParentId === this.id && - ev.newInputName === "INSTRUMENT" + ev.newInputName === 'INSTRUMENT' ) { - const instrBlock = this.getInput("INSTRUMENT")?.connection?.targetBlock(); - const doStart = this.getInputTargetBlock("DO"); + const instrBlock = this.getInput('INSTRUMENT')?.connection?.targetBlock(); + const doStart = this.getInputTargetBlock('DO'); if (instrBlock && doStart) { const instrState = Blockly.serialization.blocks.save(instrBlock); Blockly.Events.setGroup(true); @@ -1228,62 +1242,72 @@ export function defineSoundBlocks() { if (!this.hasImported_) return null; return { imported: true, - title: this.getFieldValue("TITLE") || "", - mesh: this.getFieldValue("MESH_NAME") || "__everywhere__", + title: this.getFieldValue('TITLE') || '', + mesh: this.getFieldValue('MESH_NAME') || '__everywhere__', }; }, loadExtraState: function (state) { if (!state?.imported) return; this._buildImportedShape(state.title); - if (state.mesh) this.setFieldValue(state.mesh, "MESH_NAME"); + if (state.mesh) this.setFieldValue(state.mesh, 'MESH_NAME'); }, _buildImportedShape: function (title) { - if (this.getInput("ABC_INPUT")) this.removeInput("ABC_INPUT"); + if (this.getInput('ABC_INPUT')) this.removeInput('ABC_INPUT'); - if (!this.getInput("TITLE_INPUT")) { - this.appendDummyInput("TITLE_INPUT") - .appendField(translate("play_tune")) - .appendField(new Blockly.FieldTextInput(title || ""), "TITLE") - .appendField(" on ") + if (!this.getInput('TITLE_INPUT')) { + this.appendDummyInput('TITLE_INPUT') + .appendField(translate('play_tune')) + .appendField(new Blockly.FieldTextInput(title || ''), 'TITLE') + .appendField(' on ') .appendField( new Blockly.FieldDropdown(function () { - const options = [[translate("everywhere_option"), "__everywhere__"]]; + const options = [[translate('everywhere_option'), '__everywhere__']]; const ws = this.sourceBlock_?.workspace; - if (ws) ws.getVariableMap().getAllVariables() - .forEach((v) => options.push([v.name, v.name])); + if (ws) + ws.getVariableMap() + .getAllVariables() + .forEach((v) => options.push([v.name, v.name])); return options; }), - "MESH_NAME", + 'MESH_NAME' ); - const instrInput = this.appendValueInput("INSTRUMENT") - .setCheck("Instrument") - .appendField("instrument "); + const instrInput = this.appendValueInput('INSTRUMENT') + .setCheck('Instrument') + .appendField('instrument '); instrInput.connection.setShadowState({ - type: "instrument", - fields: { INSTRUMENT_TYPE: "default" }, + type: 'instrument', + fields: { INSTRUMENT_TYPE: 'default' }, }); this.setInputsInline(true); - this.appendStatementInput("DO"); + this.appendStatementInput('DO'); setTimeout(() => { if (this.isDisposed()) return; - const tf = this.getField("TITLE"); + const tf = this.getField('TITLE'); if (tf && tf.getSvgRoot && tf.getSvgRoot()) { - Blockly.utils.aria.setState(tf.getSvgRoot(), Blockly.utils.aria.State.LABEL, "tune title"); + Blockly.utils.aria.setState( + tf.getSvgRoot(), + Blockly.utils.aria.State.LABEL, + 'tune title' + ); } - const mf = this.getField("MESH_NAME"); + const mf = this.getField('MESH_NAME'); if (mf && mf.getSvgRoot && mf.getSvgRoot()) { - Blockly.utils.aria.setState(mf.getSvgRoot(), Blockly.utils.aria.State.LABEL, "tune mesh"); + Blockly.utils.aria.setState( + mf.getSvgRoot(), + Blockly.utils.aria.State.LABEL, + 'tune mesh' + ); } - const doInput = this.getInput("DO"); - if (doInput) doInput.setAriaLabelProvider("tune bars"); + const doInput = this.getInput('DO'); + if (doInput) doInput.setAriaLabelProvider('tune bars'); }, 0); } else { - this.setFieldValue(title || "", "TITLE"); + this.setFieldValue(title || '', 'TITLE'); } this.hasImported_ = true; @@ -1291,7 +1315,14 @@ export function defineSoundBlocks() { importAbc: function (abcText) { const parsed = parseAbc(abcText); - console.log("[play_tune] importing:", parsed.title, "sections:", parsed.sections.length, "bpm:", parsed.bpm); + console.log( + '[play_tune] importing:', + parsed.title, + 'sections:', + parsed.sections.length, + 'bpm:', + parsed.bpm + ); Blockly.Events.setGroup(true); try { @@ -1300,8 +1331,8 @@ export function defineSoundBlocks() { const ws = this.workspace; if (!ws) return; - const meshName = this.getFieldValue("MESH_NAME") || "__everywhere__"; - const doInput = this.getInput("DO"); + const meshName = this.getFieldValue('MESH_NAME') || '__everywhere__'; + const doInput = this.getInput('DO'); if (doInput) { const existing = doInput.connection.targetBlock(); if (existing) existing.dispose(false); @@ -1309,7 +1340,7 @@ export function defineSoundBlocks() { try { abcBuildDoBlocks(ws, doInput, parsed.sections, parsed.bpm, meshName); } catch (e) { - console.error("[play_tune] DO block creation failed:", e); + console.error('[play_tune] DO block creation failed:', e); } } } finally { @@ -1320,62 +1351,62 @@ export function defineSoundBlocks() { }, }; - Blockly.Blocks["speak"] = { + Blockly.Blocks['speak'] = { init: function () { this.jsonInit({ - type: "speak", - message0: translate("speak"), + type: 'speak', + message0: translate('speak'), args0: [ { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, { - type: "input_dummy", - name: "MESH_INPUT", // Dummy input for the dropdown + type: 'input_dummy', + name: 'MESH_INPUT', // Dummy input for the dropdown }, { - type: "field_dropdown", - name: "VOICE", - options: [getDropdownOption("female"), getDropdownOption("male")], + type: 'field_dropdown', + name: 'VOICE', + options: [getDropdownOption('female'), getDropdownOption('male')], }, { - type: "field_dropdown", - name: "LANGUAGE", - options: [getDropdownOption("en-GB"), getDropdownOption("en-US")], + type: 'field_dropdown', + name: 'LANGUAGE', + options: [getDropdownOption('en-GB'), getDropdownOption('en-US')], }, { - type: "input_value", - name: "RATE", - check: "Number", + type: 'input_value', + name: 'RATE', + check: 'Number', }, { - type: "input_value", - name: "PITCH", - check: "Number", + type: 'input_value', + name: 'PITCH', + check: 'Number', }, { - type: "input_value", - name: "VOLUME", - check: "Number", + type: 'input_value', + name: 'VOLUME', + check: 'Number', }, { - type: "field_dropdown", - name: "ASYNC", - options: [getDropdownOption("START"), getDropdownOption("AWAIT")], + type: 'field_dropdown', + name: 'ASYNC', + options: [getDropdownOption('START'), getDropdownOption('AWAIT')], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("speak"), - extensions: ["dynamic_mesh_dropdown"], // Attach the extension + colour: categoryColours['Sound'], + tooltip: getTooltip('speak'), + extensions: ['dynamic_mesh_dropdown'], // Attach the extension }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; } diff --git a/blocks/text.js b/blocks/text.js index 5d3781c0..73777463 100644 --- a/blocks/text.js +++ b/blocks/text.js @@ -1,199 +1,194 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; import { getHelpUrlFor, nextVariableIndexes, handleBlockCreateEvent, handleBlockChange, registerBlockHandler, -} from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; +} from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; export function defineTextBlocks() { - Blockly.Blocks["comment"] = { + Blockly.Blocks['comment'] = { init: function () { this.jsonInit({ - type: "comment", - message0: translate("comment"), + type: 'comment', + message0: translate('comment'), args0: [ { - type: "input_value", - name: "COMMENT", - check: "String", + type: 'input_value', + name: 'COMMENT', + check: 'String', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: "#d3d3d3", - tooltip: getTooltip("comment"), + colour: '#d3d3d3', + tooltip: getTooltip('comment'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); }, }; - Blockly.Blocks["print_text"] = { + Blockly.Blocks['print_text'] = { init: function () { this.jsonInit({ - type: "print_text", - message0: translate("print_text"), + type: 'print_text', + message0: translate('print_text'), args0: [ { - type: "input_value", - name: "TEXT", - check: ["String", "Number", "Array"], + type: 'input_value', + name: 'TEXT', + check: ['String', 'Number', 'Array'], }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "input_value", - name: "COLOR", - colour: "#000080", - check: "Colour", + type: 'input_value', + name: 'COLOR', + colour: '#000080', + check: 'Colour', }, ], // The message words ("print"/"for"/"seconds") don't read well as // per-input labels, so label each input explicitly. - ariaLabels: { TEXT: "text", DURATION: "seconds", COLOR: "color" }, + ariaLabels: { TEXT: 'text', DURATION: 'seconds', COLOR: 'color' }, inputsInline: true, previousStatement: null, nextStatement: null, colour: 160, - tooltip: getTooltip("print_text"), + tooltip: getTooltip('print_text'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); }, }; - Blockly.Blocks["say"] = { + Blockly.Blocks['say'] = { init: function () { this.jsonInit({ - type: "say", - message0: translate("say"), + type: 'say', + message0: translate('say'), args0: [ { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "TEXT_COLOR", - colour: "#000000", - check: "Colour", + type: 'input_value', + name: 'TEXT_COLOR', + colour: '#000000', + check: 'Colour', }, { - type: "input_value", - name: "BACKGROUND_COLOR", - colour: "#ffffff", - check: "Colour", + type: 'input_value', + name: 'BACKGROUND_COLOR', + colour: '#ffffff', + check: 'Colour', }, { - type: "input_value", - name: "ALPHA", - check: "Number", + type: 'input_value', + name: 'ALPHA', + check: 'Number', }, { - type: "input_value", - name: "SIZE", - check: "Number", + type: 'input_value', + name: 'SIZE', + check: 'Number', }, { - type: "field_dropdown", - name: "MODE", - options: [getDropdownOption("ADD"), getDropdownOption("REPLACE")], + type: 'field_dropdown', + name: 'MODE', + options: [getDropdownOption('ADD'), getDropdownOption('REPLACE')], }, { - type: "field_dropdown", - name: "ASYNC", - options: [getDropdownOption("START"), getDropdownOption("AWAIT")], + type: 'field_dropdown', + name: 'ASYNC', + options: [getDropdownOption('START'), getDropdownOption('AWAIT')], }, ], inputsInline: true, previousStatement: null, nextStatement: null, colour: 160, - tooltip: getTooltip("say"), + tooltip: getTooltip('say'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); }, }; - Blockly.Blocks["ui_text"] = { + Blockly.Blocks['ui_text'] = { init: function () { - const variableNamePrefix = "uitext"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'uitext'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "ui_text", - message0: translate("ui_text"), + type: 'ui_text', + message0: translate('ui_text'), args0: [ { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, { - type: "field_variable", - name: "TEXTBLOCK_VAR", + type: 'field_variable', + name: 'TEXTBLOCK_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "FONT_SIZE", - check: "Number", + type: 'input_value', + name: 'FONT_SIZE', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, { - type: "input_value", - name: "COLOR", - check: "Colour", + type: 'input_value', + name: 'COLOR', + check: 'Colour', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Text"], - tooltip: getTooltip("ui_text"), + colour: categoryColours['Text'], + tooltip: getTooltip('ui_text'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); registerBlockHandler(this, (changeEvent) => handleBlockCreateEvent( @@ -201,80 +196,79 @@ export function defineTextBlocks() { changeEvent, variableNamePrefix, nextVariableIndexes, - "TEXTBLOCK_VAR", - ), + 'TEXTBLOCK_VAR' + ) ); }, }; - Blockly.Blocks["ui_button"] = { + Blockly.Blocks['ui_button'] = { init: function () { - const variableNamePrefix = "button"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'button'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "ui_button", - message0: translate("ui_button"), + type: 'ui_button', + message0: translate('ui_button'), args0: [ { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, { - type: "field_variable", - name: "BUTTON_VAR", + type: 'field_variable', + name: 'BUTTON_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "field_dropdown", - name: "SIZE", + type: 'field_dropdown', + name: 'SIZE', options: [ - getDropdownOption("SMALL"), - getDropdownOption("MEDIUM"), - getDropdownOption("LARGE"), + getDropdownOption('SMALL'), + getDropdownOption('MEDIUM'), + getDropdownOption('LARGE'), ], }, { - type: "field_dropdown", - name: "TEXT_SIZE", + type: 'field_dropdown', + name: 'TEXT_SIZE', options: [ - getDropdownOption("14px"), - getDropdownOption("18px"), - getDropdownOption("24px"), + getDropdownOption('14px'), + getDropdownOption('18px'), + getDropdownOption('24px'), ], }, { - type: "input_value", - name: "TEXT_COLOR", - check: "Colour", + type: 'input_value', + name: 'TEXT_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "BACKGROUND_COLOR", - check: "Colour", + type: 'input_value', + name: 'BACKGROUND_COLOR', + check: 'Colour', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Text"], - tooltip: getTooltip("ui_button"), + colour: categoryColours['Text'], + tooltip: getTooltip('ui_button'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); registerBlockHandler(this, (changeEvent) => handleBlockCreateEvent( @@ -282,76 +276,75 @@ export function defineTextBlocks() { changeEvent, variableNamePrefix, nextVariableIndexes, - "BUTTON_VAR", - ), + 'BUTTON_VAR' + ) ); }, }; - Blockly.Blocks["ui_input"] = { + Blockly.Blocks['ui_input'] = { init: function () { - const variableNamePrefix = "input"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = 'input'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "ui_input", - message0: translate("ui_input"), + type: 'ui_input', + message0: translate('ui_input'), args0: [ { - type: "field_variable", - name: "INPUT_VAR", + type: 'field_variable', + name: 'INPUT_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "field_dropdown", - name: "SIZE", + type: 'field_dropdown', + name: 'SIZE', options: [ - getDropdownOption("SMALL"), - getDropdownOption("MEDIUM"), - getDropdownOption("LARGE"), + getDropdownOption('SMALL'), + getDropdownOption('MEDIUM'), + getDropdownOption('LARGE'), ], }, { - type: "input_value", - name: "TEXT_SIZE", - check: "Number", + type: 'input_value', + name: 'TEXT_SIZE', + check: 'Number', }, { - type: "input_value", - name: "TEXT_COLOR", - check: "Colour", + type: 'input_value', + name: 'TEXT_COLOR', + check: 'Colour', }, { - type: "input_value", - name: "BACKGROUND_COLOR", - check: "Colour", + type: 'input_value', + name: 'BACKGROUND_COLOR', + check: 'Colour', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Text"], - tooltip: getTooltip("ui_input"), + colour: categoryColours['Text'], + tooltip: getTooltip('ui_input'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); registerBlockHandler(this, (changeEvent) => handleBlockCreateEvent( @@ -359,108 +352,107 @@ export function defineTextBlocks() { changeEvent, variableNamePrefix, nextVariableIndexes, - "INPUT_VAR", - ), + 'INPUT_VAR' + ) ); }, }; - Blockly.Blocks["describe"] = { + Blockly.Blocks['describe'] = { init: function () { this.jsonInit({ - type: "describe", - message0: translate("describe"), + type: 'describe', + message0: translate('describe'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Text"], - tooltip: getTooltip("describe"), + colour: categoryColours['Text'], + tooltip: getTooltip('describe'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); }, }; - Blockly.Blocks["create_3d_text"] = { + Blockly.Blocks['create_3d_text'] = { init: function () { - const variableNamePrefix = "3dtext"; - const nextVariableName = - variableNamePrefix + nextVariableIndexes[variableNamePrefix]; + const variableNamePrefix = '3dtext'; + const nextVariableName = variableNamePrefix + nextVariableIndexes[variableNamePrefix]; this.jsonInit({ - type: "create_3d_text", - message0: translate("create_3d_text"), + type: 'create_3d_text', + message0: translate('create_3d_text'), args0: [ { - type: "field_variable", - name: "ID_VAR", + type: 'field_variable', + name: 'ID_VAR', variable: nextVariableName, }, { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, { - type: "field_dropdown", - name: "FONT", - options: [getDropdownOption("__fonts_FreeSans_Bold_json")], + type: 'field_dropdown', + name: 'FONT', + options: [getDropdownOption('__fonts_FreeSans_Bold_json')], }, { - type: "input_value", - name: "SIZE", - check: "Number", + type: 'input_value', + name: 'SIZE', + check: 'Number', }, { - type: "input_value", - name: "COLOR", - check: "Colour", + type: 'input_value', + name: 'COLOR', + check: 'Colour', }, { - type: "input_value", - name: "DEPTH", - check: "Number", + type: 'input_value', + name: 'DEPTH', + check: 'Number', }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, ], inputsInline: true, - colour: categoryColours["Text"], - tooltip: getTooltip("create_3d_text"), + colour: categoryColours['Text'], + tooltip: getTooltip('create_3d_text'), previousStatement: null, nextStatement: null, }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("text_blocks"); + this.setStyle('text_blocks'); registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent, variableNamePrefix), + handleBlockChange(this, changeEvent, variableNamePrefix) ); }, }; diff --git a/blocks/transform.js b/blocks/transform.js index 3c8e196f..116f9577 100644 --- a/blocks/transform.js +++ b/blocks/transform.js @@ -1,600 +1,563 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { - getHelpUrlFor, - handleFieldOrChildChange, - registerBlockHandler, -} from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; -import { flock } from "../flock.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor, handleFieldOrChildChange, registerBlockHandler } from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; +import { flock } from '../flock.js'; export function defineTransformBlocks() { function handleBlockChange(block, changeEvent) { - const changeEventBlock = Blockly.getMainWorkspace().getBlockById( - changeEvent.blockId, - ); + const changeEventBlock = Blockly.getMainWorkspace().getBlockById(changeEvent.blockId); if (!changeEventBlock) return; - if (flock.blockDebug) - console.log("The ID of this change event is", changeEventBlock.id); + if (flock.blockDebug) console.log('The ID of this change event is', changeEventBlock.id); const changeEventParentBlock = changeEventBlock.getParent(); if (!changeEventParentBlock) return; const changeEventBlockType = changeEventParentBlock.type; - if (flock.blockDebug) - console.log("The type of this change event is", changeEventBlockType); - if (changeEventBlockType != "rotate_to" && changeEventBlockType != "resize") - return; + if (flock.blockDebug) console.log('The type of this change event is', changeEventBlockType); + if (changeEventBlockType != 'rotate_to' && changeEventBlockType != 'resize') return; const handleChange = handleFieldOrChildChange(block, changeEvent); if (flock.blockDebug) console.log(handleChange); } - Blockly.Blocks["move_by_xyz"] = { + Blockly.Blocks['move_by_xyz'] = { init: function () { this.jsonInit({ - type: "move_by_xyz", - message0: translate("move_by_xyz"), + type: 'move_by_xyz', + message0: translate('move_by_xyz'), args0: [ { - type: "field_variable", - name: "BLOCK_NAME", + type: 'field_variable', + name: 'BLOCK_NAME', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'X', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Y", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Y', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Z", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Z', + check: 'Number', + align: 'RIGHT', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("move_by_xyz"), + tooltip: getTooltip('move_by_xyz'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["move_by_xyz_single"] = { + Blockly.Blocks['move_by_xyz_single'] = { init: function () { this.jsonInit({ - type: "move_by_xyz_single", - message0: translate("move_by_xyz_single"), + type: 'move_by_xyz_single', + message0: translate('move_by_xyz_single'), args0: [ { - type: "field_variable", - name: "BLOCK_NAME", + type: 'field_variable', + name: 'BLOCK_NAME', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "COORDINATE", + type: 'field_dropdown', + name: 'COORDINATE', options: [ - getDropdownOption("x_coordinate"), - getDropdownOption("y_coordinate"), - getDropdownOption("z_coordinate"), + getDropdownOption('x_coordinate'), + getDropdownOption('y_coordinate'), + getDropdownOption('z_coordinate'), ], }, { - type: "input_value", - name: "VALUE", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'VALUE', + check: 'Number', + align: 'RIGHT', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("move_by_xyz_single"), + tooltip: getTooltip('move_by_xyz_single'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["move_to_xyz"] = { + Blockly.Blocks['move_to_xyz'] = { init: function () { this.jsonInit({ - type: "move_to_xyz", - message0: translate("move_to_xyz"), + type: 'move_to_xyz', + message0: translate('move_to_xyz'), args0: [ { - type: "field_variable", - name: "MODEL", + type: 'field_variable', + name: 'MODEL', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'X', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Y", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Y', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Z", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Z', + check: 'Number', + align: 'RIGHT', }, { - type: "field_checkbox", - name: "USE_Y", + type: 'field_checkbox', + name: 'USE_Y', checked: true, - text: "Use Y axis", + text: 'Use Y axis', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("move_to_xyz"), + tooltip: getTooltip('move_to_xyz'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["move_to_xyz_single"] = { + Blockly.Blocks['move_to_xyz_single'] = { init: function () { this.jsonInit({ - type: "move_to_xyz_single", - message0: translate("move_to_xyz_single"), + type: 'move_to_xyz_single', + message0: translate('move_to_xyz_single'), args0: [ { - type: "field_variable", - name: "MODEL", + type: 'field_variable', + name: 'MODEL', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "COORDINATE", + type: 'field_dropdown', + name: 'COORDINATE', options: [ - getDropdownOption("x_coordinate"), - getDropdownOption("y_coordinate"), - getDropdownOption("z_coordinate"), + getDropdownOption('x_coordinate'), + getDropdownOption('y_coordinate'), + getDropdownOption('z_coordinate'), ], }, { - type: "input_value", - name: "VALUE", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'VALUE', + check: 'Number', + align: 'RIGHT', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("move_to_xyz_single"), + tooltip: getTooltip('move_to_xyz_single'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["move_to"] = { + Blockly.Blocks['move_to'] = { init: function () { this.jsonInit({ - type: "move_to", - message0: translate("move_to"), + type: 'move_to', + message0: translate('move_to'), args0: [ { - type: "field_variable", - name: "MODEL1", + type: 'field_variable', + name: 'MODEL1', variable: window.currentMesh, }, { - type: "field_variable", - name: "MODEL2", - variable: "object2", + type: 'field_variable', + name: 'MODEL2', + variable: 'object2', }, { - type: "field_checkbox", - name: "USE_Y", + type: 'field_checkbox', + name: 'USE_Y', checked: false, - text: "Use Y axis", + text: 'Use Y axis', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("move_to"), + tooltip: getTooltip('move_to'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["scale"] = { + Blockly.Blocks['scale'] = { init: function () { this.jsonInit({ - type: "scale", - message0: translate("scale"), + type: 'scale', + message0: translate('scale'), args0: [ { - type: "field_variable", - name: "BLOCK_NAME", + type: 'field_variable', + name: 'BLOCK_NAME', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, { - type: "field_dropdown", - name: "X_ORIGIN", + type: 'field_dropdown', + name: 'X_ORIGIN', options: [ - getDropdownOption("CENTRE"), - getDropdownOption("LEFT"), - getDropdownOption("RIGHT"), + getDropdownOption('CENTRE'), + getDropdownOption('LEFT'), + getDropdownOption('RIGHT'), ], }, { - type: "field_dropdown", - name: "Y_ORIGIN", + type: 'field_dropdown', + name: 'Y_ORIGIN', options: [ - getDropdownOption("BASE"), - getDropdownOption("CENTRE"), - getDropdownOption("TOP"), + getDropdownOption('BASE'), + getDropdownOption('CENTRE'), + getDropdownOption('TOP'), ], }, { - type: "field_dropdown", - name: "Z_ORIGIN", + type: 'field_dropdown', + name: 'Z_ORIGIN', options: [ - getDropdownOption("CENTRE"), - getDropdownOption("FRONT"), - getDropdownOption("BACK"), + getDropdownOption('CENTRE'), + getDropdownOption('FRONT'), + getDropdownOption('BACK'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("scale"), + tooltip: getTooltip('scale'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["resize"] = { + Blockly.Blocks['resize'] = { init: function () { this.jsonInit({ - type: "resize", - message0: translate("resize"), + type: 'resize', + message0: translate('resize'), args0: [ { - type: "field_variable", - name: "BLOCK_NAME", + type: 'field_variable', + name: 'BLOCK_NAME', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", + type: 'input_value', + name: 'X', + check: 'Number', }, { - type: "input_value", - name: "Y", - check: "Number", + type: 'input_value', + name: 'Y', + check: 'Number', }, { - type: "input_value", - name: "Z", - check: "Number", + type: 'input_value', + name: 'Z', + check: 'Number', }, { - type: "field_dropdown", - name: "X_ORIGIN", + type: 'field_dropdown', + name: 'X_ORIGIN', options: [ - getDropdownOption("CENTRE"), - getDropdownOption("LEFT"), - getDropdownOption("RIGHT"), + getDropdownOption('CENTRE'), + getDropdownOption('LEFT'), + getDropdownOption('RIGHT'), ], }, { - type: "field_dropdown", - name: "Y_ORIGIN", + type: 'field_dropdown', + name: 'Y_ORIGIN', options: [ - getDropdownOption("BASE"), - getDropdownOption("CENTRE"), - getDropdownOption("TOP"), + getDropdownOption('BASE'), + getDropdownOption('CENTRE'), + getDropdownOption('TOP'), ], }, { - type: "field_dropdown", - name: "Z_ORIGIN", + type: 'field_dropdown', + name: 'Z_ORIGIN', options: [ - getDropdownOption("CENTRE"), - getDropdownOption("FRONT"), - getDropdownOption("BACK"), + getDropdownOption('CENTRE'), + getDropdownOption('FRONT'), + getDropdownOption('BACK'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("resize"), + tooltip: getTooltip('resize'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["rotate_model_xyz"] = { + Blockly.Blocks['rotate_model_xyz'] = { init: function () { this.jsonInit({ - type: "rotate_model_xyz", - message0: translate("rotate_model_xyz"), + type: 'rotate_model_xyz', + message0: translate('rotate_model_xyz'), args0: [ { - type: "field_variable", - name: "MODEL", + type: 'field_variable', + name: 'MODEL', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'X', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Y", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Y', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Z", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Z', + check: 'Number', + align: 'RIGHT', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("rotate_model_xyz"), + tooltip: getTooltip('rotate_model_xyz'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["rotate_to"] = { + Blockly.Blocks['rotate_to'] = { init: function () { this.jsonInit({ - type: "rotate_to", - message0: translate("rotate_to"), + type: 'rotate_to', + message0: translate('rotate_to'), args0: [ { - type: "field_variable", - name: "MODEL", + type: 'field_variable', + name: 'MODEL', variable: window.currentMesh, }, { - type: "input_value", - name: "X", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'X', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Y", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Y', + check: 'Number', + align: 'RIGHT', }, { - type: "input_value", - name: "Z", - check: "Number", - align: "RIGHT", + type: 'input_value', + name: 'Z', + check: 'Number', + align: 'RIGHT', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("rotate_to"), + tooltip: getTooltip('rotate_to'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["look_at"] = { + Blockly.Blocks['look_at'] = { init: function () { this.jsonInit({ - type: "look_at", - message0: translate("look_at"), + type: 'look_at', + message0: translate('look_at'), args0: [ { - type: "field_variable", - name: "MODEL1", + type: 'field_variable', + name: 'MODEL1', variable: window.currentMesh, }, { - type: "field_variable", - name: "MODEL2", - variable: "object2", + type: 'field_variable', + name: 'MODEL2', + variable: 'object2', }, { - type: "field_checkbox", - name: "USE_Y", + type: 'field_checkbox', + name: 'USE_Y', checked: false, - text: "Use Y axis", + text: 'Use Y axis', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], + colour: categoryColours['Transform'], inputsInline: true, - tooltip: getTooltip("look_at"), + tooltip: getTooltip('look_at'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["set_pivot"] = { + Blockly.Blocks['set_pivot'] = { init: function () { this.jsonInit({ - type: "set_pivot", - message0: translate("set_pivot"), + type: 'set_pivot', + message0: translate('set_pivot'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, // Assuming the mesh is stored here }, { - type: "input_value", - name: "X_PIVOT", - check: ["Number", "String"], + type: 'input_value', + name: 'X_PIVOT', + check: ['Number', 'String'], }, { - type: "input_value", - name: "Y_PIVOT", - check: ["Number", "String"], + type: 'input_value', + name: 'Y_PIVOT', + check: ['Number', 'String'], }, { - type: "input_value", - name: "Z_PIVOT", - check: ["Number", "String"], + type: 'input_value', + name: 'Z_PIVOT', + check: ['Number', 'String'], }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("set_pivot"), + colour: categoryColours['Transform'], + tooltip: getTooltip('set_pivot'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; - Blockly.Blocks["min_centre_max"] = { + Blockly.Blocks['min_centre_max'] = { init: function () { this.jsonInit({ - type: "min_centre_max", - message0: translate("min_centre_max"), + type: 'min_centre_max', + message0: translate('min_centre_max'), args0: [ { - type: "field_dropdown", - name: "PIVOT_OPTION", + type: 'field_dropdown', + name: 'PIVOT_OPTION', options: [ - getDropdownOption("MIN"), - getDropdownOption("CENTER"), - getDropdownOption("MAX"), + getDropdownOption('MIN'), + getDropdownOption('CENTER'), + getDropdownOption('MAX'), ], }, ], - output: "String", // Now returns a symbolic string - colour: categoryColours["Transform"], - tooltip: getTooltip("min_centre_max"), + output: 'String', // Now returns a symbolic string + colour: categoryColours['Transform'], + tooltip: getTooltip('min_centre_max'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); // Set up the change handler. - registerBlockHandler(this, (changeEvent) => - handleBlockChange(this, changeEvent), - ); + registerBlockHandler(this, (changeEvent) => handleBlockChange(this, changeEvent)); }, }; } diff --git a/blocks/xr.js b/blocks/xr.js index db7c1b7e..ad0b2da1 100644 --- a/blocks/xr.js +++ b/blocks/xr.js @@ -1,178 +1,171 @@ -import * as Blockly from "blockly"; -import { categoryColours } from "../toolbox.js"; -import { getHelpUrlFor } from "./blocks.js"; -import { - translate, - getTooltip, - getDropdownOption, -} from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { categoryColours } from '../toolbox.js'; +import { getHelpUrlFor } from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; export function defineXRBlocks() { - Blockly.Blocks["device_camera_background"] = { + Blockly.Blocks['device_camera_background'] = { init: function () { this.jsonInit({ - type: "device_camera_background", - message0: translate("device_camera_background"), + type: 'device_camera_background', + message0: translate('device_camera_background'), args0: [ { - type: "field_dropdown", - name: "CAMERA", - options: [ - getDropdownOption("user"), - getDropdownOption("environment"), - ], + type: 'field_dropdown', + name: 'CAMERA', + options: [getDropdownOption('user'), getDropdownOption('environment')], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("device_camera_background"), + colour: categoryColours['Scene'], + tooltip: getTooltip('device_camera_background'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; - Blockly.Blocks["set_xr_mode"] = { + Blockly.Blocks['set_xr_mode'] = { init: function () { this.jsonInit({ - type: "set_xr_mode", - message0: translate("set_xr_mode"), + type: 'set_xr_mode', + message0: translate('set_xr_mode'), args0: [ { - type: "field_dropdown", - name: "MODE", + type: 'field_dropdown', + name: 'MODE', options: [ - getDropdownOption("VR"), - getDropdownOption("AR"), - getDropdownOption("MAGIC_WINDOW"), + getDropdownOption('VR'), + getDropdownOption('AR'), + getDropdownOption('MAGIC_WINDOW'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("set_xr_mode"), + colour: categoryColours['Scene'], + tooltip: getTooltip('set_xr_mode'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; - Blockly.Blocks["play_rumble_pattern"] = { + Blockly.Blocks['play_rumble_pattern'] = { init: function () { this.jsonInit({ - type: "play_rumble_pattern", - message0: translate("play_rumble_pattern"), + type: 'play_rumble_pattern', + message0: translate('play_rumble_pattern'), args0: [ { - type: "field_dropdown", - name: "PATTERN", + type: 'field_dropdown', + name: 'PATTERN', options: [ - getDropdownOption("objectGrab"), - getDropdownOption("objectDrop"), - getDropdownOption("smallCollision"), - getDropdownOption("heavyCollision"), - getDropdownOption("snapToGrid"), - getDropdownOption("errorInvalid"), - getDropdownOption("successConfirmation"), - getDropdownOption("slidingGravel"), - getDropdownOption("slidingMetal"), - getDropdownOption("machineRunning"), - getDropdownOption("explosion"), - getDropdownOption("teleport"), + getDropdownOption('objectGrab'), + getDropdownOption('objectDrop'), + getDropdownOption('smallCollision'), + getDropdownOption('heavyCollision'), + getDropdownOption('snapToGrid'), + getDropdownOption('errorInvalid'), + getDropdownOption('successConfirmation'), + getDropdownOption('slidingGravel'), + getDropdownOption('slidingMetal'), + getDropdownOption('machineRunning'), + getDropdownOption('explosion'), + getDropdownOption('teleport'), ], }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("play_rumble_pattern"), + colour: categoryColours['Scene'], + tooltip: getTooltip('play_rumble_pattern'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; - Blockly.Blocks["controller_rumble"] = { + Blockly.Blocks['controller_rumble'] = { init: function () { this.jsonInit({ - type: "controller_rumble", - message0: translate("controller_rumble"), + type: 'controller_rumble', + message0: translate('controller_rumble'), args0: [ { - type: "field_dropdown", - name: "MOTOR", + type: 'field_dropdown', + name: 'MOTOR', options: [ - getDropdownOption("all"), - getDropdownOption("left"), - getDropdownOption("right"), + getDropdownOption('all'), + getDropdownOption('left'), + getDropdownOption('right'), ], }, { - type: "input_value", - name: "STRENGTH", - check: "Number", + type: 'input_value', + name: 'STRENGTH', + check: 'Number', }, { - type: "input_value", - name: "DURATION", - check: "Number", + type: 'input_value', + name: 'DURATION', + check: 'Number', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("controller_rumble"), + colour: categoryColours['Scene'], + tooltip: getTooltip('controller_rumble'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; - Blockly.Blocks["controller_rumble_pattern"] = { + Blockly.Blocks['controller_rumble_pattern'] = { init: function () { this.jsonInit({ - type: "controller_rumble_pattern", - message0: translate("controller_rumble_pattern"), + type: 'controller_rumble_pattern', + message0: translate('controller_rumble_pattern'), args0: [ { - type: "field_dropdown", - name: "MOTOR", + type: 'field_dropdown', + name: 'MOTOR', options: [ - getDropdownOption("all"), - getDropdownOption("left"), - getDropdownOption("right"), + getDropdownOption('all'), + getDropdownOption('left'), + getDropdownOption('right'), ], }, { - type: "input_value", - name: "STRENGTH", - check: "Number", + type: 'input_value', + name: 'STRENGTH', + check: 'Number', }, { - type: "input_value", - name: "ON_DURATION", - check: "Number", + type: 'input_value', + name: 'ON_DURATION', + check: 'Number', }, { - type: "input_value", - name: "OFF_DURATION", - check: "Number", + type: 'input_value', + name: 'OFF_DURATION', + check: 'Number', }, { - type: "input_value", - name: "REPEATS", - check: "Number", + type: 'input_value', + name: 'REPEATS', + check: 'Number', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("controller_rumble_pattern"), + colour: categoryColours['Scene'], + tooltip: getTooltip('controller_rumble_pattern'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); }, }; } diff --git a/config.js b/config.js index 1f7c0b5b..e25447d8 100644 --- a/config.js +++ b/config.js @@ -1,70 +1,69 @@ -import { getDropdownOption, translate } from "./main/translation.js"; +import { getDropdownOption, translate } from './main/translation.js'; -export const SHORTCUTS_HELP_URL = - "https://hub.flockxr.com/knowledge-base/keyboard-controls/"; +export const SHORTCUTS_HELP_URL = 'https://hub.flockxr.com/knowledge-base/keyboard-controls/'; export const TOP_BLOCK_TYPES = Object.freeze([ - "start", - "forever", - "when_clicked", - "when_touches", - "on_collision", - "when_key_event", - "when_action_event", - "on_event", - "procedures_defnoreturn", - "procedures_defreturn", - "microbit_input", + 'start', + 'forever', + 'when_clicked', + 'when_touches', + 'on_collision', + 'when_key_event', + 'when_action_event', + 'on_event', + 'procedures_defnoreturn', + 'procedures_defreturn', + 'microbit_input', ]); export const themeNames = [ - "theme-bright.mp3", - "theme-calm.mp3", - "theme-electronic.mp3", - "theme-game.mp3", - "theme-medieval.mp3", - "theme-metal.mp3", + 'theme-bright.mp3', + 'theme-calm.mp3', + 'theme-electronic.mp3', + 'theme-game.mp3', + 'theme-medieval.mp3', + 'theme-metal.mp3', ]; export const audioNames = [ - "highDown.mp3", - "highUp.mp3", - "laser1.mp3", - "laser2.mp3", - "laser3.mp3", - "lowDown.mp3", - "lowRandom.mp3", - "lowThreeTone.mp3", - "phaseJump1.mp3", - "powerUp1.mp3", - "powerUp2.mp3", - "powerUp3.mp3", - "powerUp4.mp3", - "powerUp5.mp3", - "spaceTrash.mp3", - "threeTone1.mp3", - "threeTone2.mp3", - "chop.mp3", - "creak.mp3", - "door_close.mp3", - "door_open.mp3", - "footstep.mp3", - "metal_latch.mp3", + 'highDown.mp3', + 'highUp.mp3', + 'laser1.mp3', + 'laser2.mp3', + 'laser3.mp3', + 'lowDown.mp3', + 'lowRandom.mp3', + 'lowThreeTone.mp3', + 'phaseJump1.mp3', + 'powerUp1.mp3', + 'powerUp2.mp3', + 'powerUp3.mp3', + 'powerUp4.mp3', + 'powerUp5.mp3', + 'spaceTrash.mp3', + 'threeTone1.mp3', + 'threeTone2.mp3', + 'chop.mp3', + 'creak.mp3', + 'door_close.mp3', + 'door_open.mp3', + 'footstep.mp3', + 'metal_latch.mp3', ]; export function audioFileToLabel(filename) { return filename - .replace(".mp3", "") - .replace(/([A-Z])/g, " $1") - .replace(/(\d+)/g, " $1") + .replace('.mp3', '') + .replace(/([A-Z])/g, ' $1') + .replace(/(\d+)/g, ' $1') .trim() - .replace(/\s+/g, " ") + .replace(/\s+/g, ' ') .replace(/\b\w/g, (c) => c.toUpperCase()); } export function getThemeDisplayName(filename) { - const baseName = filename.replace("theme-", "").replace(".mp3", ""); - const key = "theme_" + baseName + "_option"; + const baseName = filename.replace('theme-', '').replace('.mp3', ''); + const key = 'theme_' + baseName + '_option'; const translated = translate(key); return translated && translated !== key ? translated @@ -72,27 +71,25 @@ export function getThemeDisplayName(filename) { } export function getSoundDisplayName(filename) { - const baseName = filename.replace(".mp3", ""); - const key = "sound_" + baseName + "_option"; + const baseName = filename.replace('.mp3', ''); + const key = 'sound_' + baseName + '_option'; const translated = translate(key); - return translated && translated !== key - ? translated - : audioFileToLabel(filename); + return translated && translated !== key ? translated : audioFileToLabel(filename); } export const characterNames = [ - "Liz1.glb", - "Liz2.glb", - "Block1.glb", - "Block2.glb", - "Block3.glb", - "Block4.glb", - "Block5.glb", - "Block6.glb", - "Liz3.glb", - "Liz4.glb", - "Liz5.glb", - "Liz6.glb", + 'Liz1.glb', + 'Liz2.glb', + 'Block1.glb', + 'Block2.glb', + 'Block3.glb', + 'Block4.glb', + 'Block5.glb', + 'Block6.glb', + 'Liz3.glb', + 'Liz4.glb', + 'Liz5.glb', + 'Liz6.glb', /*"Character1.glb", "Character2.glb", "Character3.glb", @@ -100,112 +97,112 @@ export const characterNames = [ ]; export const multiObjectNames = [ - "tree.glb", - "tree2.glb", - "tree3.glb", - "tree4.glb", - "hut.glb", - "hut2.glb", - "hut3.glb", - "hut4.glb", - "rocks.glb", - "rocks2.glb", - "rocks3.glb", - "rocks4.glb", - "pond.glb", - "boat.glb", - "airplane.glb", - "airplane2.glb", - "skateboard.glb", - "humped.glb", - "jetty.glb", - "flower.glb", - "flower2.glb", + 'tree.glb', + 'tree2.glb', + 'tree3.glb', + 'tree4.glb', + 'hut.glb', + 'hut2.glb', + 'hut3.glb', + 'hut4.glb', + 'rocks.glb', + 'rocks2.glb', + 'rocks3.glb', + 'rocks4.glb', + 'pond.glb', + 'boat.glb', + 'airplane.glb', + 'airplane2.glb', + 'skateboard.glb', + 'humped.glb', + 'jetty.glb', + 'flower.glb', + 'flower2.glb', //"stones_straight.glb", //"stones_curve.glb", ]; export const objectNames = [ - "Star.glb", - "Heart.glb", - "Coin.glb", - "egg.glb", - "Gem1.glb", - "Gem2.glb", - "Gem3.glb", - "Key.glb", - "Wand.glb", - "Hat.glb", - "donut.glb", - "pumpkin.glb", - "apple.glb", - "starboppers.glb", - "headphones.glb", + 'Star.glb', + 'Heart.glb', + 'Coin.glb', + 'egg.glb', + 'Gem1.glb', + 'Gem2.glb', + 'Gem3.glb', + 'Key.glb', + 'Wand.glb', + 'Hat.glb', + 'donut.glb', + 'pumpkin.glb', + 'apple.glb', + 'starboppers.glb', + 'headphones.glb', ]; function modelNameToDisplayName(modelName) { - return String(modelName || "") - .replace(/\.[^/.]+$/, "") - .replace(/[_-]+/g, " ") - .replace(/\s+/g, " ") + return String(modelName || '') + .replace(/\.[^/.]+$/, '') + .replace(/[_-]+/g, ' ') + .replace(/\s+/g, ' ') .trim() .replace(/\b\w/g, (char) => char.toUpperCase()); } export const objectDisplayNameTranslationKeys = { - "Liz1.glb": "model_display_liz1", - "Liz2.glb": "model_display_liz2", - "Liz3.glb": "model_display_liz3", - "Liz4.glb": "model_display_liz4", - "Liz5.glb": "model_display_liz5", - "Liz6.glb": "model_display_liz6", - "Block1.glb": "model_display_block1", - "Block2.glb": "model_display_block2", - "Block3.glb": "model_display_block3", - "Block4.glb": "model_display_block4", - "Block5.glb": "model_display_block5", - "Block6.glb": "model_display_block6", - - "tree.glb": "model_display_tree", - "tree2.glb": "model_display_tree2", - "tree3.glb": "model_display_tree3", - "tree4.glb": "model_display_tree4", - "hut.glb": "model_display_hut", - "hut2.glb": "model_display_hut2", - "hut3.glb": "model_display_hut3", - "hut4.glb": "model_display_hut4", - "rocks.glb": "model_display_rocks", - "rocks2.glb": "model_display_rocks2", - "rocks3.glb": "model_display_rocks3", - "rocks4.glb": "model_display_rocks4", - "pond.glb": "model_display_pond", - "boat.glb": "model_display_boat", - "airplane.glb": "model_display_airplane", - "airplane2.glb": "model_display_airplane2", - "skateboard.glb": "model_display_skateboard", - "humped.glb": "model_display_humped", - "jetty.glb": "model_display_jetty", - "flower.glb": "model_display_flower", - "flower2.glb": "model_display_flower2", - "Star.glb": "model_display_star", - "Heart.glb": "model_display_heart", - "Coin.glb": "model_display_coin", - "egg.glb": "model_display_egg", - "Gem1.glb": "model_display_gem1", - "Gem2.glb": "model_display_gem2", - "Gem3.glb": "model_display_gem3", - "Key.glb": "model_display_key", - "Wand.glb": "model_display_wand", - "Hat.glb": "model_display_hat", - "donut.glb": "model_display_donut", - "pumpkin.glb": "model_display_pumpkin", - "apple.glb": "model_display_apple", - "starboppers.glb": "model_display_starboppers", - "headphones.glb": "model_display_headphones", - "Flock.glb": "model_display_flock", - "Flock_Santa.glb": "model_display_flock_santa", - "Character.glb": "model_display_character", - "rhino.glb": "model_display_rhino", + 'Liz1.glb': 'model_display_liz1', + 'Liz2.glb': 'model_display_liz2', + 'Liz3.glb': 'model_display_liz3', + 'Liz4.glb': 'model_display_liz4', + 'Liz5.glb': 'model_display_liz5', + 'Liz6.glb': 'model_display_liz6', + 'Block1.glb': 'model_display_block1', + 'Block2.glb': 'model_display_block2', + 'Block3.glb': 'model_display_block3', + 'Block4.glb': 'model_display_block4', + 'Block5.glb': 'model_display_block5', + 'Block6.glb': 'model_display_block6', + + 'tree.glb': 'model_display_tree', + 'tree2.glb': 'model_display_tree2', + 'tree3.glb': 'model_display_tree3', + 'tree4.glb': 'model_display_tree4', + 'hut.glb': 'model_display_hut', + 'hut2.glb': 'model_display_hut2', + 'hut3.glb': 'model_display_hut3', + 'hut4.glb': 'model_display_hut4', + 'rocks.glb': 'model_display_rocks', + 'rocks2.glb': 'model_display_rocks2', + 'rocks3.glb': 'model_display_rocks3', + 'rocks4.glb': 'model_display_rocks4', + 'pond.glb': 'model_display_pond', + 'boat.glb': 'model_display_boat', + 'airplane.glb': 'model_display_airplane', + 'airplane2.glb': 'model_display_airplane2', + 'skateboard.glb': 'model_display_skateboard', + 'humped.glb': 'model_display_humped', + 'jetty.glb': 'model_display_jetty', + 'flower.glb': 'model_display_flower', + 'flower2.glb': 'model_display_flower2', + 'Star.glb': 'model_display_star', + 'Heart.glb': 'model_display_heart', + 'Coin.glb': 'model_display_coin', + 'egg.glb': 'model_display_egg', + 'Gem1.glb': 'model_display_gem1', + 'Gem2.glb': 'model_display_gem2', + 'Gem3.glb': 'model_display_gem3', + 'Key.glb': 'model_display_key', + 'Wand.glb': 'model_display_wand', + 'Hat.glb': 'model_display_hat', + 'donut.glb': 'model_display_donut', + 'pumpkin.glb': 'model_display_pumpkin', + 'apple.glb': 'model_display_apple', + 'starboppers.glb': 'model_display_starboppers', + 'headphones.glb': 'model_display_headphones', + 'Flock.glb': 'model_display_flock', + 'Flock_Santa.glb': 'model_display_flock_santa', + 'Character.glb': 'model_display_character', + 'rhino.glb': 'model_display_rhino', }; export function getModelDisplayName(modelName) { @@ -220,269 +217,248 @@ export function getModelDisplayName(modelName) { } export const objectColours = { - "Star.glb": ["#FFD700", "#FFD700", "#FFD700"], - "Heart.glb": ["#FF69B4", "#FF69B4", "#FF69B4"], - "Coin.glb": ["#A47E1B", "#C9A227", "#76520E"], - "egg.glb": ["#fffcec", "#fffcec", "#fffcec"], - "Gem1.glb": ["#00BFFF", "#00BFFF", "#00BFFF"], - "Gem2.glb": ["#8A2BE2", "#8A2BE2", "#8A2BE2"], - "Gem3.glb": ["#FF4500", "#FF4500", "#FF4500"], - "Key.glb": ["#A47E1B", "#C9A227", "#76520E"], - "Wand.glb": ["#FF4500", "#8A2BE2", "#92614A"], - "Hat.glb": ["#9D3F72", "#B5FDFD", "#3D0073"], - "donut.glb": ["#f9cb9c", "#fba0c3"], - "pumpkin.glb": ["#E78632", "#75430F"], - "apple.glb": ["#3FAF45", "#A9323F", "#624A20"], - "starboppers.glb": ["#FFD700", "#FFD700", "#FFD700", "#f9f9f9"], - "headphones.glb": ["#53E0E7", "#3291E7", "#7D7D7D"], - - "tree.glb": ["#66CDAA", "#CD853F"], - "tree2.glb": ["#7F9F7F", "#A1623B"], - "tree3.glb": ["#403C3C", "#312616"], - "tree4.glb": ["#0D5B28", "#6D6C51"], - - "rocks.glb": ["#898D86", "#99a83d"], - "rocks2.glb": ["#898D86", "#99a83d"], - "rocks3.glb": ["#898D86", "#99a83d"], - "rocks4.glb": ["#898D86", "#99a83d", "#6BC6EF", "#f9f9f9"], - "pond.glb": ["#00E704", "#5A91E7", "#9A9A9A"], - - "hut.glb": ["#B66946", "#5F2524", "#C25A5C", "#E1B46E", "#3BACBA", "#878787"], - "hut2.glb": [ - "#814C22", - "#231E1D", - "#FFF6A6", - "#E7AF3A", - "#E73627", - "#878787", - ], - "hut3.glb": [ - "#F6DAB6", - "#6CC3C1", - "#9DC45C", - "#EEB975", - "#F3B4BE", - "#878787", - ], - "hut4.glb": [ - "#F2E8CF", - "#BC4749", - "#EEB975", - "#AF1B3F", - "#6A994E", - "#878787", - ], - - "boat.glb": [ - "#4F8A46", - "#E7D48E", - "#E76635", - "#E76C69", - "#5E64E7", - "#4A4A4A", - "#AAAAAA", - "#E711CD", + 'Star.glb': ['#FFD700', '#FFD700', '#FFD700'], + 'Heart.glb': ['#FF69B4', '#FF69B4', '#FF69B4'], + 'Coin.glb': ['#A47E1B', '#C9A227', '#76520E'], + 'egg.glb': ['#fffcec', '#fffcec', '#fffcec'], + 'Gem1.glb': ['#00BFFF', '#00BFFF', '#00BFFF'], + 'Gem2.glb': ['#8A2BE2', '#8A2BE2', '#8A2BE2'], + 'Gem3.glb': ['#FF4500', '#FF4500', '#FF4500'], + 'Key.glb': ['#A47E1B', '#C9A227', '#76520E'], + 'Wand.glb': ['#FF4500', '#8A2BE2', '#92614A'], + 'Hat.glb': ['#9D3F72', '#B5FDFD', '#3D0073'], + 'donut.glb': ['#f9cb9c', '#fba0c3'], + 'pumpkin.glb': ['#E78632', '#75430F'], + 'apple.glb': ['#3FAF45', '#A9323F', '#624A20'], + 'starboppers.glb': ['#FFD700', '#FFD700', '#FFD700', '#f9f9f9'], + 'headphones.glb': ['#53E0E7', '#3291E7', '#7D7D7D'], + + 'tree.glb': ['#66CDAA', '#CD853F'], + 'tree2.glb': ['#7F9F7F', '#A1623B'], + 'tree3.glb': ['#403C3C', '#312616'], + 'tree4.glb': ['#0D5B28', '#6D6C51'], + + 'rocks.glb': ['#898D86', '#99a83d'], + 'rocks2.glb': ['#898D86', '#99a83d'], + 'rocks3.glb': ['#898D86', '#99a83d'], + 'rocks4.glb': ['#898D86', '#99a83d', '#6BC6EF', '#f9f9f9'], + 'pond.glb': ['#00E704', '#5A91E7', '#9A9A9A'], + + 'hut.glb': ['#B66946', '#5F2524', '#C25A5C', '#E1B46E', '#3BACBA', '#878787'], + 'hut2.glb': ['#814C22', '#231E1D', '#FFF6A6', '#E7AF3A', '#E73627', '#878787'], + 'hut3.glb': ['#F6DAB6', '#6CC3C1', '#9DC45C', '#EEB975', '#F3B4BE', '#878787'], + 'hut4.glb': ['#F2E8CF', '#BC4749', '#EEB975', '#AF1B3F', '#6A994E', '#878787'], + + 'boat.glb': [ + '#4F8A46', + '#E7D48E', + '#E76635', + '#E76C69', + '#5E64E7', + '#4A4A4A', + '#AAAAAA', + '#E711CD', ], - "airplane.glb": ["#E75D43", "#6A6A6A", "#E7C777"], + 'airplane.glb': ['#E75D43', '#6A6A6A', '#E7C777'], - "airplane2.glb": ["#6A6A6A", "#E75D43", "#E7C777"], + 'airplane2.glb': ['#6A6A6A', '#E75D43', '#E7C777'], - "skateboard.glb": ["#E769D3", "#484848", "#251BE7"], + 'skateboard.glb': ['#E769D3', '#484848', '#251BE7'], - "rhino.glb": ["#6D6B6C", "#F6F6F6", "#373737", "#230F0F"], - "lion.glb": ["#000000", "#DECC9C", "#8A4900", "#C69452"], + 'rhino.glb': ['#6D6B6C', '#F6F6F6', '#373737', '#230F0F'], + 'lion.glb': ['#000000', '#DECC9C', '#8A4900', '#C69452'], - "humped.glb": ["#FFA869", "#7E5024", "#E76F31"], - "jetty.glb": ["#FFA869", "#7E5024", "#E76F31"], - "flower.glb": ["#E73F9F", "#4AB700", "#E7D535"], - "flower2.glb": ["#E73F9F", "#E7774E", "#E7D535"], + 'humped.glb': ['#FFA869', '#7E5024', '#E76F31'], + 'jetty.glb': ['#FFA869', '#7E5024', '#E76F31'], + 'flower.glb': ['#E73F9F', '#4AB700', '#E7D535'], + 'flower2.glb': ['#E73F9F', '#E7774E', '#E7D535'], //"stones_straight.glb": ["#E73F9F", "#4AB700", "#E7D535","#dce0d9", "#fbf6ef", "#ead7c3"], //"stones_curve.glb": ["#E73F9F", "#4AB700", "#E7D535","#dce0d9", "#fbf6ef", "#ead7c3"], }; export const modelNames = [ - "Flock.glb", + 'Flock.glb', //"Flock_Santa.glb", //"Character.glb", //"bird.glb", //"boat.glb", - "lion.glb", - "rhino.glb", + 'lion.glb', + 'rhino.glb', //"Seagull.glb", ]; export const blockNames = [ - "Character1.glb", - "Character2.glb", - "Character3.glb", - "Character4.glb", - "Flock.glb", - "Flock_Santa.glb", - "Character.glb", - "lion.glb", - "rhino.glb", + 'Character1.glb', + 'Character2.glb', + 'Character3.glb', + 'Character4.glb', + 'Flock.glb', + 'Flock_Santa.glb', + 'Character.glb', + 'lion.glb', + 'rhino.glb', //"Seagull.glb", ]; export const modelAnimationNames = [ - "Flock.glb", - "Flock_Santa.glb", - "rhino.glb", - "lion.glb", + 'Flock.glb', + 'Flock_Santa.glb', + 'rhino.glb', + 'lion.glb', //"Seagull.glb", ]; export function mapNames() { return [ - getDropdownOption("circular_depression.png"), - getDropdownOption("checkerboard.png"), - getDropdownOption("sloped_plane.png"), - getDropdownOption("cove_plateau.png"), - getDropdownOption("random_hills.png"), - getDropdownOption("diagonal_ridge.png"), - getDropdownOption("mixed_heights.png"), - getDropdownOption("uneven_terrain.png"), - getDropdownOption("mountains.png"), - getDropdownOption("Islands.png"), - getDropdownOption("Lookout.png"), - getDropdownOption("Valley.png"), + getDropdownOption('circular_depression.png'), + getDropdownOption('checkerboard.png'), + getDropdownOption('sloped_plane.png'), + getDropdownOption('cove_plateau.png'), + getDropdownOption('random_hills.png'), + getDropdownOption('diagonal_ridge.png'), + getDropdownOption('mixed_heights.png'), + getDropdownOption('uneven_terrain.png'), + getDropdownOption('mountains.png'), + getDropdownOption('Islands.png'), + getDropdownOption('Lookout.png'), + getDropdownOption('Valley.png'), ]; } export function animationNames() { return [ - getDropdownOption("Idle"), - getDropdownOption("Walk"), - getDropdownOption("Run"), - getDropdownOption("Wave"), - getDropdownOption("Yes"), - getDropdownOption("No"), - getDropdownOption("Duck"), - getDropdownOption("Fall"), - getDropdownOption("Fly"), - getDropdownOption("Jump"), - getDropdownOption("JumpUp"), - getDropdownOption("JumpIdle"), - getDropdownOption("JumpLand"), - getDropdownOption("Flip"), - getDropdownOption("Dance1"), - getDropdownOption("Dance2"), - getDropdownOption("Dance3"), - getDropdownOption("Dance4"), - getDropdownOption("Punch"), - getDropdownOption("HitReact"), - getDropdownOption("Idle_Hold"), - getDropdownOption("Walk_Hold"), - getDropdownOption("Run_Hold"), - getDropdownOption("Sit_Down"), - getDropdownOption("Sitting"), - getDropdownOption("Stand_Up"), - getDropdownOption("Wobble"), - getDropdownOption("Clap"), - getDropdownOption("Climb_rope"), + getDropdownOption('Idle'), + getDropdownOption('Walk'), + getDropdownOption('Run'), + getDropdownOption('Wave'), + getDropdownOption('Yes'), + getDropdownOption('No'), + getDropdownOption('Duck'), + getDropdownOption('Fall'), + getDropdownOption('Fly'), + getDropdownOption('Jump'), + getDropdownOption('JumpUp'), + getDropdownOption('JumpIdle'), + getDropdownOption('JumpLand'), + getDropdownOption('Flip'), + getDropdownOption('Dance1'), + getDropdownOption('Dance2'), + getDropdownOption('Dance3'), + getDropdownOption('Dance4'), + getDropdownOption('Punch'), + getDropdownOption('HitReact'), + getDropdownOption('Idle_Hold'), + getDropdownOption('Walk_Hold'), + getDropdownOption('Run_Hold'), + getDropdownOption('Sit_Down'), + getDropdownOption('Sitting'), + getDropdownOption('Stand_Up'), + getDropdownOption('Wobble'), + getDropdownOption('Clap'), + getDropdownOption('Climb_rope'), ]; } export const materialNames = [ - "none.png", - "arrows.png", - "bricks.png", - "carbonfibre.png", - "carpet.png", - "circles.png", - "eyeball.png", - "fabric.png", - "fishes.png", - "fish_above.png", - "flowers.png", - "flower_tile.png", - "fruit.png", - "grass.png", - "gravel.png", - "Grid.png", - "hedge.png", - "jigsaw.png", - "leaves.png", - "marble.png", - "mosaic.png", - "mushroom.png", - "planks.png", - "road.png", - "rough.png", - "shapes.png", - "squares.png", - "stars.png", - "stripes.png", - "swirl.png", - "tiles.png", - "triangles.png", - "wiggles.png", - "windmill.png", - "wood.png", - "gridxy.png", + 'none.png', + 'arrows.png', + 'bricks.png', + 'carbonfibre.png', + 'carpet.png', + 'circles.png', + 'eyeball.png', + 'fabric.png', + 'fishes.png', + 'fish_above.png', + 'flowers.png', + 'flower_tile.png', + 'fruit.png', + 'grass.png', + 'gravel.png', + 'Grid.png', + 'hedge.png', + 'jigsaw.png', + 'leaves.png', + 'marble.png', + 'mosaic.png', + 'mushroom.png', + 'planks.png', + 'road.png', + 'rough.png', + 'shapes.png', + 'squares.png', + 'stars.png', + 'stripes.png', + 'swirl.png', + 'tiles.png', + 'triangles.png', + 'wiggles.png', + 'windmill.png', + 'wood.png', + 'gridxy.png', ]; export const attachNames = [ - "LeftHand", - "RightHand", - "Head", - "Hips", - "Spine", - "Spine1", - "Spine2", - "Neck", - "LeftShoulder", - "LeftArm", - "LeftForeArm", - "RightShoulder", - "RightArm", - "RightForeArm", - "LeftUpLeg", - "LeftLeg", - "LeftFoot", - "RightUpLeg", - "RightLeg", - "RightFoot", + 'LeftHand', + 'RightHand', + 'Head', + 'Hips', + 'Spine', + 'Spine1', + 'Spine2', + 'Neck', + 'LeftShoulder', + 'LeftArm', + 'LeftForeArm', + 'RightShoulder', + 'RightArm', + 'RightForeArm', + 'LeftUpLeg', + 'LeftLeg', + 'LeftFoot', + 'RightUpLeg', + 'RightLeg', + 'RightFoot', ]; export const attachBlockMapping = { - LeftHand: "Hold", - Hold: "Hold", - Head: "Head", + LeftHand: 'Hold', + Hold: 'Hold', + Head: 'Head', }; // Mixamo bone names are from the character's own perspective, but GLTF // import mirrors the model, so left/right are swapped from the viewer's // perspective. LeftHand (viewer's left) maps to mixamorig:RightHand, etc. -export const AUTOSAVE_KEY = "flock_autosave.flock"; +export const AUTOSAVE_KEY = 'flock_autosave.flock'; // Feature flag for autosaving to the last explicitly-saved file. localStorage // autosave is unaffected by this flag. export const AUTOSAVE_TO_FILE_ENABLED = false; export const attachMixamoMapping = { - LeftHand: "mixamorig:RightHand", - Hold: "mixamorig:RightHand", - RightHand: "mixamorig:LeftHand", - Head: "mixamorig:Head", - Hips: "mixamorig:Hips", - Spine: "mixamorig:Spine", - Spine1: "mixamorig:Spine1", - Spine2: "mixamorig:Spine2", - Neck: "mixamorig:Neck", - LeftShoulder: "mixamorig:RightShoulder", - LeftArm: "mixamorig:RightArm", - LeftForeArm: "mixamorig:RightForeArm", - RightShoulder: "mixamorig:LeftShoulder", - RightArm: "mixamorig:LeftArm", - RightForeArm: "mixamorig:LeftForeArm", - LeftUpLeg: "mixamorig:RightUpLeg", - LeftLeg: "mixamorig:RightLeg", - LeftFoot: "mixamorig:RightFoot", - RightUpLeg: "mixamorig:LeftUpLeg", - RightLeg: "mixamorig:LeftLeg", - RightFoot: "mixamorig:LeftFoot", + LeftHand: 'mixamorig:RightHand', + Hold: 'mixamorig:RightHand', + RightHand: 'mixamorig:LeftHand', + Head: 'mixamorig:Head', + Hips: 'mixamorig:Hips', + Spine: 'mixamorig:Spine', + Spine1: 'mixamorig:Spine1', + Spine2: 'mixamorig:Spine2', + Neck: 'mixamorig:Neck', + LeftShoulder: 'mixamorig:RightShoulder', + LeftArm: 'mixamorig:RightArm', + LeftForeArm: 'mixamorig:RightForeArm', + RightShoulder: 'mixamorig:LeftShoulder', + RightArm: 'mixamorig:LeftArm', + RightForeArm: 'mixamorig:LeftForeArm', + LeftUpLeg: 'mixamorig:RightUpLeg', + LeftLeg: 'mixamorig:RightLeg', + LeftFoot: 'mixamorig:RightFoot', + RightUpLeg: 'mixamorig:LeftUpLeg', + RightLeg: 'mixamorig:LeftLeg', + RightFoot: 'mixamorig:LeftFoot', }; export function getAttachNames() { diff --git a/cubeart.html b/cubeart.html index 199e8994..d196c203 100644 --- a/cubeart.html +++ b/cubeart.html @@ -8,8 +8,7 @@ @@ -86,7 +85,7 @@ diff --git a/dev-docs/API_QUALITY_TOOLS.md b/dev-docs/API_QUALITY_TOOLS.md index 262aa81e..78fbfe33 100644 --- a/dev-docs/API_QUALITY_TOOLS.md +++ b/dev-docs/API_QUALITY_TOOLS.md @@ -182,7 +182,7 @@ node scripts/utils/test-analyzer.mjs **Example:** ```javascript - flock.methodName("value", options); + flock.methodName('value', options); ``` ```` @@ -260,19 +260,19 @@ node scripts/utils/test-analyzer.mjs 4. Follow existing test patterns: ```javascript - describe("Mesh operations @mesh", function () { + describe('Mesh operations @mesh', function () { let testMesh; beforeEach(function () { - testMesh = flock.createBox("testBox"); + testMesh = flock.createBox('testBox'); }); afterEach(function () { flock.dispose(testMesh); }); - it("should attach mesh to parent", function (done) { - const parent = flock.createBox("parent"); + it('should attach mesh to parent', function (done) { + const parent = flock.createBox('parent'); flock.attach(testMesh, parent); setTimeout(() => { diff --git a/dev-docs/API_RECONCILIATION_PLAN.md b/dev-docs/API_RECONCILIATION_PLAN.md index c48ff0d2..9ee21769 100644 --- a/dev-docs/API_RECONCILIATION_PLAN.md +++ b/dev-docs/API_RECONCILIATION_PLAN.md @@ -211,16 +211,16 @@ createBox(boxId, options = {}) { ```javascript // tests/sound.test.js (auto-generated stub) -import { expect } from "chai"; +import { expect } from 'chai'; export function runSoundTests(flock) { - describe("Sound API Methods @sound", function () { - describe("playNotes function", function () { - it("should exist and be callable", function () { - expect(typeof flock.playNotes).to.equal("function"); + describe('Sound API Methods @sound', function () { + describe('playNotes function', function () { + it('should exist and be callable', function () { + expect(typeof flock.playNotes).to.equal('function'); }); - it.skip("should play notes with valid parameters", function () { + it.skip('should play notes with valid parameters', function () { // TODO: Implement test // Example usage: // await flock.playNotes({ @@ -229,17 +229,17 @@ export function runSoundTests(flock) { // }); }); - it.skip("should handle invalid parameters gracefully", function () { + it.skip('should handle invalid parameters gracefully', function () { // TODO: Test error handling }); }); - describe("setBPM function", function () { - it("should exist and be callable", function () { - expect(typeof flock.setBPM).to.equal("function"); + describe('setBPM function', function () { + it('should exist and be callable', function () { + expect(typeof flock.setBPM).to.equal('function'); }); - it.skip("should set beats per minute", function () { + it.skip('should set beats per minute', function () { // TODO: Implement test }); }); @@ -277,7 +277,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: "18" + node-version: '18' - name: Install dependencies run: npm ci diff --git a/docs/screen-reader.md b/docs/screen-reader.md index 8118f230..284d1a8e 100644 --- a/docs/screen-reader.md +++ b/docs/screen-reader.md @@ -4,13 +4,13 @@ Flock XR has an intial prototype of built-in screen reader support, enabling users who rely on assistive technology to navigate 3D worlds, interact with objects, and build programs. The implementation uses standard ARIA live regions and works with major screen readers including Windows Narrator, NVDA, JAWS, and VoiceOver. -This functionality is available in the [development version of Flock XR](https://flipcomputing.github.io/flock/) which is under active development. If you're interested in a more stable version for trying out the features then do get in touch. +This functionality is available in the [development version of Flock XR](https://flipcomputing.github.io/flock/) which is under active development. If you're interested in a more stable version for trying out the features then do get in touch. -Flock XR also has initial screen reader support for creating project using Blockly blocks using Blockly v13. Additional work is ongoing to integrate this functionality into Flock XR so that creating projects is fully screen reader accessible. +Flock XR also has initial screen reader support for creating project using Blockly blocks using Blockly v13. Additional work is ongoing to integrate this functionality into Flock XR so that creating projects is fully screen reader accessible. -Flock XR supports spatial audio so sounds can be attached to meshes in 3D space and the volume and direction changes based on the proximity of the player to the source. +Flock XR supports spatial audio so sounds can be attached to meshes in 3D space and the volume and direction changes based on the proximity of the player to the source. -The 2D UI componentsthat can be used in Flock XR are accessible by using tab and then interacting. This can be used to build text-based programs that are screen reader accessible. +The 2D UI componentsthat can be used in Flock XR are accessible by using tab and then interacting. This can be used to build text-based programs that are screen reader accessible. Many more features are on our roadmap, subject to funding, making Flock XR a viable coding environment for blind and visually impaired learners. @@ -92,7 +92,7 @@ The visual interact indicator appears when an interactable object is within appr ### 2D UI components -Flock XR programs can include 2D UI components such as text input fields, sliders, and buttons. +Flock XR programs can include 2D UI components such as text input fields, sliders, and buttons. - **Tab** from the canvas to move focus into the UI controls. - **Shift+Tab** moves backwards, returning to the canvas at either end. @@ -104,9 +104,9 @@ The tab order of UI controls reflects the order in which they are added. ## Creating projects with Blockly -Blockly v13 is adding screen reader support for creating projects using a screen reader. Flock XR has partial support for this and we're hoping to add full support as soon as we can by fully integrating with Flock XR blockly blocks. +Blockly v13 is adding screen reader support for creating projects using a screen reader. Flock XR has partial support for this and we're hoping to add full support as soon as we can by fully integrating with Flock XR blockly blocks. -Screen reader support builds on keyboard controls support for Blockly. Keyboard navigation is currently being added to the Flock XR UI and Blockly blocks. +Screen reader support builds on keyboard controls support for Blockly. Keyboard navigation is currently being added to the Flock XR UI and Blockly blocks. ### Sound blocks @@ -143,25 +143,27 @@ Flock XR programs can display text to the player using **say** and **print** blo - **Say blocks with a duration** — text is live-announced immediately when the say block fires, and is also included in the nearest object description (Ctrl+J) for as long as it is displayed. - **Say with duration 0** (persistent say) — the text is not live-announced, but is stored as a permanent description for the object and becomes part of the nearest object description when you use Ctrl+J. This is useful for setting a standing description that players can discover when they approach. -Flock XR also has speech blocks that can generate speech from text. +Flock XR also has speech blocks that can generate speech from text. ### Text projects + The 2D controls mean that Flock XR can be used to build text-based programs — games and interactive stories that work through text output and input without relying on the 3D scene visually. Using print blocks, say blocks, and UI input blocks, it is possible to create projects that are usable by screen reader users. ## Future -There's so much we can do in Flock XR to allow the creation and exploration of 3D spatial worlds using a screen reader. -This includes adding sonification to help users navigate, adding improved music composition and spatial audio support and adding haptic feedback through low cost games controllers. And probably lots of things we haven't thought of yet. +There's so much we can do in Flock XR to allow the creation and exploration of 3D spatial worlds using a screen reader. + +This includes adding sonification to help users navigate, adding improved music composition and spatial audio support and adding haptic feedback through low cost games controllers. And probably lots of things we haven't thought of yet. -Flock XR enables young people to have creative expression. It can also be used to increase awareness of screen reader use so that young people can themselves create projects that are accessible to others. +Flock XR enables young people to have creative expression. It can also be used to increase awareness of screen reader use so that young people can themselves create projects that are accessible to others. -Spatial computing is increasingly important in society and industry with some key capabilities that are relevant to users who are blind or visually impaired. Flock XR has the potential to offer virtual experiences that enable users to practice skills such as tech-supported 3D navigation. +Spatial computing is increasingly important in society and industry with some key capabilities that are relevant to users who are blind or visually impaired. Flock XR has the potential to offer virtual experiences that enable users to practice skills such as tech-supported 3D navigation. ## Credits -Screen reader support for creating Blockly projects in Flock XR is possible through [Blockly's keyboard navigation and screen reader support](https://www.blockly.com/accessibility). Tracy (creator and maintainer of Flock XR played a key role in this work through the [micro:bit Educational Foundation](https://microbit.org/accessibility/microsoft-makecode/)), working closely with a youth panel. +Screen reader support for creating Blockly projects in Flock XR is possible through [Blockly's keyboard navigation and screen reader support](https://www.blockly.com/accessibility). Tracy (creator and maintainer of Flock XR played a key role in this work through the [micro:bit Educational Foundation](https://microbit.org/accessibility/microsoft-makecode/)), working closely with a youth panel. -Support for adding keyboard controls support to Flock XR is ongoing and supported by [NLnet](https://nlnet.nl/project/FlockXR-a11y-mobile-UX/). +Support for adding keyboard controls support to Flock XR is ongoing and supported by [NLnet](https://nlnet.nl/project/FlockXR-a11y-mobile-UX/). The initial screen reader accessibility prototype for Flock XR was developed by **Esther Mbugua**, a final year Computer Science student at the University of Sheffield. @@ -176,7 +178,7 @@ We continually use the [RNIB Gaming Devkit](https://github.com/RNIB-MediaAndCult Flock XR is open-source and accessibility work is ongoing. We are actively looking for sponsorship to continue developing screen reader support, improve the Blockly editor experience, and make Flock XR more widely accessible. -We're really excited about what is possible here, but we're going to need support to complete this work to a high standard including working closely with users who have a deep understanding of how this capability needs to work. +We're really excited about what is possible here, but we're going to need support to complete this work to a high standard including working closely with users who have a deep understanding of how this capability needs to work. - [Sponsor Flock XR on GitHub Sponsors](https://github.com/sponsors/flipcomputing) - [Get in touch via flockxr.com](https://flipcomputing.com/contact/) diff --git a/embed-example.html b/embed-example.html index 1beb9abd..b122871d 100644 --- a/embed-example.html +++ b/embed-example.html @@ -1,29 +1,29 @@ - - - - Flock XR embed example - - - -

Flock XR embed example

+ + + + Flock XR embed example + + + +

Flock XR embed example

- + -

Copy/paste embed snippet

-
<iframe
+    

Copy/paste embed snippet

+
<iframe
 		  title="Flock XR embedded project player"
 		  width="604"
 		  height="420"
@@ -31,5 +31,5 @@ 

Copy/paste embed snippet

allow="fullscreen; xr-spatial-tracking; accelerometer; gyroscope" src="https://your-host/flock/index.html?embed=true&size=default&controls=playstop&project=YOUR_RAW_FLOCK_URL" ></iframe>
- + diff --git a/eslint.config.mjs b/eslint.config.mjs index 1be2ec50..6e9a170b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,33 +1,33 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; +import globals from 'globals'; +import pluginJs from '@eslint/js'; /** @type {import('eslint').Linter.Config[]} */ export default [ { ignores: [ - "node_modules/", - "dev-dist/", - "dist/", - "examples/", - ".local/", - "**/.local/", - "test-playwright.js", - "test-visual.js", + 'node_modules/', + 'dev-dist/', + 'dist/', + 'examples/', + '.local/', + '**/.local/', + 'test-playwright.js', + 'test-visual.js', ], }, { languageOptions: { globals: globals.browser } }, { - files: ["tests/**/*.test.js"], + files: ['tests/**/*.test.js'], languageOptions: { globals: { ...globals.chai, ...globals.mocha, - chai: "readonly", + chai: 'readonly', }, }, }, { - files: ["scripts/**/*.mjs", "scripts/**/*.js", "**/scripts/**/*.js"], + files: ['scripts/**/*.mjs', 'scripts/**/*.js', '**/scripts/**/*.js'], languageOptions: { globals: { ...globals.node, @@ -36,12 +36,12 @@ export default [ }, { files: [ - "*.config.js", - "*.config.mjs", - "*.config.cjs", - "**/*.config.js", - "**/*.config.mjs", - "**/*.config.cjs", + '*.config.js', + '*.config.mjs', + '*.config.cjs', + '**/*.config.js', + '**/*.config.mjs', + '**/*.config.cjs', ], languageOptions: { globals: { @@ -50,9 +50,9 @@ export default [ }, }, { - files: ["vite.config.js"], + files: ['vite.config.js'], languageOptions: { - sourceType: "module", + sourceType: 'module', globals: { ...globals.node, }, @@ -61,7 +61,7 @@ export default [ pluginJs.configs.recommended, { rules: { - "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], }, }, ]; diff --git a/generators/generators-animate.js b/generators/generators-animate.js index 77596c95..c62084a3 100644 --- a/generators/generators-animate.js +++ b/generators/generators-animate.js @@ -1,138 +1,116 @@ -import * as Blockly from "blockly"; -import { getFieldValue } from "./generators-utilities.js"; +import * as Blockly from 'blockly'; +import { getFieldValue } from './generators-utilities.js'; export function registerAnimateGenerators(javascriptGenerator) { // ------------------------------- // ANIMATE // ------------------------------- // Switch animation to - javascriptGenerator.forBlock["switch_animation"] = function (block) { + javascriptGenerator.forBlock['switch_animation'] = function (block) { const model = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL'), + Blockly.Names.NameType.VARIABLE ); const animationName = - javascriptGenerator.valueToCode( - block, - "ANIMATION_NAME", - javascriptGenerator.ORDER_NONE, - ) || '"Idle"'; + javascriptGenerator.valueToCode(block, 'ANIMATION_NAME', javascriptGenerator.ORDER_NONE) || + '"Idle"'; const code = `switchAnimation(${model}, { animationName: ${animationName} });\n`; return code; }; // Play animation on object - javascriptGenerator.forBlock["play_animation"] = function (block) { + javascriptGenerator.forBlock['play_animation'] = function (block) { const model = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL'), + Blockly.Names.NameType.VARIABLE ); const animationName = - javascriptGenerator.valueToCode( - block, - "ANIMATION_NAME", - javascriptGenerator.ORDER_NONE, - ) || '"Idle"'; + javascriptGenerator.valueToCode(block, 'ANIMATION_NAME', javascriptGenerator.ORDER_NONE) || + '"Idle"'; const code = `await playAnimation(${model}, { animationName: ${animationName} });\n`; return code; }; // Animation name - javascriptGenerator.forBlock["animation_name"] = function (block) { - const animationName = block.getFieldValue("ANIMATION_NAME"); + javascriptGenerator.forBlock['animation_name'] = function (block) { + const animationName = block.getFieldValue('ANIMATION_NAME'); return [`"${animationName}"`, javascriptGenerator.ORDER_ATOMIC]; }; // Glide to coordinates - javascriptGenerator.forBlock["glide_to_seconds"] = function (block) { + javascriptGenerator.forBlock['glide_to_seconds'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); - const duration = getFieldValue(block, "DURATION", "0"); - const mode = block.getFieldValue("MODE"); - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const easing = block.getFieldValue("EASING"); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); + const duration = getFieldValue(block, 'DURATION', '0'); + const mode = block.getFieldValue('MODE'); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const easing = block.getFieldValue('EASING'); - const asyncWrapper = mode === "AWAIT" ? "await " : ""; + const asyncWrapper = mode === 'AWAIT' ? 'await ' : ''; return `${asyncWrapper}glideTo(${meshName}, { x: ${x}, y: ${y}, z: ${z}, duration: ${duration}, reverse: ${reverse}, loop: ${loop}, easing: "${easing}" });\n`; }; // Glide to object - javascriptGenerator.forBlock["glide_to_object"] = function (block) { + javascriptGenerator.forBlock['glide_to_object'] = function (block) { const meshName1 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL1"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL1'), + Blockly.Names.NameType.VARIABLE ); const meshName2 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL2"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL2'), + Blockly.Names.NameType.VARIABLE ); const xOffset = - javascriptGenerator.valueToCode( - block, - "X_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'X_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const yOffset = - javascriptGenerator.valueToCode( - block, - "Y_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Y_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const zOffset = - javascriptGenerator.valueToCode( - block, - "Z_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const duration = getFieldValue(block, "DURATION", "0"); - const mode = block.getFieldValue("MODE"); - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const easing = block.getFieldValue("EASING"); - const asyncWrapper = mode === "AWAIT" ? "await " : ""; + javascriptGenerator.valueToCode(block, 'Z_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; + const duration = getFieldValue(block, 'DURATION', '0'); + const mode = block.getFieldValue('MODE'); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const easing = block.getFieldValue('EASING'); + const asyncWrapper = mode === 'AWAIT' ? 'await ' : ''; return `${asyncWrapper}glideToObject(${meshName1}, ${meshName2}, { offsetX: ${xOffset}, offsetY: ${yOffset}, offsetZ: ${zOffset}, duration: ${duration}, reverse: ${reverse}, loop: ${loop}, easing: "${easing}" });\n`; }; // Glide along a single axis, fixing other axes to current position - javascriptGenerator.forBlock["glide_to_axis"] = function (block) { + javascriptGenerator.forBlock['glide_to_axis'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const axis = block.getFieldValue("AXIS"); + const axis = block.getFieldValue('AXIS'); const target = - javascriptGenerator.valueToCode( - block, - "TARGET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const duration = getFieldValue(block, "DURATION", "0"); - const mode = block.getFieldValue("MODE"); - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const easing = block.getFieldValue("EASING"); - - const asyncWrapper = mode === "AWAIT" ? "await " : ""; - - if (axis === "forward" || axis === "sideways") { + javascriptGenerator.valueToCode(block, 'TARGET', javascriptGenerator.ORDER_ATOMIC) || '0'; + const duration = getFieldValue(block, 'DURATION', '0'); + const mode = block.getFieldValue('MODE'); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const easing = block.getFieldValue('EASING'); + + const asyncWrapper = mode === 'AWAIT' ? 'await ' : ''; + + if (axis === 'forward' || axis === 'sideways') { return `${asyncWrapper}glideDirection(${meshName}, { direction: "${axis}", distance: ${target}, duration: ${duration}, reverse: ${reverse}, loop: ${loop}, easing: "${easing}" });\n`; } let x, y, z; - if (axis === "x") { + if (axis === 'x') { x = target; y = `"__current__"`; z = `"__current__"`; - } else if (axis === "y") { + } else if (axis === 'y') { x = `"__current__"`; y = target; z = `"__current__"`; @@ -146,54 +124,53 @@ export function registerAnimateGenerators(javascriptGenerator) { }; // Rotate object to coordinates - javascriptGenerator.forBlock["rotate_anim_seconds"] = function (block) { + javascriptGenerator.forBlock['rotate_anim_seconds'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const rotX = getFieldValue(block, "ROT_X", "0"); - const rotY = getFieldValue(block, "ROT_Y", "0"); - const rotZ = getFieldValue(block, "ROT_Z", "0"); - const duration = getFieldValue(block, "DURATION", "0"); - const mode = block.getFieldValue("MODE"); - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const easing = block.getFieldValue("EASING"); + const rotX = getFieldValue(block, 'ROT_X', '0'); + const rotY = getFieldValue(block, 'ROT_Y', '0'); + const rotZ = getFieldValue(block, 'ROT_Z', '0'); + const duration = getFieldValue(block, 'DURATION', '0'); + const mode = block.getFieldValue('MODE'); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const easing = block.getFieldValue('EASING'); - const asyncWrapper = mode === "AWAIT" ? "await " : ""; + const asyncWrapper = mode === 'AWAIT' ? 'await ' : ''; return `${asyncWrapper}rotateAnim(${meshName}, { x: ${rotX}, y: ${rotY}, z: ${rotZ}, duration: ${duration}, reverse: ${reverse}, loop: ${loop}, easing: "${easing}" });\n`; }; // Rotate object towards object - javascriptGenerator.forBlock["rotate_to_object"] = function (block) { + javascriptGenerator.forBlock['rotate_to_object'] = function (block) { const meshName1 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL1"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL1'), + Blockly.Names.NameType.VARIABLE ); const meshName2 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL2"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL2'), + Blockly.Names.NameType.VARIABLE ); - const rotateMode = block.getFieldValue("ROTATE_MODE"); - const apiRotateMode = - rotateMode === "SAME_ROTATION" ? "same_rotation" : "towards"; - const duration = getFieldValue(block, "DURATION", "0"); - const mode = block.getFieldValue("MODE"); - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const easing = block.getFieldValue("EASING"); + const rotateMode = block.getFieldValue('ROTATE_MODE'); + const apiRotateMode = rotateMode === 'SAME_ROTATION' ? 'same_rotation' : 'towards'; + const duration = getFieldValue(block, 'DURATION', '0'); + const mode = block.getFieldValue('MODE'); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const easing = block.getFieldValue('EASING'); - const asyncWrapper = mode === "AWAIT" ? "await " : ""; + const asyncWrapper = mode === 'AWAIT' ? 'await ' : ''; return `${asyncWrapper}rotateToObject(${meshName1}, ${meshName2}, { mode: "${apiRotateMode}", duration: ${duration}, reverse: ${reverse}, loop: ${loop}, easing: "${easing}" });\n`; }; // Stop animations on object - javascriptGenerator.forBlock["stop_animations"] = function (block) { + javascriptGenerator.forBlock['stop_animations'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); return `await stopAnimations(${modelName});\n`; @@ -202,76 +179,65 @@ export function registerAnimateGenerators(javascriptGenerator) { // KEYFRAME // ------------------------------- // Animate keyframes on object group - javascriptGenerator.forBlock["animation"] = function (block) { + javascriptGenerator.forBlock['animation'] = function (block) { const meshVariable = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH'), + Blockly.Names.NameType.VARIABLE ); - const property = block.getFieldValue("PROPERTY"); + const property = block.getFieldValue('PROPERTY'); const animationGroupVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("ANIMATION_GROUP"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('ANIMATION_GROUP'), + Blockly.Names.NameType.VARIABLE ); - const keyframesBlock = block.getInputTargetBlock("KEYFRAMES"); + const keyframesBlock = block.getInputTargetBlock('KEYFRAMES'); const keyframesArray = []; if (keyframesBlock) { // Loop through keyframe blocks to gather data for (let i = 0; i < keyframesBlock.inputList.length; i++) { const keyframeInput = keyframesBlock.inputList[i]; - const valueBlock = keyframeInput.connection - ? keyframeInput.connection.targetBlock() - : null; + const valueBlock = keyframeInput.connection ? keyframeInput.connection.targetBlock() : null; let value; if (valueBlock) { // If the keyframe block is of type "colour_keyframe", treat it as a color keyframe. - if (valueBlock.type === "colour_keyframe") { + if (valueBlock.type === 'colour_keyframe') { value = javascriptGenerator.valueToCode( valueBlock, - "VALUE", - javascriptGenerator.ORDER_NONE, + 'VALUE', + javascriptGenerator.ORDER_NONE ); - } else if (property === "color") { + } else if (property === 'color') { // Otherwise, if property equals "color", extract as a color. value = javascriptGenerator.valueToCode( valueBlock, - "VALUE", - javascriptGenerator.ORDER_NONE, + 'VALUE', + javascriptGenerator.ORDER_NONE ); - } else if (["position", "rotation", "scaling"].includes(property)) { + } else if (['position', 'rotation', 'scaling'].includes(property)) { // For vector keyframes, extract X, Y, and Z. const x = - javascriptGenerator.valueToCode( - valueBlock, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(valueBlock, 'X', javascriptGenerator.ORDER_ATOMIC) || + 0; const y = - javascriptGenerator.valueToCode( - valueBlock, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(valueBlock, 'Y', javascriptGenerator.ORDER_ATOMIC) || + 0; const z = - javascriptGenerator.valueToCode( - valueBlock, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(valueBlock, 'Z', javascriptGenerator.ORDER_ATOMIC) || + 0; value = `createVector3(${x}, ${y}, ${z})`; } else { // Handle alpha or other scalar properties. value = javascriptGenerator.valueToCode( valueBlock, - "VALUE", - javascriptGenerator.ORDER_ATOMIC, + 'VALUE', + javascriptGenerator.ORDER_ATOMIC ); } } else { // Default value for missing blocks. value = - property === "color" || property === "colour_keyframe" + property === 'color' || property === 'colour_keyframe' ? '"#ffffff"' : `createVector3(0, 0, 0)`; } @@ -283,28 +249,28 @@ export function registerAnimateGenerators(javascriptGenerator) { const duration = durationBlock ? javascriptGenerator.valueToCode( durationBlock, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, + 'DURATION', + javascriptGenerator.ORDER_ATOMIC ) - : "1"; // Default duration of 1 second if not specified + : '1'; // Default duration of 1 second if not specified keyframesArray.push({ value, duration }); } } - const easing = block.getFieldValue("EASING") || "Linear"; - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const mode = block.getFieldValue("MODE"); + const easing = block.getFieldValue('EASING') || 'Linear'; + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const mode = block.getFieldValue('MODE'); const keyframesCode = keyframesArray .map( (kf) => `{ value: ${kf.value}, duration: ${kf.duration} - }`, + }` ) - .join(", "); + .join(', '); return ` ${animationGroupVar} = await createAnimation( @@ -323,12 +289,12 @@ export function registerAnimateGenerators(javascriptGenerator) { }; // Animate keyframes on object - javascriptGenerator.forBlock["animate_keyframes"] = function (block) { + javascriptGenerator.forBlock['animate_keyframes'] = function (block) { const meshVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH'), + Blockly.Names.NameType.VARIABLE ); - const keyframesBlock = block.getInputTargetBlock("KEYFRAMES"); + const keyframesBlock = block.getInputTargetBlock('KEYFRAMES'); const keyframesArray = []; if (keyframesBlock) { @@ -336,78 +302,67 @@ export function registerAnimateGenerators(javascriptGenerator) { for (let i = 0; i < keyframesBlock.inputList.length; i++) { const keyframeInput = keyframesBlock.inputList[i]; - const valueBlock = keyframeInput.connection - ? keyframeInput.connection.targetBlock() - : null; + const valueBlock = keyframeInput.connection ? keyframeInput.connection.targetBlock() : null; const durationBlock = keyframeInput.connection ? keyframeInput.connection.targetBlock() : null; let value; - const property = block.getFieldValue("PROPERTY"); + const property = block.getFieldValue('PROPERTY'); if (valueBlock) { - if (property === "color") { + if (property === 'color') { // Handle color keyframe (as a string) value = javascriptGenerator.valueToCode( valueBlock, - "VALUE", - javascriptGenerator.ORDER_NONE, + 'VALUE', + javascriptGenerator.ORDER_NONE ); - } else if (["position", "rotation", "scaling"].includes(property)) { + } else if (['position', 'rotation', 'scaling'].includes(property)) { // Handle XYZ (Vector3) keyframe for position, rotation, or scaling const x = - javascriptGenerator.valueToCode( - valueBlock, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(valueBlock, 'X', javascriptGenerator.ORDER_ATOMIC) || + 0; const y = - javascriptGenerator.valueToCode( - valueBlock, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(valueBlock, 'Y', javascriptGenerator.ORDER_ATOMIC) || + 0; const z = - javascriptGenerator.valueToCode( - valueBlock, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(valueBlock, 'Z', javascriptGenerator.ORDER_ATOMIC) || + 0; value = `createVector3(${x}, ${y}, ${z})`; // Generate the text for Vector3, not the object itself } else { // Handle alpha or other properties value = javascriptGenerator.valueToCode( valueBlock, - "VALUE", - javascriptGenerator.ORDER_ATOMIC, + 'VALUE', + javascriptGenerator.ORDER_ATOMIC ); } } else { // Default values for missing blocks - value = property === "color" ? '"#ffffff"' : `createVector3(0, 0, 0)`; // Correct color string for colours + value = property === 'color' ? '"#ffffff"' : `createVector3(0, 0, 0)`; // Correct color string for colours } const duration = durationBlock ? javascriptGenerator.valueToCode( durationBlock, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, + 'DURATION', + javascriptGenerator.ORDER_ATOMIC ) - : "1"; // Default duration of 1 second if not specified + : '1'; // Default duration of 1 second if not specified keyframesArray.push({ value, duration }); } } - const easing = block.getFieldValue("EASING") || "Linear"; - const property = block.getFieldValue("PROPERTY") || "color"; // Default to "color" if no property is set + const easing = block.getFieldValue('EASING') || 'Linear'; + const property = block.getFieldValue('PROPERTY') || 'color'; // Default to "color" if no property is set - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const mode = block.getFieldValue("MODE"); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const mode = block.getFieldValue('MODE'); - const asyncWrapper = mode === "AWAIT" ? "await " : ""; + const asyncWrapper = mode === 'AWAIT' ? 'await ' : ''; // Generate the keyframes text for both colors and Vector3 const keyframesCode = keyframesArray @@ -415,50 +370,46 @@ export function registerAnimateGenerators(javascriptGenerator) { (kf) => `{ value: ${kf.value}, duration: ${kf.duration} - }`, + }` ) - .join(", "); + .join(', '); // Return the final code, passing keyframes with durations and properties return `${asyncWrapper}animateKeyFrames(${meshVar}, { keyframes: [${keyframesCode}], property: "${property}", easing: "${easing}", reverse: ${reverse}, loop: ${loop} });\n`; }; // Animation group - javascriptGenerator.forBlock["control_animation_group"] = function (block) { + javascriptGenerator.forBlock['control_animation_group'] = function (block) { const animationGroupName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("GROUP_NAME"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('GROUP_NAME'), + Blockly.Names.NameType.VARIABLE ); - const action = block.getFieldValue("ACTION"); + const action = block.getFieldValue('ACTION'); return `${action}AnimationGroup(${animationGroupName});\n`; }; // Animate group - javascriptGenerator.forBlock["animate_from"] = function (block) { + javascriptGenerator.forBlock['animate_from'] = function (block) { const groupVariable = javascriptGenerator.nameDB_.getName( - block.getFieldValue("GROUP_NAME"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('GROUP_NAME'), + Blockly.Names.NameType.VARIABLE ); const timeInSeconds = javascriptGenerator.valueToCode( block, - "TIME", - javascriptGenerator.ORDER_ATOMIC, + 'TIME', + javascriptGenerator.ORDER_ATOMIC ); return `animateFrom(${groupVariable}, ${timeInSeconds});\n`; }; // Keyframe colour - javascriptGenerator.forBlock["colour_keyframe"] = function (block) { - const color = javascriptGenerator.valueToCode( - block, - "COLOR", - javascriptGenerator.ORDER_ATOMIC, - ); + javascriptGenerator.forBlock['colour_keyframe'] = function (block) { + const color = javascriptGenerator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC); const duration = javascriptGenerator.valueToCode( block, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, + 'DURATION', + javascriptGenerator.ORDER_ATOMIC ); const code = `{ value: ${color}, duration: ${duration} }`; @@ -467,26 +418,14 @@ export function registerAnimateGenerators(javascriptGenerator) { // Keyframe number // Keyframe position - javascriptGenerator.forBlock["xyz_keyframe"] = function (block) { - const x = javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ); - const y = javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ); - const z = javascriptGenerator.valueToCode( - block, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ); + javascriptGenerator.forBlock['xyz_keyframe'] = function (block) { + const x = javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC); + const y = javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC); + const z = javascriptGenerator.valueToCode(block, 'Z', javascriptGenerator.ORDER_ATOMIC); const duration = javascriptGenerator.valueToCode( block, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, + 'DURATION', + javascriptGenerator.ORDER_ATOMIC ); const code = `{ value: createVector3(${x}, ${y}, ${z}), duration: ${duration} }`; return [code, javascriptGenerator.ORDER_ATOMIC]; diff --git a/generators/generators-condition.js b/generators/generators-condition.js index dc8787c1..6e47ea62 100644 --- a/generators/generators-condition.js +++ b/generators/generators-condition.js @@ -2,20 +2,20 @@ export function registerConditionGenerators(javascriptGenerator) { // ------------------------------- // CONDITION // ------------------------------- - const MODE = { IF: "IF", ELSEIF: "ELSEIF", ELSE: "ELSE" }; + const MODE = { IF: 'IF', ELSEIF: 'ELSEIF', ELSE: 'ELSE' }; // If block ---------------------------------------------------- - javascriptGenerator.forBlock["if_clause"] = function (block, generator) { - const isClause = (b) => b && b.type === "if_clause"; + javascriptGenerator.forBlock['if_clause'] = function (block, generator) { + const isClause = (b) => b && b.type === 'if_clause'; - const mode = block.getFieldValue("MODE"); + const mode = block.getFieldValue('MODE'); const prev = block.getPreviousBlock(); // A new IF always starts a new chain, even if it follows another if_clause. const isChainTop = !isClause(prev) || mode === MODE.IF; // Non-top clauses do not emit code independently. - if (!isChainTop) return ""; + if (!isChainTop) return ''; // Collect this IF plus any following ELSEIF/ELSE clauses, // but stop before the next IF (that starts a new chain). @@ -26,36 +26,32 @@ export function registerConditionGenerators(javascriptGenerator) { chain.push(cur); const next = cur.getNextBlock(); - if (next && isClause(next) && next.getFieldValue("MODE") === MODE.IF) - break; + if (next && isClause(next) && next.getFieldValue('MODE') === MODE.IF) break; cur = next; } - let code = ""; + let code = ''; const first = chain[0]; - const firstCond = - generator.valueToCode(first, "COND", generator.ORDER_NONE) || "false"; - const firstBody = generator.statementToCode(first, "DO"); + const firstCond = generator.valueToCode(first, 'COND', generator.ORDER_NONE) || 'false'; + const firstBody = generator.statementToCode(first, 'DO'); code += `if (${firstCond}) {\n${firstBody}}`; for (let i = 1; i < chain.length; i++) { const clause = chain[i]; - const clauseMode = clause.getFieldValue("MODE"); + const clauseMode = clause.getFieldValue('MODE'); if (clauseMode === MODE.ELSEIF) { - const cond = - generator.valueToCode(clause, "COND", generator.ORDER_NONE) || - "false"; - const body = generator.statementToCode(clause, "DO"); + const cond = generator.valueToCode(clause, 'COND', generator.ORDER_NONE) || 'false'; + const body = generator.statementToCode(clause, 'DO'); code += ` else if (${cond}) {\n${body}}`; continue; } if (clauseMode === MODE.ELSE) { - const body = generator.statementToCode(clause, "DO"); + const body = generator.statementToCode(clause, 'DO'); code += ` else {\n${body}}`; break; } @@ -64,7 +60,7 @@ export function registerConditionGenerators(javascriptGenerator) { if (clauseMode === MODE.IF) break; } - return code + "\n"; + return code + '\n'; }; // The following blocks use default blockly generators diff --git a/generators/generators-control.js b/generators/generators-control.js index a7b31fad..31df8699 100644 --- a/generators/generators-control.js +++ b/generators/generators-control.js @@ -1,123 +1,98 @@ -import * as Blockly from "blockly"; +import * as Blockly from 'blockly'; export function registerControlGenerators(javascriptGenerator) { // ------------------------------- // CONTROL // ------------------------------- // Wait for x seconds - javascriptGenerator.forBlock["wait_seconds"] = function (block) { + javascriptGenerator.forBlock['wait_seconds'] = function (block) { const duration = - javascriptGenerator.valueToCode( - block, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'DURATION', javascriptGenerator.ORDER_ATOMIC) || '1'; return `await wait(${duration});\n`; }; // Wait until condition is true - javascriptGenerator.forBlock["wait_until"] = function (block) { + javascriptGenerator.forBlock['wait_until'] = function (block) { const condition = - javascriptGenerator.valueToCode( - block, - "CONDITION", - javascriptGenerator.ORDER_ATOMIC, - ) || "false"; // Default to false if no condition is connected + javascriptGenerator.valueToCode(block, 'CONDITION', javascriptGenerator.ORDER_ATOMIC) || + 'false'; // Default to false if no condition is connected return `await waitUntil(() => ${condition});\n`; }; // Repeat x times - javascriptGenerator.forBlock["controls_repeat_ext"] = function ( - block, - generator, - ) { + javascriptGenerator.forBlock['controls_repeat_ext'] = function (block, generator) { let repeats; - if (block.getField("TIMES")) { - repeats = String(Number(block.getFieldValue("TIMES"))); + if (block.getField('TIMES')) { + repeats = String(Number(block.getFieldValue('TIMES'))); } else { - repeats = - generator.valueToCode(block, "TIMES", generator.ORDER_ASSIGNMENT) || - "0"; + repeats = generator.valueToCode(block, 'TIMES', generator.ORDER_ASSIGNMENT) || '0'; } - let branch = generator.statementToCode(block, "DO"); + let branch = generator.statementToCode(block, 'DO'); - let code = ""; - const loopVar = generator.nameDB_.getDistinctName( - "count", - Blockly.Names.NameType.VARIABLE, - ); + let code = ''; + const loopVar = generator.nameDB_.getDistinctName('count', Blockly.Names.NameType.VARIABLE); let endVar = repeats; if (!/^\w+$/.test(repeats) && isNaN(repeats)) { - endVar = generator.nameDB_.getDistinctName( - "repeat_end", - Blockly.Names.NameType.VARIABLE, - ); - code += "let " + endVar + " = " + repeats + ";\n"; + endVar = generator.nameDB_.getDistinctName('repeat_end', Blockly.Names.NameType.VARIABLE); + code += 'let ' + endVar + ' = ' + repeats + ';\n'; } code += - "for (let " + + 'for (let ' + loopVar + - " = 0; " + + ' = 0; ' + loopVar + - " < " + + ' < ' + endVar + - "; " + + '; ' + loopVar + - "++) {\n" + + '++) {\n' + branch + - "await wait(0);\n" + - "}\n"; + 'await wait(0);\n' + + '}\n'; return code; }; // Repeat while/until condition - javascriptGenerator.forBlock["controls_whileUntil"] = function (block) { - const until = block.getFieldValue("MODE") === "UNTIL"; + javascriptGenerator.forBlock['controls_whileUntil'] = function (block) { + const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = javascriptGenerator.valueToCode( block, - "BOOL", - until - ? javascriptGenerator.ORDER_LOGICAL_NOT - : javascriptGenerator.ORDER_NONE, - ) || "false"; - let branch = javascriptGenerator.statementToCode(block, "DO"); + 'BOOL', + until ? javascriptGenerator.ORDER_LOGICAL_NOT : javascriptGenerator.ORDER_NONE + ) || 'false'; + let branch = javascriptGenerator.statementToCode(block, 'DO'); if (until) { - argument0 = "!" + argument0; + argument0 = '!' + argument0; } - return ( - "while (" + argument0 + ") {\n" + branch + `\nawait wait(0);\n` + "}\n" - ); + return 'while (' + argument0 + ') {\n' + branch + `\nawait wait(0);\n` + '}\n'; }; // For each loop with iterator variable - javascriptGenerator.forBlock["controls_for"] = function (block, generator) { - const variable0 = generator.getVariableName(block.getFieldValue("VAR")); + javascriptGenerator.forBlock['controls_for'] = function (block, generator) { + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); - const argument0 = - generator.valueToCode(block, "FROM", generator.ORDER_ASSIGNMENT) || "0"; - const argument1 = - generator.valueToCode(block, "TO", generator.ORDER_ASSIGNMENT) || "0"; - const increment = - generator.valueToCode(block, "BY", generator.ORDER_ASSIGNMENT) || "1"; + const argument0 = generator.valueToCode(block, 'FROM', generator.ORDER_ASSIGNMENT) || '0'; + const argument1 = generator.valueToCode(block, 'TO', generator.ORDER_ASSIGNMENT) || '0'; + const increment = generator.valueToCode(block, 'BY', generator.ORDER_ASSIGNMENT) || '1'; - const branch = generator.statementToCode(block, "DO"); + const branch = generator.statementToCode(block, 'DO'); // Timing and iteration counter variables const timingVar = generator.nameDB_.getDistinctName( `${variable0}_timing`, - Blockly.Names.DEVELOPER_VARIABLE_TYPE, + Blockly.Names.DEVELOPER_VARIABLE_TYPE ); const counterVar = generator.nameDB_.getDistinctName( `${variable0}_counter`, - Blockly.Names.DEVELOPER_VARIABLE_TYPE, + Blockly.Names.DEVELOPER_VARIABLE_TYPE ); return ` @@ -135,54 +110,35 @@ export function registerControlGenerators(javascriptGenerator) { }; // For each loop iterating over list - javascriptGenerator.forBlock["controls_forEach"] = function ( - block, - generator, - ) { + javascriptGenerator.forBlock['controls_forEach'] = function (block, generator) { // For each loop. - const variable0 = generator.getVariableName(block.getFieldValue("VAR")); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); // Use correct ORDER constant from the generator - const argument0 = - generator.valueToCode(block, "LIST", generator.ORDER_ASSIGNMENT) || "[]"; + const argument0 = generator.valueToCode(block, 'LIST', generator.ORDER_ASSIGNMENT) || '[]'; - let branch = generator.statementToCode(block, "DO"); - let code = ""; + let branch = generator.statementToCode(block, 'DO'); + let code = ''; let listVar = argument0; if (!/^\w+$/.test(argument0)) { listVar = generator.nameDB_.getDistinctName( - variable0 + "_list", - Blockly.Names.NameType.VARIABLE, + variable0 + '_list', + Blockly.Names.NameType.VARIABLE ); - code += "var " + listVar + " = " + argument0 + ";\n"; + code += 'var ' + listVar + ' = ' + argument0 + ';\n'; } const indexVar = generator.nameDB_.getDistinctName( - variable0 + "_index", - Blockly.Names.NameType.VARIABLE, + variable0 + '_index', + Blockly.Names.NameType.VARIABLE ); // Construct the loop body - branch = - generator.INDENT + - variable0 + - " = " + - listVar + - "[" + - indexVar + - "];\n" + - branch; + branch = generator.INDENT + variable0 + ' = ' + listVar + '[' + indexVar + '];\n' + branch; code += - "for (var " + - indexVar + - " in " + - listVar + - ") {\n" + - branch + - "\n await wait(0);\n" + - "}\n"; + 'for (var ' + indexVar + ' in ' + listVar + ') {\n' + branch + '\n await wait(0);\n' + '}\n'; return code; }; @@ -191,11 +147,11 @@ export function registerControlGenerators(javascriptGenerator) { // ?? Uses blockly standard // Local - javascriptGenerator.forBlock["local_variable"] = function (block, generator) { + javascriptGenerator.forBlock['local_variable'] = function (block, generator) { // Retrieve the variable selected by the user const variable = generator.nameDB_.getName( - block.getFieldValue("VAR"), - Blockly.VARIABLE_CATEGORY_NAME, + block.getFieldValue('VAR'), + Blockly.VARIABLE_CATEGORY_NAME ); // Generate a local 'let' declaration for the selected variable @@ -204,13 +160,9 @@ export function registerControlGenerators(javascriptGenerator) { }; // Wait x milliseconds - javascriptGenerator.forBlock["wait"] = function (block) { + javascriptGenerator.forBlock['wait'] = function (block) { const duration = - javascriptGenerator.valueToCode( - block, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'DURATION', javascriptGenerator.ORDER_ATOMIC) || '1'; return `await wait(${duration} / 1000);\n`; }; diff --git a/generators/generators-data.js b/generators/generators-data.js index 66ef97ef..c7f3fc98 100644 --- a/generators/generators-data.js +++ b/generators/generators-data.js @@ -1,4 +1,4 @@ -import * as Blockly from "blockly"; +import * as Blockly from 'blockly'; export function registerDataGenerators(javascriptGenerator) { // ------------------------------- @@ -23,33 +23,25 @@ export function registerDataGenerators(javascriptGenerator) { // Set list to colour list // Add string to list --------------------------------------------- - javascriptGenerator.forBlock["lists_add_item"] = function (block) { + javascriptGenerator.forBlock['lists_add_item'] = function (block) { const listName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("LIST"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('LIST'), + Blockly.Names.NameType.VARIABLE ); const value = - javascriptGenerator.valueToCode( - block, - "TO", - javascriptGenerator.ORDER_ASSIGNMENT, - ) || '""'; + javascriptGenerator.valueToCode(block, 'TO', javascriptGenerator.ORDER_ASSIGNMENT) || '""'; return `if (!Array.isArray(${listName})) {\n ${listName} = [];\n}\n${listName}.push(${value});\n`; }; // Delete from list ----------------------------------------------- - javascriptGenerator.forBlock["lists_delete_nth"] = function (block) { + javascriptGenerator.forBlock['lists_delete_nth'] = function (block) { const listName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("LIST"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('LIST'), + Blockly.Names.NameType.VARIABLE ); const index = - javascriptGenerator.valueToCode( - block, - "INDEX", - javascriptGenerator.ORDER_NONE, - ) || "0"; + javascriptGenerator.valueToCode(block, 'INDEX', javascriptGenerator.ORDER_NONE) || '0'; return `if (Array.isArray(${listName})) { ${listName}.splice(${index}, 1); @@ -67,81 +59,69 @@ export function registerDataGenerators(javascriptGenerator) { // Find item in list // Get index of item in list ------------------------------------ - javascriptGenerator.forBlock["lists_getIndex"] = function (block) { - const mode = block.getFieldValue("MODE") || "GET"; - const where = block.getFieldValue("WHERE") || "FROM_START"; + javascriptGenerator.forBlock['lists_getIndex'] = function (block) { + const mode = block.getFieldValue('MODE') || 'GET'; + const where = block.getFieldValue('WHERE') || 'FROM_START'; const listOrder = - where === "RANDOM" - ? javascriptGenerator.ORDER_NONE - : javascriptGenerator.ORDER_MEMBER; - const list = - javascriptGenerator.valueToCode(block, "VALUE", listOrder) || "[]"; + where === 'RANDOM' ? javascriptGenerator.ORDER_NONE : javascriptGenerator.ORDER_MEMBER; + const list = javascriptGenerator.valueToCode(block, 'VALUE', listOrder) || '[]'; switch (where) { - case "FIRST": - if (mode === "GET") { + case 'FIRST': + if (mode === 'GET') { return [`${list}[0]`, javascriptGenerator.ORDER_MEMBER]; } - if (mode === "GET_REMOVE") { + if (mode === 'GET_REMOVE') { return [`${list}.shift()`, javascriptGenerator.ORDER_MEMBER]; } - if (mode === "REMOVE") { + if (mode === 'REMOVE') { return `${list}.shift();\n`; } break; - case "LAST": - if (mode === "GET") { + case 'LAST': + if (mode === 'GET') { return [`${list}.slice(-1)[0]`, javascriptGenerator.ORDER_MEMBER]; } - if (mode === "GET_REMOVE") { + if (mode === 'GET_REMOVE') { return [`${list}.pop()`, javascriptGenerator.ORDER_MEMBER]; } - if (mode === "REMOVE") { + if (mode === 'REMOVE') { return `${list}.pop();\n`; } break; - case "FROM_START": { - const at = javascriptGenerator.getAdjusted(block, "AT"); - if (mode === "GET") { + case 'FROM_START': { + const at = javascriptGenerator.getAdjusted(block, 'AT'); + if (mode === 'GET') { return [`${list}[${at}]`, javascriptGenerator.ORDER_MEMBER]; } - if (mode === "GET_REMOVE") { - return [ - `${list}.splice(${at}, 1)[0]`, - javascriptGenerator.ORDER_FUNCTION_CALL, - ]; + if (mode === 'GET_REMOVE') { + return [`${list}.splice(${at}, 1)[0]`, javascriptGenerator.ORDER_FUNCTION_CALL]; } - if (mode === "REMOVE") { + if (mode === 'REMOVE') { return `${list}.splice(${at}, 1);\n`; } break; } - case "FROM_END": { - const at = javascriptGenerator.getAdjusted(block, "AT", 1, true); - if (mode === "GET") { - return [ - `${list}.slice(${at})[0]`, - javascriptGenerator.ORDER_FUNCTION_CALL, - ]; + case 'FROM_END': { + const at = javascriptGenerator.getAdjusted(block, 'AT', 1, true); + if (mode === 'GET') { + return [`${list}.slice(${at})[0]`, javascriptGenerator.ORDER_FUNCTION_CALL]; } - if (mode === "GET_REMOVE") { - return [ - `${list}.splice(${at}, 1)[0]`, - javascriptGenerator.ORDER_FUNCTION_CALL, - ]; + if (mode === 'GET_REMOVE') { + return [`${list}.splice(${at}, 1)[0]`, javascriptGenerator.ORDER_FUNCTION_CALL]; } - if (mode === "REMOVE") { + if (mode === 'REMOVE') { return `${list}.splice(${at}, 1);\n`; } break; } - case "RANDOM": { + case 'RANDOM': { const functionName = javascriptGenerator.provideFunction_( - "listsGetRandomItem", + 'listsGetRandomItem', ` function ${javascriptGenerator.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { var x = randomInteger(0, list.length - 1); @@ -151,32 +131,32 @@ export function registerDataGenerators(javascriptGenerator) { return list[x]; } } - `, + ` ); - const code = `${functionName}(${list}, ${mode !== "GET"})`; - if (mode === "GET" || mode === "GET_REMOVE") { + const code = `${functionName}(${list}, ${mode !== 'GET'})`; + if (mode === 'GET' || mode === 'GET_REMOVE') { return [code, javascriptGenerator.ORDER_FUNCTION_CALL]; } - if (mode === "REMOVE") { + if (mode === 'REMOVE') { return `${code};\n`; } break; } } - throw Error("Unhandled combination (lists_getIndex)."); + throw Error('Unhandled combination (lists_getIndex).'); }; // Set index of item in list -------------------------------------- - javascriptGenerator.forBlock["lists_setIndex"] = function (block) { + javascriptGenerator.forBlock['lists_setIndex'] = function (block) { function cacheList() { if (list.match(/^\w+$/)) { - return ""; + return ''; } const listVar = javascriptGenerator.nameDB_.getDistinctName( - "tmpList", - Blockly.Names.NameType.VARIABLE, + 'tmpList', + Blockly.Names.NameType.VARIABLE ); const listAssignment = `var ${listVar} = ${list};\n`; list = listVar; @@ -184,89 +164,78 @@ export function registerDataGenerators(javascriptGenerator) { } let list = - javascriptGenerator.valueToCode( - block, - "LIST", - javascriptGenerator.ORDER_MEMBER, - ) || "[]"; - const mode = block.getFieldValue("MODE") || "GET"; - let where = block.getFieldValue("WHERE") || "FROM_START"; + javascriptGenerator.valueToCode(block, 'LIST', javascriptGenerator.ORDER_MEMBER) || '[]'; + const mode = block.getFieldValue('MODE') || 'GET'; + let where = block.getFieldValue('WHERE') || 'FROM_START'; const value = - javascriptGenerator.valueToCode( - block, - "TO", - javascriptGenerator.ORDER_ASSIGNMENT, - ) || "null"; + javascriptGenerator.valueToCode(block, 'TO', javascriptGenerator.ORDER_ASSIGNMENT) || 'null'; switch (where) { - case "FIRST": - if (mode === "SET") { + case 'FIRST': + if (mode === 'SET') { return `${list}[0] = ${value};\n`; } - if (mode === "INSERT") { + if (mode === 'INSERT') { return `${list}.unshift(${value});\n`; } break; - case "LAST": - if (mode === "SET") { + case 'LAST': + if (mode === 'SET') { return cacheList() + `${list}[${list}.length - 1] = ${value};\n`; } - if (mode === "INSERT") { + if (mode === 'INSERT') { return `${list}.push(${value});\n`; } break; - case "FROM_START": { - const index = javascriptGenerator.getAdjusted(block, "AT"); - if (mode === "SET") { + case 'FROM_START': { + const index = javascriptGenerator.getAdjusted(block, 'AT'); + if (mode === 'SET') { return `${list}[${index}] = ${value};\n`; } - if (mode === "INSERT") { + if (mode === 'INSERT') { return `${list}.splice(${index}, 0, ${value});\n`; } break; } - case "FROM_END": { + case 'FROM_END': { const index = javascriptGenerator.getAdjusted( block, - "AT", + 'AT', 1, false, - javascriptGenerator.ORDER_SUBTRACTION, + javascriptGenerator.ORDER_SUBTRACTION ); const listCache = cacheList(); - if (mode === "SET") { + if (mode === 'SET') { return listCache + `${list}[${list}.length - ${index}] = ${value};\n`; } - if (mode === "INSERT") { - return ( - listCache + - `${list}.splice(${list}.length - ${index}, 0, ${value});\n` - ); + if (mode === 'INSERT') { + return listCache + `${list}.splice(${list}.length - ${index}, 0, ${value});\n`; } break; } - case "RANDOM": { + case 'RANDOM': { let code = cacheList(); const xVar = javascriptGenerator.nameDB_.getDistinctName( - "tmpX", - Blockly.Names.NameType.VARIABLE, + 'tmpX', + Blockly.Names.NameType.VARIABLE ); code += `var ${xVar} = randomInteger(0, ${list}.length - 1);\n`; - if (mode === "SET") { + if (mode === 'SET') { return code + `${list}[${xVar}] = ${value};\n`; } - if (mode === "INSERT") { + if (mode === 'INSERT') { return code + `${list}.splice(${xVar}, 0, ${value});\n`; } break; } } - throw Error("Unhandled combination (lists_setIndex)."); + throw Error('Unhandled combination (lists_setIndex).'); }; // The following use Blockly standard implementation diff --git a/generators/generators-deprecated.js b/generators/generators-deprecated.js index 21622668..959993af 100644 --- a/generators/generators-deprecated.js +++ b/generators/generators-deprecated.js @@ -1,121 +1,88 @@ -import * as Blockly from "blockly"; -import { getFieldValue } from "./generators-utilities.js"; +import * as Blockly from 'blockly'; +import { getFieldValue } from './generators-utilities.js'; export function registerDeprecatedGenerators(javascriptGenerator) { - javascriptGenerator.forBlock["keyword_block"] = function (_block) { + javascriptGenerator.forBlock['keyword_block'] = function (_block) { // Since this block is replaced with another block, we return an empty string. - return ""; + return ''; }; - javascriptGenerator.forBlock["rotate_camera"] = function (block) { + javascriptGenerator.forBlock['rotate_camera'] = function (block) { const degrees = - javascriptGenerator.valueToCode( - block, - "DEGREES", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'DEGREES', javascriptGenerator.ORDER_ATOMIC) || '0'; return `rotateCamera(${degrees});\n`; }; - javascriptGenerator.forBlock["play_rumble_pattern"] = function (block) { - const pattern = block.getFieldValue("PATTERN"); + javascriptGenerator.forBlock['play_rumble_pattern'] = function (block) { + const pattern = block.getFieldValue('PATTERN'); return `playRumblePattern("${pattern}");\n`; }; - javascriptGenerator.forBlock["controller_rumble"] = function (block) { - const motor = block.getFieldValue("MOTOR"); + javascriptGenerator.forBlock['controller_rumble'] = function (block) { + const motor = block.getFieldValue('MOTOR'); const strength = - javascriptGenerator.valueToCode( - block, - "STRENGTH", - javascriptGenerator.ORDER_NONE, - ) || "1"; + javascriptGenerator.valueToCode(block, 'STRENGTH', javascriptGenerator.ORDER_NONE) || '1'; const duration = - javascriptGenerator.valueToCode( - block, - "DURATION", - javascriptGenerator.ORDER_NONE, - ) || "500"; + javascriptGenerator.valueToCode(block, 'DURATION', javascriptGenerator.ORDER_NONE) || '500'; return `controllerRumble("${motor}", ${strength}, ${duration});\n`; }; - javascriptGenerator.forBlock["controller_rumble_pattern"] = function (block) { - const motor = block.getFieldValue("MOTOR"); + javascriptGenerator.forBlock['controller_rumble_pattern'] = function (block) { + const motor = block.getFieldValue('MOTOR'); const strength = - javascriptGenerator.valueToCode( - block, - "STRENGTH", - javascriptGenerator.ORDER_NONE, - ) || "1"; + javascriptGenerator.valueToCode(block, 'STRENGTH', javascriptGenerator.ORDER_NONE) || '1'; const onDuration = - javascriptGenerator.valueToCode( - block, - "ON_DURATION", - javascriptGenerator.ORDER_NONE, - ) || "200"; + javascriptGenerator.valueToCode(block, 'ON_DURATION', javascriptGenerator.ORDER_NONE) || + '200'; const offDuration = - javascriptGenerator.valueToCode( - block, - "OFF_DURATION", - javascriptGenerator.ORDER_NONE, - ) || "100"; + javascriptGenerator.valueToCode(block, 'OFF_DURATION', javascriptGenerator.ORDER_NONE) || + '100'; const repeats = - javascriptGenerator.valueToCode( - block, - "REPEATS", - javascriptGenerator.ORDER_NONE, - ) || "3"; + javascriptGenerator.valueToCode(block, 'REPEATS', javascriptGenerator.ORDER_NONE) || '3'; return `controllerRumblePattern("${motor}", ${strength}, ${onDuration}, ${offDuration}, ${repeats});\n`; }; - javascriptGenerator.forBlock["create_custom_map"] = function (block) { + javascriptGenerator.forBlock['create_custom_map'] = function (block) { const colors = []; for (let i = 1; i <= 25; i++) { const color = - javascriptGenerator.valueToCode( - block, - `COLOR_${i}`, - javascriptGenerator.ORDER_ATOMIC, - ) || "#808080"; + javascriptGenerator.valueToCode(block, `COLOR_${i}`, javascriptGenerator.ORDER_ATOMIC) || + '#808080'; colors.push(color); } - return `await createCustomMap([${colors.join(", ")}]);\n`; + return `await createCustomMap([${colors.join(', ')}]);\n`; }; - - javascriptGenerator.forBlock["when_touches"] = function (block) { + javascriptGenerator.forBlock['when_touches'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), + block.getFieldValue('MODEL_VAR'), Blockly.Names.NameType.VARIABLE, - true, + true ); const otherModelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("OTHER_MODEL_VAR"), + block.getFieldValue('OTHER_MODEL_VAR'), Blockly.Names.NameType.VARIABLE, - true, + true ); const callbackVar2Name = block.callbackVar2Id ? javascriptGenerator.nameDB_.getName( block.callbackVar2Id, Blockly.Names.NameType.VARIABLE, - true, + true ) : null; const param2 = callbackVar2Name ?? otherModelName; - const trigger = block.getFieldValue("TRIGGER"); - const doCode = javascriptGenerator.statementToCode(block, "DO"); + const trigger = block.getFieldValue('TRIGGER'); + const doCode = javascriptGenerator.statementToCode(block, 'DO'); - if ( - trigger === "OnIntersectionEnterTrigger" || - trigger === "OnIntersectionExitTrigger" - ) { - const groupLine = block.callbackVar2Id ? ",\n applyToGroupSelf: true" : ""; + if (trigger === 'OnIntersectionEnterTrigger' || trigger === 'OnIntersectionExitTrigger') { + const groupLine = block.callbackVar2Id ? ',\n applyToGroupSelf: true' : ''; return `onIntersect(${modelName}, ${otherModelName}, { trigger: "${trigger}", callback: async function(${modelName}, ${param2}) { @@ -124,76 +91,54 @@ export function registerDeprecatedGenerators(javascriptGenerator) { });\n`; } else { console.error("Invalid trigger type for 'when_touches' block:", trigger); - return ""; + return ''; } }; - javascriptGenerator.forBlock["glide_to"] = function (block) { + javascriptGenerator.forBlock['glide_to'] = function (block) { const meshVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const x = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const y = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const z = - javascriptGenerator.valueToCode( - block, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + const x = javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC) || '0'; + const y = javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC) || '0'; + const z = javascriptGenerator.valueToCode(block, 'Z', javascriptGenerator.ORDER_ATOMIC) || '0'; const duration = - javascriptGenerator.valueToCode( - block, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, - ) || "1000"; - const mode = block.getFieldValue("MODE"); - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const easing = block.getFieldValue("EASING"); - - const code = `${mode === "AWAIT" ? "await " : ""}glideTo(${meshVar}, { x: ${x}, y: ${y}, z: ${z}, duration: ${duration} / 1000, reverse: ${reverse}, loop: ${loop}, easing: "${easing}" });\n`; + javascriptGenerator.valueToCode(block, 'DURATION', javascriptGenerator.ORDER_ATOMIC) || + '1000'; + const mode = block.getFieldValue('MODE'); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const easing = block.getFieldValue('EASING'); + + const code = `${mode === 'AWAIT' ? 'await ' : ''}glideTo(${meshVar}, { x: ${x}, y: ${y}, z: ${z}, duration: ${duration} / 1000, reverse: ${reverse}, loop: ${loop}, easing: "${easing}" });\n`; return code; }; - javascriptGenerator.forBlock["rotate_anim"] = function (block) { + javascriptGenerator.forBlock['rotate_anim'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const rotX = getFieldValue(block, "ROT_X", "0"); - const rotY = getFieldValue(block, "ROT_Y", "0"); - const rotZ = getFieldValue(block, "ROT_Z", "0"); - const duration = getFieldValue(block, "DURATION", "0"); - const mode = block.getFieldValue("MODE"); - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - const easing = block.getFieldValue("EASING"); + const rotX = getFieldValue(block, 'ROT_X', '0'); + const rotY = getFieldValue(block, 'ROT_Y', '0'); + const rotZ = getFieldValue(block, 'ROT_Z', '0'); + const duration = getFieldValue(block, 'DURATION', '0'); + const mode = block.getFieldValue('MODE'); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + const easing = block.getFieldValue('EASING'); - const asyncWrapper = mode === "AWAIT" ? "await " : ""; + const asyncWrapper = mode === 'AWAIT' ? 'await ' : ''; return `${asyncWrapper}rotateAnim(${meshName}, { x: ${rotX}, y: ${rotY}, z: ${rotZ}, duration: ${duration}, reverse: ${reverse}, loop: ${loop}, easing: "${easing}" });\n`; }; - javascriptGenerator.forBlock["controls_doWhile"] = function (block) { + javascriptGenerator.forBlock['controls_doWhile'] = function (block) { const condition = - javascriptGenerator.valueToCode( - block, - "BOOL", - javascriptGenerator.ORDER_NONE, - ) || "false"; - const branch = javascriptGenerator.statementToCode(block, "DO"); + javascriptGenerator.valueToCode(block, 'BOOL', javascriptGenerator.ORDER_NONE) || 'false'; + const branch = javascriptGenerator.statementToCode(block, 'DO'); return ` do { @@ -203,27 +148,24 @@ export function registerDeprecatedGenerators(javascriptGenerator) { } while (${condition});\n`; }; - javascriptGenerator.forBlock["for_loop"] = function (block, generator) { - const variable0 = generator.getVariableName(block.getFieldValue("VAR")); + javascriptGenerator.forBlock['for_loop'] = function (block, generator) { + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); - const argument0 = - generator.valueToCode(block, "FROM", generator.ORDER_ASSIGNMENT) || "0"; - const argument1 = - generator.valueToCode(block, "TO", generator.ORDER_ASSIGNMENT) || "0"; - const increment = - generator.valueToCode(block, "BY", generator.ORDER_ASSIGNMENT) || "1"; + const argument0 = generator.valueToCode(block, 'FROM', generator.ORDER_ASSIGNMENT) || '0'; + const argument1 = generator.valueToCode(block, 'TO', generator.ORDER_ASSIGNMENT) || '0'; + const increment = generator.valueToCode(block, 'BY', generator.ORDER_ASSIGNMENT) || '1'; - const branch = generator.statementToCode(block, "DO"); + const branch = generator.statementToCode(block, 'DO'); // Timing and iteration counter variables const timingVar = generator.nameDB_.getDistinctName( `${variable0}_timing`, - Blockly.Names.DEVELOPER_VARIABLE_TYPE, + Blockly.Names.DEVELOPER_VARIABLE_TYPE ); const counterVar = generator.nameDB_.getDistinctName( `${variable0}_counter`, - Blockly.Names.DEVELOPER_VARIABLE_TYPE, + Blockly.Names.DEVELOPER_VARIABLE_TYPE ); return ` @@ -240,141 +182,106 @@ export function registerDeprecatedGenerators(javascriptGenerator) { `; }; - javascriptGenerator.forBlock["when_key_event"] = function (block) { - const key = block.getFieldValue("KEY"); - const event = block.getFieldValue("EVENT"); // "pressed" or "released" - const statements_do = javascriptGenerator.statementToCode(block, "DO"); + javascriptGenerator.forBlock['when_key_event'] = function (block) { + const key = block.getFieldValue('KEY'); + const event = block.getFieldValue('EVENT'); // "pressed" or "released" + const statements_do = javascriptGenerator.statementToCode(block, 'DO'); // Pass "true" if event is "released" for the whenKeyEvent helper function - return `whenKeyEvent("${key}", async () => {${statements_do}}, ${event === "released"});\n`; + return `whenKeyEvent("${key}", async () => {${statements_do}}, ${event === 'released'});\n`; }; - javascriptGenerator.forBlock["change_material"] = function (block) { + javascriptGenerator.forBlock['change_material'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("ID_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('ID_VAR'), + Blockly.Names.NameType.VARIABLE ); - const material = block.getFieldValue("MATERIALS"); - const color = getFieldValue(block, "COLOR", '"#ffffff"'); + const material = block.getFieldValue('MATERIALS'); + const color = getFieldValue(block, 'COLOR', '"#ffffff"'); return `await changeMaterial(${modelName}, "${material}", ${color});\n`; }; - javascriptGenerator.forBlock["greyscale_colour"] = function (block) { - const colour = block.getFieldValue("COLOR"); + javascriptGenerator.forBlock['greyscale_colour'] = function (block) { + const colour = block.getFieldValue('COLOR'); const code = `"${colour}"`; return [code, javascriptGenerator.ORDER_ATOMIC]; }; - javascriptGenerator.forBlock["set_scene_bpm"] = function (block) { - const bpm = javascriptGenerator.valueToCode( - block, - "BPM", - javascriptGenerator.ORDER_ATOMIC, - ); + javascriptGenerator.forBlock['set_scene_bpm'] = function (block) { + const bpm = javascriptGenerator.valueToCode(block, 'BPM', javascriptGenerator.ORDER_ATOMIC); return `setBPM("__everywhere__", ${bpm});\n`; }; - javascriptGenerator.forBlock["set_mesh_bpm"] = function (block) { - const meshNameField = block.getFieldValue("MESH") || "__everywhere__"; + javascriptGenerator.forBlock['set_mesh_bpm'] = function (block) { + const meshNameField = block.getFieldValue('MESH') || '__everywhere__'; const meshName = `"${meshNameField}"`; // Always quoted const bpm = - javascriptGenerator.valueToCode( - block, - "BPM", - javascriptGenerator.ORDER_ATOMIC, - ) || "120"; // Default BPM if not connected + javascriptGenerator.valueToCode(block, 'BPM', javascriptGenerator.ORDER_ATOMIC) || '120'; // Default BPM if not connected return `await setBPM(${meshName}, ${bpm});\n`; }; - javascriptGenerator.forBlock["up"] = function (block) { + javascriptGenerator.forBlock['up'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); - const upForce = getFieldValue(block, "UP_FORCE", "1"); // Default up force + const upForce = getFieldValue(block, 'UP_FORCE', '1'); // Default up force return `up(${modelName}, ${upForce});\n`; }; - javascriptGenerator.forBlock["hold"] = function (block) { + javascriptGenerator.forBlock['hold'] = function (block) { const meshToAttach = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_TO_ATTACH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_TO_ATTACH'), + Blockly.Names.NameType.VARIABLE ); const targetMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("TARGET_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('TARGET_MESH'), + Blockly.Names.NameType.VARIABLE ); const xOffset = - javascriptGenerator.valueToCode( - block, - "X_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'X_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const yOffset = - javascriptGenerator.valueToCode( - block, - "Y_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Y_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const zOffset = - javascriptGenerator.valueToCode( - block, - "Z_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Z_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; // Establish the hold action with offset return `await hold(${meshToAttach}, ${targetMesh}, ${xOffset}, ${yOffset}, ${zOffset}); `; }; - javascriptGenerator.forBlock["key_pressed"] = function (block) { - const key = block.getFieldValue("KEY"); + javascriptGenerator.forBlock['key_pressed'] = function (block) { + const key = block.getFieldValue('KEY'); return [`keyPressed("${key}")`, javascriptGenerator.ORDER_NONE]; }; - javascriptGenerator.forBlock["xyz"] = function (block) { - const x = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const y = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const z = - javascriptGenerator.valueToCode( - block, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.forBlock['xyz'] = function (block) { + const x = javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC) || '0'; + const y = javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC) || '0'; + const z = javascriptGenerator.valueToCode(block, 'Z', javascriptGenerator.ORDER_ATOMIC) || '0'; // Generate a tuple representing the vector const code = `[${x}, ${y}, ${z}]`; return [code, javascriptGenerator.ORDER_ATOMIC]; }; - javascriptGenerator.forBlock["animate_property"] = function (block) { + javascriptGenerator.forBlock['animate_property'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const property = block.getFieldValue("PROPERTY"); - const targetValue = getFieldValue(block, "TARGET_VALUE", "0.5"); - const duration = getFieldValue(block, "DURATION", "1"); - const mode = block.getFieldValue("MODE"); - const reverse = block.getFieldValue("REVERSE") === "TRUE"; - const loop = block.getFieldValue("LOOP") === "TRUE"; - - const asyncWrapper = mode === "AWAIT" ? "await " : ""; + const property = block.getFieldValue('PROPERTY'); + const targetValue = getFieldValue(block, 'TARGET_VALUE', '0.5'); + const duration = getFieldValue(block, 'DURATION', '1'); + const mode = block.getFieldValue('MODE'); + const reverse = block.getFieldValue('REVERSE') === 'TRUE'; + const loop = block.getFieldValue('LOOP') === 'TRUE'; + + const asyncWrapper = mode === 'AWAIT' ? 'await ' : ''; return `${asyncWrapper}animateProperty(${meshName}, { property: "${property}", targetValue: ${targetValue}, duration: ${duration}, reverse: ${reverse}, loop: ${loop}, mode: "${mode}" });\n`; }; diff --git a/generators/generators-events.js b/generators/generators-events.js index 23b0a6ca..dcde8eb4 100644 --- a/generators/generators-events.js +++ b/generators/generators-events.js @@ -1,49 +1,45 @@ -import * as Blockly from "blockly"; -import { emitSafeIdentifierLiteral } from "./generators-utilities.js"; +import * as Blockly from 'blockly'; +import { emitSafeIdentifierLiteral } from './generators-utilities.js'; export function registerEventsGenerators(javascriptGenerator) { // ------------------------------- // EVENTS // ------------------------------- // Start ----------------------------------------------------- - javascriptGenerator.forBlock["start"] = function (block) { - const branch = javascriptGenerator.statementToCode(block, "DO"); + javascriptGenerator.forBlock['start'] = function (block) { + const branch = javascriptGenerator.statementToCode(block, 'DO'); return `(async () => {\n${branch}})();\n`; }; // Forever --------------------------------------------------- - javascriptGenerator.forBlock["forever"] = function (block) { - const branch = javascriptGenerator.statementToCode(block, "DO"); + javascriptGenerator.forBlock['forever'] = function (block) { + const branch = javascriptGenerator.statementToCode(block, 'DO'); const code = `forever(async function(){\n${branch}});\n`; return code; }; // when % clicked -------------------------------------------- - javascriptGenerator.forBlock["when_clicked"] = function (block) { + javascriptGenerator.forBlock['when_clicked'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); - const trigger = block.getFieldValue("TRIGGER"); - const mode = block.getFieldValue("MODE") || "wait"; + const trigger = block.getFieldValue('TRIGGER'); + const mode = block.getFieldValue('MODE') || 'wait'; - const doCode = javascriptGenerator.statementToCode(block, "DO").trim(); + const doCode = javascriptGenerator.statementToCode(block, 'DO').trim(); const thenCodes = []; for (let i = 0; i < block.thenCount_; i++) { - const thenCode = javascriptGenerator - .statementToCode(block, "THEN" + i) - .trim(); + const thenCode = javascriptGenerator.statementToCode(block, 'THEN' + i).trim(); if (thenCode) { thenCodes.push(thenCode); } } const allActions = [doCode, ...thenCodes].filter((code) => code); - const actionFunctions = allActions.map( - (code) => `async function(${modelName}) {\n${code}\n}`, - ); + const actionFunctions = allActions.map((code) => `async function(${modelName}) {\n${code}\n}`); // Determine if this is a top-level block (not nested) const isTopLevel = !block.getSurroundParent(); @@ -51,50 +47,47 @@ export function registerEventsGenerators(javascriptGenerator) { const code = `onTrigger(${modelName}, {\n` + ` trigger: "${trigger}",\n` + - ` callback: [\n${actionFunctions.join(",\n")}\n],\n` + + ` callback: [\n${actionFunctions.join(',\n')}\n],\n` + ` mode: "${mode}"` + - (isTopLevel ? `,\n applyToGroup: true` : "") + + (isTopLevel ? `,\n applyToGroup: true` : '') + `\n});\n`; return code; }; // on % collision with % ------------------------------------- - javascriptGenerator.forBlock["on_collision"] = function (block) { + javascriptGenerator.forBlock['on_collision'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), + block.getFieldValue('MODEL_VAR'), Blockly.Names.NameType.VARIABLE, - true, + true ); const otherModelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("OTHER_MODEL_VAR"), + block.getFieldValue('OTHER_MODEL_VAR'), Blockly.Names.NameType.VARIABLE, - true, + true ); const callbackVar2Name = block.callbackVar2Id ? javascriptGenerator.nameDB_.getName( block.callbackVar2Id, Blockly.Names.NameType.VARIABLE, - true, + true ) : null; const param2 = callbackVar2Name ?? otherModelName; - const trigger = block.getFieldValue("TRIGGER"); - const doCode = javascriptGenerator.statementToCode(block, "DO"); + const trigger = block.getFieldValue('TRIGGER'); + const doCode = javascriptGenerator.statementToCode(block, 'DO'); const isTopLevel = !block.getSurroundParent(); - if ( - trigger === "OnIntersectionEnterTrigger" || - trigger === "OnIntersectionExitTrigger" - ) { + if (trigger === 'OnIntersectionEnterTrigger' || trigger === 'OnIntersectionExitTrigger') { const groupLine = isTopLevel ? block.callbackVar2Id - ? ",\n applyToGroupSelf: true" - : ",\n applyToGroupOther: true" - : ""; + ? ',\n applyToGroupSelf: true' + : ',\n applyToGroupOther: true' + : ''; return `onIntersect(${modelName}, ${otherModelName}, { trigger: "${trigger}", callback: async function(${modelName}, ${param2}) { @@ -103,44 +96,37 @@ export function registerEventsGenerators(javascriptGenerator) { });\n`; } else { console.error("Invalid trigger type for 'on_collision' block:", trigger); - return ""; + return ''; } }; // When % action event ---------------------------------------- - javascriptGenerator.forBlock["when_action_event"] = function (block) { - const action = block.getFieldValue("ACTION"); - const event = block.getFieldValue("EVENT"); - const statements_do = javascriptGenerator.statementToCode(block, "DO"); + javascriptGenerator.forBlock['when_action_event'] = function (block) { + const action = block.getFieldValue('ACTION'); + const event = block.getFieldValue('EVENT'); + const statements_do = javascriptGenerator.statementToCode(block, 'DO'); - return `whenActionEvent("${action}", async () => {${statements_do}}, ${event === "released"});\n`; + return `whenActionEvent("${action}", async () => {${statements_do}}, ${event === 'released'});\n`; }; // Broadcast event ------------------------------------------- - javascriptGenerator.forBlock["broadcast_event"] = function (block) { + javascriptGenerator.forBlock['broadcast_event'] = function (block) { const raw = - javascriptGenerator.valueToCode( - block, - "EVENT_NAME", - javascriptGenerator.ORDER_ATOMIC, - ) || "undefined"; + javascriptGenerator.valueToCode(block, 'EVENT_NAME', javascriptGenerator.ORDER_ATOMIC) || + 'undefined'; const safe = emitSafeIdentifierLiteral(raw, undefined); return `broadcastEvent(${safe});\n`; }; // On event -------------------------------------------------- - javascriptGenerator.forBlock["on_event"] = function (block) { + javascriptGenerator.forBlock['on_event'] = function (block) { // Don't force a default; let invalid/empty resolve to undefined const raw = - javascriptGenerator.valueToCode( - block, - "EVENT_NAME", - javascriptGenerator.ORDER_ATOMIC, - ) || ""; + javascriptGenerator.valueToCode(block, 'EVENT_NAME', javascriptGenerator.ORDER_ATOMIC) || ''; const safe = emitSafeIdentifierLiteral(raw); - const statements_do = javascriptGenerator.statementToCode(block, "DO"); + const statements_do = javascriptGenerator.statementToCode(block, 'DO'); return `onEvent(${safe}, async function() {\n${statements_do}});\n`; }; } diff --git a/generators/generators-functions.js b/generators/generators-functions.js index 70606547..ebdfb7c1 100644 --- a/generators/generators-functions.js +++ b/generators/generators-functions.js @@ -1,4 +1,4 @@ -import * as Blockly from "blockly"; +import * as Blockly from 'blockly'; export function registerFunctionsGenerators(javascriptGenerator) { // ------------------------------- @@ -6,98 +6,72 @@ export function registerFunctionsGenerators(javascriptGenerator) { // ------------------------------- // Function definition, no return -------------------------------- - javascriptGenerator.forBlock["procedures_defnoreturn"] = function (block) { + javascriptGenerator.forBlock['procedures_defnoreturn'] = function (block) { const functionName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("NAME"), - Blockly.PROCEDURE_CATEGORY_NAME, + block.getFieldValue('NAME'), + Blockly.PROCEDURE_CATEGORY_NAME ); const args = block.argData_.map((elem) => - javascriptGenerator.nameDB_.getName( - elem.model.name, - Blockly.Names.NameType.VARIABLE, - ), + javascriptGenerator.nameDB_.getName(elem.model.name, Blockly.Names.NameType.VARIABLE) ); - const params = args.join(", "); + const params = args.join(', '); const branch = - javascriptGenerator.statementToCode( - block, - "STACK", - javascriptGenerator.ORDER_NONE, - ) || ""; + javascriptGenerator.statementToCode(block, 'STACK', javascriptGenerator.ORDER_NONE) || ''; const code = `async function ${functionName}(${params}) {\n${branch}\n}`; return code; }; // Function definition with return ------------------------------- - javascriptGenerator.forBlock["procedures_defreturn"] = function (block) { + javascriptGenerator.forBlock['procedures_defreturn'] = function (block) { const functionName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("NAME"), - Blockly.PROCEDURE_CATEGORY_NAME, + block.getFieldValue('NAME'), + Blockly.PROCEDURE_CATEGORY_NAME ); const args = block.argData_.map((elem) => - javascriptGenerator.nameDB_.getName( - elem.model.name, - Blockly.Names.NameType.VARIABLE, - ), + javascriptGenerator.nameDB_.getName(elem.model.name, Blockly.Names.NameType.VARIABLE) ); - const params = args.join(", "); + const params = args.join(', '); const branch = - javascriptGenerator.statementToCode( - block, - "STACK", - javascriptGenerator.ORDER_NONE, - ) || ""; + javascriptGenerator.statementToCode(block, 'STACK', javascriptGenerator.ORDER_NONE) || ''; const returnValue = - javascriptGenerator.valueToCode( - block, - "RETURN", - javascriptGenerator.ORDER_NONE, - ) || ""; + javascriptGenerator.valueToCode(block, 'RETURN', javascriptGenerator.ORDER_NONE) || ''; const code = `async function ${functionName}(${params}) {\n${branch}return ${returnValue};\n}`; return code; }; // If condition, return ------------------------------------------ - javascriptGenerator.forBlock["procedures_callreturn"] = function (block) { + javascriptGenerator.forBlock['procedures_callreturn'] = function (block) { const functionName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("NAME"), - Blockly.PROCEDURE_CATEGORY_NAME, + block.getFieldValue('NAME'), + Blockly.PROCEDURE_CATEGORY_NAME ); const args = []; const variables = block.arguments_ || []; for (let i = 0; i < variables.length; i++) { args[i] = - javascriptGenerator.valueToCode( - block, - "ARG" + i, - javascriptGenerator.ORDER_NONE, - ) || "null"; + javascriptGenerator.valueToCode(block, 'ARG' + i, javascriptGenerator.ORDER_NONE) || 'null'; } - const code = `await ${functionName}(${args.join(", ")})`; + const code = `await ${functionName}(${args.join(', ')})`; return [code, javascriptGenerator.ORDER_ATOMIC]; }; // Call function ------------------------------------------------- - javascriptGenerator.forBlock["procedures_callnoreturn"] = function (block) { + javascriptGenerator.forBlock['procedures_callnoreturn'] = function (block) { const functionName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("NAME"), - Blockly.PROCEDURE_CATEGORY_NAME, + block.getFieldValue('NAME'), + Blockly.PROCEDURE_CATEGORY_NAME ); const args = []; const variables = block.arguments_; for (let i = 0; i < variables.length; i++) { args[i] = - javascriptGenerator.valueToCode( - block, - "ARG" + i, - javascriptGenerator.ORDER_NONE, - ) || "null"; + javascriptGenerator.valueToCode(block, 'ARG' + i, javascriptGenerator.ORDER_NONE) || 'null'; } - const code = `await ${functionName}(${args.join(", ")});\n`; + const code = `await ${functionName}(${args.join(', ')});\n`; return code; }; } diff --git a/generators/generators-material.js b/generators/generators-material.js index 68e7adbc..dd1359d9 100644 --- a/generators/generators-material.js +++ b/generators/generators-material.js @@ -1,80 +1,80 @@ -import * as Blockly from "blockly"; -import { getFieldValue } from "./generators-utilities.js"; +import * as Blockly from 'blockly'; +import { getFieldValue } from './generators-utilities.js'; export function registerMaterialGenerators(javascriptGenerator) { // ------------------------------- // MATERIAL // ------------------------------- // Color object to color list --------------------------------- - javascriptGenerator.forBlock["change_color"] = function (block) { + javascriptGenerator.forBlock['change_color'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); - const color = getFieldValue(block, "COLOR", '"#ffffff"'); + const color = getFieldValue(block, 'COLOR', '"#ffffff"'); return `await changeColor(${modelName}, { color: ${color} });\n`; }; // Set alpha of object ---------------------------------------- - javascriptGenerator.forBlock["set_alpha"] = function (block) { + javascriptGenerator.forBlock['set_alpha'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH'), + Blockly.Names.NameType.VARIABLE ); const alphaValue = javascriptGenerator.valueToCode( block, - "ALPHA", - javascriptGenerator.ORDER_ATOMIC, + 'ALPHA', + javascriptGenerator.ORDER_ATOMIC ); return `await setAlpha(${modelName}, { value: ${alphaValue} });\n`; }; // Tint object ------------------------------------------------ - javascriptGenerator.forBlock["tint"] = function (block) { + javascriptGenerator.forBlock['tint'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); - const color = getFieldValue(block, "COLOR", '"#AA336A"'); + const color = getFieldValue(block, 'COLOR', '"#AA336A"'); return `await tint(${modelName}, { color: ${color} });\n`; }; // Highlight object ------------------------------------------- - javascriptGenerator.forBlock["highlight"] = function (block) { + javascriptGenerator.forBlock['highlight'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); - const color = getFieldValue(block, "COLOR", '"#FFD700"'); + const color = getFieldValue(block, 'COLOR', '"#FFD700"'); return `await highlight(${modelName}, { color: ${color} });\n`; }; // Glow object ------------------------------------------------ - javascriptGenerator.forBlock["glow"] = function (block) { + javascriptGenerator.forBlock['glow'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); return `await glow(${modelName});\n`; }; // Clear effects on object ------------------------------------ - javascriptGenerator.forBlock["clear_effects"] = function (block) { + javascriptGenerator.forBlock['clear_effects'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); return `await clearEffects(${modelName});\n`; }; // Colour ----------------------------------------------------- - javascriptGenerator.forBlock["colour"] = function (block) { - const colour = block.getFieldValue("COLOR"); + javascriptGenerator.forBlock['colour'] = function (block) { + const colour = block.getFieldValue('COLOR'); const code = `"${colour}"`; return [code, javascriptGenerator.ORDER_ATOMIC]; }; @@ -84,54 +84,41 @@ export function registerMaterialGenerators(javascriptGenerator) { // Uses Blockly built in // Random colour ---------------------------------------------- - javascriptGenerator.forBlock["random_colour"] = function (_block) { + javascriptGenerator.forBlock['random_colour'] = function (_block) { const code = `randomColour()`; return [code, javascriptGenerator.ORDER_ATOMIC]; }; // Hex colour ------------------------------------------------- - javascriptGenerator.forBlock["colour_from_string"] = function (block) { - const rawColourValue = (block.getFieldValue("COLOR") || "").trim(); + javascriptGenerator.forBlock['colour_from_string'] = function (block) { + const rawColourValue = (block.getFieldValue('COLOR') || '').trim(); const isBareHex = /^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(rawColourValue); - const colourValue = isBareHex - ? `#${rawColourValue}` - : rawColourValue || "#000000"; + const colourValue = isBareHex ? `#${rawColourValue}` : rawColourValue || '#000000'; return [`"${colourValue}"`, javascriptGenerator.ORDER_ATOMIC]; }; // Set material of object ------------------------------------- - javascriptGenerator.forBlock["set_material"] = function (block) { + javascriptGenerator.forBlock['set_material'] = function (block) { const meshVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH'), + Blockly.Names.NameType.VARIABLE ); const material = - javascriptGenerator.valueToCode( - block, - "MATERIAL", - javascriptGenerator.ORDER_ATOMIC, - ) || "{}"; + javascriptGenerator.valueToCode(block, 'MATERIAL', javascriptGenerator.ORDER_ATOMIC) || '{}'; const code = `setMaterial(${meshVar}, ${material});\n`; return code; }; // Material ---------------------------------------------------- - javascriptGenerator.forBlock["material"] = function (block) { + javascriptGenerator.forBlock['material'] = function (block) { const baseColor = - javascriptGenerator.valueToCode( - block, - "BASE_COLOR", - javascriptGenerator.ORDER_ATOMIC, - ) || '"#ffffff"'; + javascriptGenerator.valueToCode(block, 'BASE_COLOR', javascriptGenerator.ORDER_ATOMIC) || + '"#ffffff"'; - const textureSet = block.getFieldValue("TEXTURE_SET"); + const textureSet = block.getFieldValue('TEXTURE_SET'); const alpha = - javascriptGenerator.valueToCode( - block, - "ALPHA", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'ALPHA', javascriptGenerator.ORDER_ATOMIC) || '1'; // Always return a standard data object. // Logic that uses this block (like set_material) will handle the application. @@ -146,27 +133,20 @@ export function registerMaterialGenerators(javascriptGenerator) { // Skin colour -------------------------------------------------- // used within character definition but not as a standalone block - javascriptGenerator.forBlock["skin_colour"] = function (block) { - const colour = block.getFieldValue("COLOR"); + javascriptGenerator.forBlock['skin_colour'] = function (block) { + const colour = block.getFieldValue('COLOR'); const code = `"${colour}"`; return [code, javascriptGenerator.ORDER_ATOMIC]; }; // Gradient material -------------------------------------------- - javascriptGenerator.forBlock["gradient_material"] = function (block) { + javascriptGenerator.forBlock['gradient_material'] = function (block) { const color = - javascriptGenerator.valueToCode( - block, - "COLOR", - javascriptGenerator.ORDER_ATOMIC, - ) || '"#ffffff"'; + javascriptGenerator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC) || + '"#ffffff"'; const alpha = - javascriptGenerator.valueToCode( - block, - "ALPHA", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'ALPHA', javascriptGenerator.ORDER_ATOMIC) || '1'; const code = `{ color: ${color}, materialName: "none.png", alpha: ${alpha} }`; return [code, javascriptGenerator.ORDER_ATOMIC]; diff --git a/generators/generators-math.js b/generators/generators-math.js index 5a7ac13e..fc44dc81 100644 --- a/generators/generators-math.js +++ b/generators/generators-math.js @@ -1,4 +1,4 @@ -import { getFieldValue } from "./generators-utilities.js"; +import { getFieldValue } from './generators-utilities.js'; export function registerMathGenerators(javascriptGenerator) { // ------------------------------- @@ -8,28 +8,19 @@ export function registerMathGenerators(javascriptGenerator) { // Arithmetic operator - uses Blockly default // Random integer ---------------------------------------------------- - javascriptGenerator.forBlock["math_random_int"] = function (block) { + javascriptGenerator.forBlock['math_random_int'] = function (block) { const from = - javascriptGenerator.valueToCode( - block, - "FROM", - javascriptGenerator.ORDER_NONE, - ) || "0"; - const to = - javascriptGenerator.valueToCode( - block, - "TO", - javascriptGenerator.ORDER_NONE, - ) || "0"; - const code = "randomInteger(" + from + ", " + to + ")"; + javascriptGenerator.valueToCode(block, 'FROM', javascriptGenerator.ORDER_NONE) || '0'; + const to = javascriptGenerator.valueToCode(block, 'TO', javascriptGenerator.ORDER_NONE) || '0'; + const code = 'randomInteger(' + from + ', ' + to + ')'; return [code, javascriptGenerator.ORDER_FUNCTION_CALL]; }; // Random integer with seed ----------------------------------------- - javascriptGenerator.forBlock["random_seeded_int"] = function (block) { - const value_from = getFieldValue(block, "FROM", 0); - const value_to = getFieldValue(block, "TO", 10); - const value_seed = getFieldValue(block, "SEED", 123456); + javascriptGenerator.forBlock['random_seeded_int'] = function (block) { + const value_from = getFieldValue(block, 'FROM', 0); + const value_to = getFieldValue(block, 'TO', 10); + const value_seed = getFieldValue(block, 'SEED', 123456); const code = `seededRandom(${value_from}, ${value_to}, ${value_seed})`; @@ -39,16 +30,16 @@ export function registerMathGenerators(javascriptGenerator) { // Integer - uses Blockly default // Convert to integer ----------------------------------------------- - javascriptGenerator.forBlock["to_number"] = function (block) { + javascriptGenerator.forBlock['to_number'] = function (block) { const string = javascriptGenerator.valueToCode( block, - "STRING", - javascriptGenerator.ORDER_ATOMIC, + 'STRING', + javascriptGenerator.ORDER_ATOMIC ); - const conversionType = block.getFieldValue("TYPE"); + const conversionType = block.getFieldValue('TYPE'); let code; - if (conversionType === "INT") { + if (conversionType === 'INT') { code = `parseInt(${string})`; } else { code = `parseFloat(${string})`; diff --git a/generators/generators-scene.js b/generators/generators-scene.js index d075d0ae..017a8160 100644 --- a/generators/generators-scene.js +++ b/generators/generators-scene.js @@ -1,11 +1,11 @@ -import * as Blockly from "blockly"; -import { meshMap, meshBlockIdMap, generateUniqueId } from "./mesh-state.js"; +import * as Blockly from 'blockly'; +import { meshMap, meshBlockIdMap, generateUniqueId } from './mesh-state.js'; import { getFieldValue, getVariableInfo, getPositionTuple, createMesh, -} from "./generators-utilities.js"; +} from './generators-utilities.js'; export function registerSceneGenerators(javascriptGenerator) { // ------------------------------- @@ -13,50 +13,43 @@ export function registerSceneGenerators(javascriptGenerator) { // ------------------------------- // Sky ------------------------------------------------------------- - javascriptGenerator.forBlock["set_sky_color"] = function (block) { - const meshId = "sky"; + javascriptGenerator.forBlock['set_sky_color'] = function (block) { + const meshId = 'sky'; meshMap[meshId] = block; meshBlockIdMap[meshId] = block.id; let color = - javascriptGenerator.valueToCode( - block, - "COLOR", - javascriptGenerator.ORDER_NONE, - ) || '"#6495ED"'; + javascriptGenerator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_NONE) || + '"#6495ED"'; return `setSky(${color});\n`; }; // Map with material ------------------------------------------------ - javascriptGenerator.forBlock["create_map"] = function (block) { - const mapName = block.getFieldValue("MAP_NAME"); + javascriptGenerator.forBlock['create_map'] = function (block) { + const mapName = block.getFieldValue('MAP_NAME'); const material = - javascriptGenerator.valueToCode( - block, - "MATERIAL", - javascriptGenerator.ORDER_NONE, - ) || "null"; - const meshId = "ground"; + javascriptGenerator.valueToCode(block, 'MATERIAL', javascriptGenerator.ORDER_NONE) || 'null'; + const meshId = 'ground'; meshMap[meshId] = block; meshBlockIdMap[meshId] = block.id; return `createMap("${mapName}", ${material});\n`; }; // Background ------------------------------------------------------- - javascriptGenerator.forBlock["set_background_color"] = function (block) { + javascriptGenerator.forBlock['set_background_color'] = function (block) { // Defaults to a quoted hex string (e.g., "#6495ED") - let color = getFieldValue(block, "COLOR", '"#6495ED"'); + let color = getFieldValue(block, 'COLOR', '"#6495ED"'); - const colorInput = block.getInput("COLOR"); + const colorInput = block.getInput('COLOR'); const colorBlock = colorInput?.connection?.targetBlock(); - if (colorBlock && colorBlock.type === "material") { + if (colorBlock && colorBlock.type === 'material') { color = `(function(m){ const c = (m && (m.color || m.diffuseColor || m.albedoColor)); return (c && c.toHexString) ? c.toHexString() : "#6495ED"; })(${color})`; } - const meshId = "sky"; + const meshId = 'sky'; meshMap[meshId] = block; meshBlockIdMap[meshId] = block.id; @@ -65,10 +58,10 @@ export function registerSceneGenerators(javascriptGenerator) { }; // Show object ------------------------------------------------------ - javascriptGenerator.forBlock["show"] = function (block) { + javascriptGenerator.forBlock['show'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); return `await show(${modelName});\n`; @@ -76,21 +69,21 @@ export function registerSceneGenerators(javascriptGenerator) { // Hide object ------------------------------------------------------ - javascriptGenerator.forBlock["hide"] = function (block) { + javascriptGenerator.forBlock['hide'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); return `await hide(${modelName});\n`; }; // Dispose object --------------------------------------------------- - javascriptGenerator.forBlock["dispose"] = function (block) { + javascriptGenerator.forBlock['dispose'] = function (block) { // Get the selected variable name const meshVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); // Generate code to call the dispose helper for the selected mesh @@ -103,64 +96,58 @@ export function registerSceneGenerators(javascriptGenerator) { // ------------------------------- // Add model -------------------------------------------------------- - javascriptGenerator.forBlock["load_model"] = function (block) { - const modelName = block.getFieldValue("MODELS"); - const scale = getFieldValue(block, "SCALE", "1"); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); - const { generatedName: variableName, userVariableName } = getVariableInfo( - block, - "ID_VAR", - ); + javascriptGenerator.forBlock['load_model'] = function (block) { + const modelName = block.getFieldValue('MODELS'); + const scale = getFieldValue(block, 'SCALE', '1'); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); + const { generatedName: variableName, userVariableName } = getVariableInfo(block, 'ID_VAR'); const meshId = `${userVariableName}__${block.id}`; meshMap[block.id] = block; meshBlockIdMap[block.id] = block.id; - let doCode = ""; - if (block.getInput("DO")) { - doCode = javascriptGenerator.statementToCode(block, "DO") || ""; + let doCode = ''; + if (block.getInput('DO')) { + doCode = javascriptGenerator.statementToCode(block, 'DO') || ''; } - doCode = doCode ? `async function() {\n${doCode}\n}` : ""; + doCode = doCode ? `async function() {\n${doCode}\n}` : ''; return `${variableName} = createModel({ modelName: '${modelName}', modelId: '${meshId}', scale: ${scale}, - position: { x: ${x}, y: ${y}, z: ${z} }${doCode ? `,\ncallback: ${doCode}` : ""} + position: { x: ${x}, y: ${y}, z: ${z} }${doCode ? `,\ncallback: ${doCode}` : ''} });\n`; }; // Add character ---------------------------------------------------- - javascriptGenerator.forBlock["load_character"] = function (block) { - const modelName = block.getFieldValue("MODELS"); - const scale = getFieldValue(block, "SCALE", "1"); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); - const hairColor = getFieldValue(block, "HAIR_COLOR", "#000000"); - const skinColor = getFieldValue(block, "SKIN_COLOR", "#FFE0BD"); - const eyesColor = getFieldValue(block, "EYES_COLOR", "#0000FF"); - const sleevesColor = getFieldValue(block, "SLEEVES_COLOR", "#FFFFFF"); - const shortsColor = getFieldValue(block, "SHORTS_COLOR", "#000000"); - const tshirtColor = getFieldValue(block, "TSHIRT_COLOR", "#FF0000"); - const { generatedName: variableName, userVariableName } = getVariableInfo( - block, - "ID_VAR", - ); + javascriptGenerator.forBlock['load_character'] = function (block) { + const modelName = block.getFieldValue('MODELS'); + const scale = getFieldValue(block, 'SCALE', '1'); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); + const hairColor = getFieldValue(block, 'HAIR_COLOR', '#000000'); + const skinColor = getFieldValue(block, 'SKIN_COLOR', '#FFE0BD'); + const eyesColor = getFieldValue(block, 'EYES_COLOR', '#0000FF'); + const sleevesColor = getFieldValue(block, 'SLEEVES_COLOR', '#FFFFFF'); + const shortsColor = getFieldValue(block, 'SHORTS_COLOR', '#000000'); + const tshirtColor = getFieldValue(block, 'TSHIRT_COLOR', '#FF0000'); + const { generatedName: variableName, userVariableName } = getVariableInfo(block, 'ID_VAR'); const meshId = `${userVariableName}__${block.id}`; meshMap[block.id] = block; meshBlockIdMap[block.id] = block.id; // Generate the code for the "do" part (if present) - let doCode = ""; + let doCode = ''; - if (block.getInput("DO")) { - doCode = javascriptGenerator.statementToCode(block, "DO") || ""; + if (block.getInput('DO')) { + doCode = javascriptGenerator.statementToCode(block, 'DO') || ''; } - doCode = doCode ? `async function() {\n${doCode}\n}` : ""; + doCode = doCode ? `async function() {\n${doCode}\n}` : ''; return `${variableName} = createCharacter({ modelName: '${modelName}', @@ -174,86 +161,80 @@ export function registerSceneGenerators(javascriptGenerator) { sleeves: ${sleevesColor}, shorts: ${shortsColor}, tshirt: ${tshirtColor} - }${doCode ? `, callback: ${doCode}` : ""} + }${doCode ? `, callback: ${doCode}` : ''} });\n`; }; // Add item --------------------------------------------------------- - javascriptGenerator.forBlock["load_object"] = function (block) { - const modelName = block.getFieldValue("MODELS"); - const scale = getFieldValue(block, "SCALE", "1"); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); - const color = getFieldValue(block, "COLOR", '"#000000"'); - - const { generatedName: variableName, userVariableName } = getVariableInfo( - block, - "ID_VAR", - ); + javascriptGenerator.forBlock['load_object'] = function (block) { + const modelName = block.getFieldValue('MODELS'); + const scale = getFieldValue(block, 'SCALE', '1'); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); + const color = getFieldValue(block, 'COLOR', '"#000000"'); + + const { generatedName: variableName, userVariableName } = getVariableInfo(block, 'ID_VAR'); const meshId = `${userVariableName}__${block.id}`; meshMap[block.id] = block; meshBlockIdMap[block.id] = block.id; // Generate the code for the "do" part (if present) - let doCode = ""; + let doCode = ''; - if (block.getInput("DO")) { - doCode = javascriptGenerator.statementToCode(block, "DO") || ""; + if (block.getInput('DO')) { + doCode = javascriptGenerator.statementToCode(block, 'DO') || ''; } - doCode = doCode ? `async function() {\n${doCode}\n}` : ""; + doCode = doCode ? `async function() {\n${doCode}\n}` : ''; return `${variableName} = createObject({ modelName: '${modelName}', modelId: '${meshId}', color: ${color}, scale: ${scale}, - position: { x: ${x}, y: ${y}, z: ${z} }${doCode ? `,\ncallback: ${doCode}` : ""} + position: { x: ${x}, y: ${y}, z: ${z} }${doCode ? `,\ncallback: ${doCode}` : ''} });\n`; }; // Add object ------------------------------------------------------- - javascriptGenerator.forBlock["load_multi_object"] = function (block) { - const modelName = block.getFieldValue("MODELS"); - const scale = getFieldValue(block, "SCALE", "1"); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); - const color = getFieldValue(block, "COLORS", "#000000"); - - const { generatedName: variableName, userVariableName } = getVariableInfo( - block, - "ID_VAR", - ); + javascriptGenerator.forBlock['load_multi_object'] = function (block) { + const modelName = block.getFieldValue('MODELS'); + const scale = getFieldValue(block, 'SCALE', '1'); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); + const color = getFieldValue(block, 'COLORS', '#000000'); + + const { generatedName: variableName, userVariableName } = getVariableInfo(block, 'ID_VAR'); const meshId = `${userVariableName}__${block.id}`; meshMap[block.id] = block; meshBlockIdMap[block.id] = block.id; // Generate the code for the "do" part (if present) - let doCode = ""; + let doCode = ''; - if (block.getInput("DO")) { - doCode = javascriptGenerator.statementToCode(block, "DO") || ""; + if (block.getInput('DO')) { + doCode = javascriptGenerator.statementToCode(block, 'DO') || ''; } - doCode = doCode ? `async function() {\n${doCode}\n}` : ""; + doCode = doCode ? `async function() {\n${doCode}\n}` : ''; return `${variableName} = createObject({ modelName: '${modelName}', modelId: '${meshId}', color: ${color}, scale: ${scale}, - position: { x: ${x}, y: ${y}, z: ${z} }${doCode ? `,\ncallback: ${doCode}` : ""} + position: { x: ${x}, y: ${y}, z: ${z} }${doCode ? `,\ncallback: ${doCode}` : ''} });\n`; }; // Add box --------------------------------------------------------- - javascriptGenerator.forBlock["create_box"] = function (block) { - const color = getFieldValue(block, "COLOR", '"#9932CC"'); - const width = getFieldValue(block, "WIDTH", "1"); - const height = getFieldValue(block, "HEIGHT", "1"); - const depth = getFieldValue(block, "DEPTH", "1"); + javascriptGenerator.forBlock['create_box'] = function (block) { + const color = getFieldValue(block, 'COLOR', '"#9932CC"'); + const width = getFieldValue(block, 'WIDTH', '1'); + const height = getFieldValue(block, 'HEIGHT', '1'); + const depth = getFieldValue(block, 'DEPTH', '1'); const positionSource = getPositionTuple(block); @@ -265,15 +246,15 @@ export function registerSceneGenerators(javascriptGenerator) { `position: ${positionSource}`, ]; - return createMesh(block, "Box", params, "box"); + return createMesh(block, 'Box', params, 'box'); }; // Add sphere --------------------------------------------------------- - javascriptGenerator.forBlock["create_sphere"] = function (block) { - const color = getFieldValue(block, "COLOR", '"#9932CC"'); - const diameterX = getFieldValue(block, "DIAMETER_X", "1"); - const diameterY = getFieldValue(block, "DIAMETER_Y", "1"); - const diameterZ = getFieldValue(block, "DIAMETER_Z", "1"); + javascriptGenerator.forBlock['create_sphere'] = function (block) { + const color = getFieldValue(block, 'COLOR', '"#9932CC"'); + const diameterX = getFieldValue(block, 'DIAMETER_X', '1'); + const diameterY = getFieldValue(block, 'DIAMETER_Y', '1'); + const diameterZ = getFieldValue(block, 'DIAMETER_Z', '1'); const positionSource = getPositionTuple(block); @@ -285,16 +266,16 @@ export function registerSceneGenerators(javascriptGenerator) { `position: ${positionSource}`, ]; - return createMesh(block, "Sphere", params, "sphere"); + return createMesh(block, 'Sphere', params, 'sphere'); }; // Add cylinder ------------------------------------------------------- - javascriptGenerator.forBlock["create_cylinder"] = function (block) { - const color = getFieldValue(block, "COLOR", '"#9932CC"'); - const height = getFieldValue(block, "HEIGHT", "2"); - const diameterTop = getFieldValue(block, "DIAMETER_TOP", "1"); - const diameterBottom = getFieldValue(block, "DIAMETER_BOTTOM", "1"); - const tessellations = getFieldValue(block, "TESSELLATIONS", "12"); + javascriptGenerator.forBlock['create_cylinder'] = function (block) { + const color = getFieldValue(block, 'COLOR', '"#9932CC"'); + const height = getFieldValue(block, 'HEIGHT', '2'); + const diameterTop = getFieldValue(block, 'DIAMETER_TOP', '1'); + const diameterBottom = getFieldValue(block, 'DIAMETER_BOTTOM', '1'); + const tessellations = getFieldValue(block, 'TESSELLATIONS', '12'); const positionSource = getPositionTuple(block); @@ -307,14 +288,14 @@ export function registerSceneGenerators(javascriptGenerator) { `position: ${positionSource}`, ]; - return createMesh(block, "Cylinder", params, "cylinder"); + return createMesh(block, 'Cylinder', params, 'cylinder'); }; // Add capsule -------------------------------------------------------- - javascriptGenerator.forBlock["create_capsule"] = function (block) { - const color = getFieldValue(block, "COLOR", '"#9932CC"'); - const diameter = getFieldValue(block, "DIAMETER", "1"); - const height = getFieldValue(block, "HEIGHT", "2"); + javascriptGenerator.forBlock['create_capsule'] = function (block) { + const color = getFieldValue(block, 'COLOR', '"#9932CC"'); + const diameter = getFieldValue(block, 'DIAMETER', '1'); + const height = getFieldValue(block, 'HEIGHT', '2'); const positionSource = getPositionTuple(block); @@ -325,14 +306,14 @@ export function registerSceneGenerators(javascriptGenerator) { `position: ${positionSource}`, ]; - return createMesh(block, "Capsule", params, "capsule"); + return createMesh(block, 'Capsule', params, 'capsule'); }; // Add plane ---------------------------------------------------------- - javascriptGenerator.forBlock["create_plane"] = function (block) { - const color = getFieldValue(block, "COLOR", '"#9932CC"'); - const width = getFieldValue(block, "WIDTH", "1"); - const height = getFieldValue(block, "HEIGHT", "1"); + javascriptGenerator.forBlock['create_plane'] = function (block) { + const color = getFieldValue(block, 'COLOR', '"#9932CC"'); + const width = getFieldValue(block, 'WIDTH', '1'); + const height = getFieldValue(block, 'HEIGHT', '1'); const positionSource = getPositionTuple(block); @@ -343,194 +324,139 @@ export function registerSceneGenerators(javascriptGenerator) { `position: ${positionSource}`, ]; - return createMesh(block, "Plane", params, "plane"); + return createMesh(block, 'Plane', params, 'plane'); }; // Add clone ---------------------------------------------------------- - javascriptGenerator.forBlock["clone_mesh"] = function (block) { + javascriptGenerator.forBlock['clone_mesh'] = function (block) { // Get the source mesh variable const sourceMeshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("SOURCE_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('SOURCE_MESH'), + Blockly.Names.NameType.VARIABLE ); // Get the target clone variable const cloneVariableName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("CLONE_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('CLONE_VAR'), + Blockly.Names.NameType.VARIABLE ); // Generate a unique ID for the clone - const cloneId = sourceMeshName + "_" + generateUniqueId(); + const cloneId = sourceMeshName + '_' + generateUniqueId(); meshMap[cloneId] = block; meshBlockIdMap[cloneId] = block.id; // Generate the code for the "do" part (if present) - let doCode = ""; - if (block.getInput("DO")) { - doCode = javascriptGenerator.statementToCode(block, "DO") || ""; + let doCode = ''; + if (block.getInput('DO')) { + doCode = javascriptGenerator.statementToCode(block, 'DO') || ''; } // Wrap "DO" code in an async function if it exists - doCode = doCode ? `async function() {\n${doCode}\n}` : ""; + doCode = doCode ? `async function() {\n${doCode}\n}` : ''; // Return the code to clone the mesh return `${cloneVariableName} = cloneMesh({ sourceMeshName: ${sourceMeshName}, - cloneId: '${cloneId}'${doCode ? `,\ncallback: ${doCode}` : ""} + cloneId: '${cloneId}'${doCode ? `,\ncallback: ${doCode}` : ''} });\n`; }; // ------------------------------- // EFFECTS // ------------------------------- // Light intensity ------------------------------------------------- - javascriptGenerator.forBlock["main_light"] = function (block) { + javascriptGenerator.forBlock['main_light'] = function (block) { const intensity = - javascriptGenerator.valueToCode( - block, - "INTENSITY", - javascriptGenerator.ORDER_ATOMIC, - ) || "1.0"; + javascriptGenerator.valueToCode(block, 'INTENSITY', javascriptGenerator.ORDER_ATOMIC) || + '1.0'; const diffuse = - javascriptGenerator.valueToCode( - block, - "DIFFUSE", - javascriptGenerator.ORDER_ATOMIC, - ) || "#FFFFFF"; + javascriptGenerator.valueToCode(block, 'DIFFUSE', javascriptGenerator.ORDER_ATOMIC) || + '#FFFFFF'; const groundColor = - javascriptGenerator.valueToCode( - block, - "GROUND_COLOR", - javascriptGenerator.ORDER_ATOMIC, - ) || "#808080"; + javascriptGenerator.valueToCode(block, 'GROUND_COLOR', javascriptGenerator.ORDER_ATOMIC) || + '#808080'; return `lightIntensity(${intensity});\nlightColor(${diffuse}, ${groundColor});\n`; }; // Get light as -------------------------------------------------- - javascriptGenerator.forBlock["get_light"] = function (block) { + javascriptGenerator.forBlock['get_light'] = function (block) { const variableName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('VAR'), + Blockly.Names.NameType.VARIABLE ); return `${variableName} = getMainLight();\n`; }; // Add particle effect on object ---------------------------------- - javascriptGenerator.forBlock["create_particle_effect"] = function (block) { + javascriptGenerator.forBlock['create_particle_effect'] = function (block) { const emitRate = parseFloat( - javascriptGenerator.valueToCode( - block, - "RATE", - javascriptGenerator.ORDER_ATOMIC, - ) || "10", + javascriptGenerator.valueToCode(block, 'RATE', javascriptGenerator.ORDER_ATOMIC) || '10' ); const startColor = - javascriptGenerator.valueToCode( - block, - "START_COLOR", - javascriptGenerator.ORDER_ATOMIC, - ) || '"#FFFFFF"'; + javascriptGenerator.valueToCode(block, 'START_COLOR', javascriptGenerator.ORDER_ATOMIC) || + '"#FFFFFF"'; const endColor = - javascriptGenerator.valueToCode( - block, - "END_COLOR", - javascriptGenerator.ORDER_ATOMIC, - ) || '"#000000"'; + javascriptGenerator.valueToCode(block, 'END_COLOR', javascriptGenerator.ORDER_ATOMIC) || + '"#000000"'; const startAlpha = parseFloat( - javascriptGenerator.valueToCode( - block, - "START_ALPHA", - javascriptGenerator.ORDER_ATOMIC, - ) || "1.0", + javascriptGenerator.valueToCode(block, 'START_ALPHA', javascriptGenerator.ORDER_ATOMIC) || + '1.0' ); const endAlpha = parseFloat( - javascriptGenerator.valueToCode( - block, - "END_ALPHA", - javascriptGenerator.ORDER_ATOMIC, - ) || "1.0", + javascriptGenerator.valueToCode(block, 'END_ALPHA', javascriptGenerator.ORDER_ATOMIC) || '1.0' ); const minSize = - javascriptGenerator.valueToCode( - block, - "MIN_SIZE", - javascriptGenerator.ORDER_ATOMIC, - ) || "0.1"; + javascriptGenerator.valueToCode(block, 'MIN_SIZE', javascriptGenerator.ORDER_ATOMIC) || '0.1'; const maxSize = - javascriptGenerator.valueToCode( - block, - "MAX_SIZE", - javascriptGenerator.ORDER_ATOMIC, - ) || "1.0"; + javascriptGenerator.valueToCode(block, 'MAX_SIZE', javascriptGenerator.ORDER_ATOMIC) || '1.0'; const minLifetime = - javascriptGenerator.valueToCode( - block, - "MIN_LIFETIME", - javascriptGenerator.ORDER_ATOMIC, - ) || "1.0"; + javascriptGenerator.valueToCode(block, 'MIN_LIFETIME', javascriptGenerator.ORDER_ATOMIC) || + '1.0'; const maxLifetime = - javascriptGenerator.valueToCode( - block, - "MAX_LIFETIME", - javascriptGenerator.ORDER_ATOMIC, - ) || "5.0"; - const x = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const y = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const z = - javascriptGenerator.valueToCode( - block, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'MAX_LIFETIME', javascriptGenerator.ORDER_ATOMIC) || + '5.0'; + const x = javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC) || '0'; + const y = javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC) || '0'; + const z = javascriptGenerator.valueToCode(block, 'Z', javascriptGenerator.ORDER_ATOMIC) || '0'; const minAngularSpeed = javascriptGenerator.valueToCode( block, - "MIN_ANGULAR_SPEED", - javascriptGenerator.ORDER_ATOMIC, + 'MIN_ANGULAR_SPEED', + javascriptGenerator.ORDER_ATOMIC ) || 0; const maxAngularSpeed = javascriptGenerator.valueToCode( block, - "MAX_ANGULAR_SPEED", - javascriptGenerator.ORDER_ATOMIC, + 'MAX_ANGULAR_SPEED', + javascriptGenerator.ORDER_ATOMIC ) || 0; const minInitialRotation = javascriptGenerator.valueToCode( block, - "MIN_INITIAL_ROTATION", - javascriptGenerator.ORDER_ATOMIC, + 'MIN_INITIAL_ROTATION', + javascriptGenerator.ORDER_ATOMIC ) || 0; const maxInitialRotation = javascriptGenerator.valueToCode( block, - "MAX_INITIAL_ROTATION", - javascriptGenerator.ORDER_ATOMIC, + 'MAX_INITIAL_ROTATION', + javascriptGenerator.ORDER_ATOMIC ) || 0; const variableName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("ID_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('ID_VAR'), + Blockly.Names.NameType.VARIABLE ); - const { - generatedName: emitterMesh, - userVariableName: emitterMeshName, - } = getVariableInfo(block, "EMITTER_MESH"); + const { generatedName: emitterMesh, userVariableName: emitterMeshName } = getVariableInfo( + block, + 'EMITTER_MESH' + ); - const shape = block.getFieldValue("SHAPE"); - const gravity = block.getFieldValue("GRAVITY") === "TRUE"; + const shape = block.getFieldValue('SHAPE'); + const gravity = block.getFieldValue('GRAVITY') === 'TRUE'; const options = ` { @@ -571,38 +497,26 @@ export function registerSceneGenerators(javascriptGenerator) { }; // Particle system ------------------------------------------------ - javascriptGenerator.forBlock["control_particle_system"] = function (block) { + javascriptGenerator.forBlock['control_particle_system'] = function (block) { const systemName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("SYSTEM_NAME"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('SYSTEM_NAME'), + Blockly.Names.NameType.VARIABLE ); - const action = block.getFieldValue("ACTION"); + const action = block.getFieldValue('ACTION'); return `${action}ParticleSystem(${systemName});\n`; }; // Set fog color -------------------------------------------------- - javascriptGenerator.forBlock["set_fog"] = function (block) { - const fogColorHex = getFieldValue(block, "FOG_COLOR", "#9932CC"); - const fogMode = block.getFieldValue("FOG_MODE"); + javascriptGenerator.forBlock['set_fog'] = function (block) { + const fogColorHex = getFieldValue(block, 'FOG_COLOR', '#9932CC'); + const fogMode = block.getFieldValue('FOG_MODE'); const fogDensity = - javascriptGenerator.valueToCode( - block, - "DENSITY", - javascriptGenerator.ORDER_ATOMIC, - ) || "0.1"; // Default density + javascriptGenerator.valueToCode(block, 'DENSITY', javascriptGenerator.ORDER_ATOMIC) || '0.1'; // Default density const fogStart = - javascriptGenerator.valueToCode( - block, - "START", - javascriptGenerator.ORDER_ATOMIC, - ) || "50"; // Default start + javascriptGenerator.valueToCode(block, 'START', javascriptGenerator.ORDER_ATOMIC) || '50'; // Default start const fogEnd = - javascriptGenerator.valueToCode( - block, - "END", - javascriptGenerator.ORDER_ATOMIC, - ) || "100"; // Default end + javascriptGenerator.valueToCode(block, 'END', javascriptGenerator.ORDER_ATOMIC) || '100'; // Default end return `setFog({ fogColorHex: ${fogColorHex}, fogMode: "${fogMode}", fogDensity: ${fogDensity}, fogStart: ${fogStart}, fogEnd: ${fogEnd} });\n`; }; @@ -611,38 +525,34 @@ export function registerSceneGenerators(javascriptGenerator) { // CAMERA // ------------------------------- // Get camera as -------------------------------------------------- - javascriptGenerator.forBlock["get_camera"] = function (block) { + javascriptGenerator.forBlock['get_camera'] = function (block) { const variableName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('VAR'), + Blockly.Names.NameType.VARIABLE ); return `${variableName} = getCamera();\n`; }; // Camera follow object ------------------------------------------- - javascriptGenerator.forBlock["camera_follow"] = function (block) { + javascriptGenerator.forBlock['camera_follow'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); const radius = - javascriptGenerator.valueToCode( - block, - "RADIUS", - javascriptGenerator.ORDER_ATOMIC, - ) || 7; + javascriptGenerator.valueToCode(block, 'RADIUS', javascriptGenerator.ORDER_ATOMIC) || 7; - const front = block.getFieldValue("FRONT") === "TRUE"; + const front = block.getFieldValue('FRONT') === 'TRUE'; return `await attachCamera(${modelName}, { radius: ${radius}, front: ${front} });\n`; }; // Camera rotate -------------------------------------------------- - javascriptGenerator.forBlock["camera_control"] = function (block) { - const key = block.getFieldValue("KEY"); - const action = block.getFieldValue("ACTION"); + javascriptGenerator.forBlock['camera_control'] = function (block) { + const key = block.getFieldValue('KEY'); + const action = block.getFieldValue('ACTION'); return `cameraControl(${JSON.stringify(key)}, "${action}");\n`; }; @@ -651,26 +561,26 @@ export function registerSceneGenerators(javascriptGenerator) { // XR // ------------------------------- // Use camera as background --------------------------------------- - javascriptGenerator.forBlock["device_camera_background"] = function (block) { - const cameraType = block.getFieldValue("CAMERA"); + javascriptGenerator.forBlock['device_camera_background'] = function (block) { + const cameraType = block.getFieldValue('CAMERA'); return `setCameraBackground("${cameraType}");\n`; }; // Set XR mode to ------------------------------------------------- - javascriptGenerator.forBlock["set_xr_mode"] = function (block) { - const mode = block.getFieldValue("MODE"); + javascriptGenerator.forBlock['set_xr_mode'] = function (block) { + const mode = block.getFieldValue('MODE'); return `await setXRMode("${mode}");\n`; }; // Export object as ----------------------------------------------- - javascriptGenerator.forBlock["export_mesh"] = function (block) { + javascriptGenerator.forBlock['export_mesh'] = function (block) { const meshVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const format = block.getFieldValue("FORMAT"); + const format = block.getFieldValue('FORMAT'); // Generate the code that calls the helper function return `exportMesh(${meshVar}, "${format}");\n`; diff --git a/generators/generators-sensing.js b/generators/generators-sensing.js index 37371f3c..d9637633 100644 --- a/generators/generators-sensing.js +++ b/generators/generators-sensing.js @@ -1,52 +1,52 @@ -import * as Blockly from "blockly"; -import { getFieldValue } from "./generators-utilities.js"; +import * as Blockly from 'blockly'; +import { getFieldValue } from './generators-utilities.js'; export function registerSensingGenerators(javascriptGenerator) { // ------------------------------- // SENSING // ------------------------------- // Movement or action control - javascriptGenerator.forBlock["action_pressed"] = function (block) { - const action = block.getFieldValue("ACTION"); + javascriptGenerator.forBlock['action_pressed'] = function (block) { + const action = block.getFieldValue('ACTION'); return [`actionPressed("${action}")`, javascriptGenerator.ORDER_NONE]; }; // Set % key to % - javascriptGenerator.forBlock["set_action_key"] = function (block) { - const action = block.getFieldValue("ACTION"); - const key = block.getFieldValue("KEY"); + javascriptGenerator.forBlock['set_action_key'] = function (block) { + const action = block.getFieldValue('ACTION'); + const key = block.getFieldValue('KEY'); return `setActionKey("${action}", ${JSON.stringify(key)});\n`; }; // Object exists? - javascriptGenerator.forBlock["mesh_exists"] = function (block) { + javascriptGenerator.forBlock['mesh_exists'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); return [`meshExists(${modelName})`, javascriptGenerator.ORDER_NONE]; }; // Is object touching surface - javascriptGenerator.forBlock["touching_surface"] = function (block) { + javascriptGenerator.forBlock['touching_surface'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); return [`isTouchingSurface(${modelName})`, javascriptGenerator.ORDER_NONE]; }; // Object touching object - javascriptGenerator.forBlock["meshes_touching"] = function (block) { + javascriptGenerator.forBlock['meshes_touching'] = function (block) { const mesh1VarName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH1"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH1'), + Blockly.Names.NameType.VARIABLE ); const mesh2VarName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH2"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH2'), + Blockly.Names.NameType.VARIABLE ); const code = `checkMeshesTouching(${mesh1VarName}, ${mesh2VarName})`; @@ -54,27 +54,27 @@ export function registerSensingGenerators(javascriptGenerator) { }; // Get property of object - javascriptGenerator.forBlock["get_property"] = function (block) { + javascriptGenerator.forBlock['get_property'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH'), + Blockly.Names.NameType.VARIABLE ); - const propertyName = block.getFieldValue("PROPERTY"); + const propertyName = block.getFieldValue('PROPERTY'); const code = `getProperty(${modelName}, '${propertyName}')`; return [code, javascriptGenerator.ORDER_NONE]; }; // Distance from object to object - javascriptGenerator.forBlock["distance_to"] = function (block) { + javascriptGenerator.forBlock['distance_to'] = function (block) { const meshName1 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL1"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL1'), + Blockly.Names.NameType.VARIABLE ); const meshName2 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL2"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL2'), + Blockly.Names.NameType.VARIABLE ); const code = `distanceTo(${meshName1}, ${meshName2})`; @@ -82,51 +82,51 @@ export function registerSensingGenerators(javascriptGenerator) { }; // Ground level - javascriptGenerator.forBlock["ground_level"] = function () { - const code = "-999999"; + javascriptGenerator.forBlock['ground_level'] = function () { + const code = '-999999'; return [code, javascriptGenerator.ORDER_NONE]; }; // Time in seconds - javascriptGenerator.forBlock["time"] = function (block) { - const unit = block.getFieldValue("UNIT") || "seconds"; + javascriptGenerator.forBlock['time'] = function (block) { + const unit = block.getFieldValue('UNIT') || 'seconds'; const code = `getTime("${unit}")`; return [code, javascriptGenerator.ORDER_NONE]; }; // Canvas controls - javascriptGenerator.forBlock["canvas_controls"] = function (block) { - const controls = block.getFieldValue("CONTROLS") == "TRUE"; + javascriptGenerator.forBlock['canvas_controls'] = function (block) { + const controls = block.getFieldValue('CONTROLS') == 'TRUE'; return `canvasControls(${controls});\n`; }; // Interact indicator - javascriptGenerator.forBlock["interact_indicator"] = function (block) { - const enabled = block.getFieldValue("ENABLED") == "TRUE"; + javascriptGenerator.forBlock['interact_indicator'] = function (block) { + const enabled = block.getFieldValue('ENABLED') == 'TRUE'; return `interactIndicator(${enabled});\n`; }; // Button controls - javascriptGenerator.forBlock["button_controls"] = function (block) { - const color = getFieldValue(block, "COLOR", '"#ffffff"'); - const control = block.getFieldValue("CONTROL"); - const mode = block.getFieldValue("ENABLED"); + javascriptGenerator.forBlock['button_controls'] = function (block) { + const color = getFieldValue(block, 'COLOR', '"#ffffff"'); + const control = block.getFieldValue('CONTROL'); + const mode = block.getFieldValue('ENABLED'); return `buttonControls("${control}", "${mode}", ${color});\n`; }; // On-screen controls - javascriptGenerator.forBlock["on_screen_controls"] = function (block) { - const color = getFieldValue(block, "COLOR", '"#ffffff"'); - const movement = block.getFieldValue("MOVEMENT"); - const actions = block.getFieldValue("ACTIONS"); - const mode = block.getFieldValue("ENABLED"); + javascriptGenerator.forBlock['on_screen_controls'] = function (block) { + const color = getFieldValue(block, 'COLOR', '"#ffffff"'); + const movement = block.getFieldValue('MOVEMENT'); + const actions = block.getFieldValue('ACTIONS'); + const mode = block.getFieldValue('ENABLED'); return `onScreenControls("${movement}", "${actions}", "${mode}", ${color});\n`; }; // When micro:bit event occurs - javascriptGenerator.forBlock["microbit_input"] = function (block) { - const event = block.getFieldValue("EVENT"); - const statements_do = javascriptGenerator.statementToCode(block, "DO"); + javascriptGenerator.forBlock['microbit_input'] = function (block) { + const event = block.getFieldValue('EVENT'); + const statements_do = javascriptGenerator.statementToCode(block, 'DO'); return `whenKeyEvent("${event}", async () => {${statements_do}});\n`; }; diff --git a/generators/generators-sound.js b/generators/generators-sound.js index e8a99e23..6d6be7a6 100644 --- a/generators/generators-sound.js +++ b/generators/generators-sound.js @@ -1,184 +1,134 @@ -import * as Blockly from "blockly"; +import * as Blockly from 'blockly'; export function registerSoundGenerators(javascriptGenerator) { // ------------------------------- // SOUND // ------------------------------- // Play theme ----------------------------------------------- - javascriptGenerator.forBlock["play_theme"] = function (block) { + javascriptGenerator.forBlock['play_theme'] = function (block) { const idVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("ID_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('ID_VAR'), + Blockly.Names.NameType.VARIABLE ); - const meshNameField = block.getFieldValue("MESH_NAME"); + const meshNameField = block.getFieldValue('MESH_NAME'); const meshName = `"${meshNameField}"`; - const themeName = block.getFieldValue("THEME_NAME"); + const themeName = block.getFieldValue('THEME_NAME'); const speedCode = - javascriptGenerator.valueToCode( - block, - "SPEED", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'SPEED', javascriptGenerator.ORDER_ATOMIC) || '1'; const volumeCode = - javascriptGenerator.valueToCode( - block, - "VOLUME", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'VOLUME', javascriptGenerator.ORDER_ATOMIC) || '1'; - const loop = block.getFieldValue("MODE") === "LOOP"; - const asyncMode = block.getFieldValue("ASYNC"); + const loop = block.getFieldValue('MODE') === 'LOOP'; + const asyncMode = block.getFieldValue('ASYNC'); - const code = `${idVar} = ${asyncMode === "AWAIT" ? "await " : ""}playSound(${meshName}, { soundName: "${themeName}", loop: ${loop}, volume: ${volumeCode}, playbackRate: ${speedCode} });\n`; + const code = `${idVar} = ${asyncMode === 'AWAIT' ? 'await ' : ''}playSound(${meshName}, { soundName: "${themeName}", loop: ${loop}, volume: ${volumeCode}, playbackRate: ${speedCode} });\n`; return code; }; // Play sound ----------------------------------------------- - javascriptGenerator.forBlock["play_sound"] = function (block) { + javascriptGenerator.forBlock['play_sound'] = function (block) { const idVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("ID_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('ID_VAR'), + Blockly.Names.NameType.VARIABLE ); - const meshNameField = block.getFieldValue("MESH_NAME"); + const meshNameField = block.getFieldValue('MESH_NAME'); const meshName = `"${meshNameField}"`; // Always quoted - const soundName = block.getFieldValue("SOUND_NAME"); + const soundName = block.getFieldValue('SOUND_NAME'); const speedCode = - javascriptGenerator.valueToCode( - block, - "SPEED", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'SPEED', javascriptGenerator.ORDER_ATOMIC) || '1'; const volumeCode = - javascriptGenerator.valueToCode( - block, - "VOLUME", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'VOLUME', javascriptGenerator.ORDER_ATOMIC) || '1'; - const loop = block.getFieldValue("MODE") === "LOOP"; - const asyncMode = block.getFieldValue("ASYNC"); + const loop = block.getFieldValue('MODE') === 'LOOP'; + const asyncMode = block.getFieldValue('ASYNC'); // Build the final code line - const code = `${idVar} = ${asyncMode === "AWAIT" ? "await " : ""}playSound(${meshName}, { soundName: "${soundName}", loop: ${loop}, volume: ${volumeCode}, playbackRate: ${speedCode} });\n`; + const code = `${idVar} = ${asyncMode === 'AWAIT' ? 'await ' : ''}playSound(${meshName}, { soundName: "${soundName}", loop: ${loop}, volume: ${volumeCode}, playbackRate: ${speedCode} });\n`; return code; }; // Stop all sounds --------------------------------------------- - javascriptGenerator.forBlock["stop_all_sounds"] = function (_block) { + javascriptGenerator.forBlock['stop_all_sounds'] = function (_block) { // JavaScript code to stop all sounds in a Babylon.js scene - return "stopAllSounds();\n"; + return 'stopAllSounds();\n'; }; // MIDI note --------------------------------------------------- - javascriptGenerator.forBlock["midi_note"] = function (block) { + javascriptGenerator.forBlock['midi_note'] = function (block) { const note = - javascriptGenerator.valueToCode( - block, - "NOTE", - javascriptGenerator.ORDER_ATOMIC, - ) || "60"; + javascriptGenerator.valueToCode(block, 'NOTE', javascriptGenerator.ORDER_ATOMIC) || '60'; return [note, javascriptGenerator.ORDER_ATOMIC]; }; // Note -------------------------------------------------------- - javascriptGenerator.forBlock["note"] = function (block) { + javascriptGenerator.forBlock['note'] = function (block) { const pitch = - javascriptGenerator.valueToCode( - block, - "PITCH", - javascriptGenerator.ORDER_ATOMIC, - ) || "60"; + javascriptGenerator.valueToCode(block, 'PITCH', javascriptGenerator.ORDER_ATOMIC) || '60'; const duration = - javascriptGenerator.valueToCode( - block, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, - ) || "0.5"; - return [ - `{ pitch: ${pitch}, duration: ${duration} }`, - javascriptGenerator.ORDER_ATOMIC, - ]; + javascriptGenerator.valueToCode(block, 'DURATION', javascriptGenerator.ORDER_ATOMIC) || '0.5'; + return [`{ pitch: ${pitch}, duration: ${duration} }`, javascriptGenerator.ORDER_ATOMIC]; }; // Rest -------------------------------------------------------- - javascriptGenerator.forBlock["rest"] = function () { - return ["null", javascriptGenerator.ORDER_ATOMIC]; + javascriptGenerator.forBlock['rest'] = function () { + return ['null', javascriptGenerator.ORDER_ATOMIC]; }; // Play music -------------------------------------------------- - javascriptGenerator.forBlock["play_tune_notes"] = function (block) { - const meshNameField = block.getFieldValue("MESH_NAME"); + javascriptGenerator.forBlock['play_tune_notes'] = function (block) { + const meshNameField = block.getFieldValue('MESH_NAME'); const meshName = `"${meshNameField}"`; const notes = - javascriptGenerator.valueToCode( - block, - "NOTES", - javascriptGenerator.ORDER_ATOMIC, - ) || "[]"; + javascriptGenerator.valueToCode(block, 'NOTES', javascriptGenerator.ORDER_ATOMIC) || '[]'; const instrument = - javascriptGenerator.valueToCode( - block, - "INSTRUMENT", - javascriptGenerator.ORDER_ATOMIC, - ) || 'createInstrument("sine")'; + javascriptGenerator.valueToCode(block, 'INSTRUMENT', javascriptGenerator.ORDER_ATOMIC) || + 'createInstrument("sine")'; return `await playMusic(${meshName}, { notes: ${notes}, instrument: ${instrument} });\n`; }; // Set music speed --------------------------------------------- - javascriptGenerator.forBlock["set_music_speed"] = function (block) { - const meshNameField = block.getFieldValue("MESH_NAME"); + javascriptGenerator.forBlock['set_music_speed'] = function (block) { + const meshNameField = block.getFieldValue('MESH_NAME'); const meshName = `"${meshNameField}"`; const speed = - javascriptGenerator.valueToCode( - block, - "SPEED", - javascriptGenerator.ORDER_ATOMIC, - ) || "1.0"; + javascriptGenerator.valueToCode(block, 'SPEED', javascriptGenerator.ORDER_ATOMIC) || '1.0'; return `setMusicSpeed(${meshName}, ${speed});\n`; }; // Play notes -------------------------------------------------- - javascriptGenerator.forBlock["play_notes"] = function (block) { + javascriptGenerator.forBlock['play_notes'] = function (block) { const meshVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH'), + Blockly.Names.NameType.VARIABLE ); const notes = - javascriptGenerator.valueToCode( - block, - "NOTES", - javascriptGenerator.ORDER_ATOMIC, - ) || "[]"; + javascriptGenerator.valueToCode(block, 'NOTES', javascriptGenerator.ORDER_ATOMIC) || '[]'; const durations = - javascriptGenerator.valueToCode( - block, - "DURATIONS", - javascriptGenerator.ORDER_ATOMIC, - ) || "[]"; + javascriptGenerator.valueToCode(block, 'DURATIONS', javascriptGenerator.ORDER_ATOMIC) || '[]'; const instrument = javascriptGenerator.valueToCode( block, - "INSTRUMENT", - javascriptGenerator.ORDER_ATOMIC, + 'INSTRUMENT', + javascriptGenerator.ORDER_ATOMIC ); - const asyncMode = block.getFieldValue("ASYNC"); + const asyncMode = block.getFieldValue('ASYNC'); // Use the appropriate function based on the async mode - if (asyncMode === "AWAIT") { + if (asyncMode === 'AWAIT') { return `await playNotes(${meshVar}, { notes: ${notes}, durations: ${durations}, instrument: ${instrument} });\n`; } else { return `playNotes(${meshVar}, { notes: ${notes}, durations: ${durations}, instrument: ${instrument} });\n`; @@ -186,24 +136,24 @@ export function registerSoundGenerators(javascriptGenerator) { }; // Play tune (ABC import) -------------------------------------- - javascriptGenerator.forBlock["play_tune"] = function (block) { - if (!block.getInput("DO")) return ""; - return javascriptGenerator.statementToCode(block, "DO") || ""; + javascriptGenerator.forBlock['play_tune'] = function (block) { + if (!block.getInput('DO')) return ''; + return javascriptGenerator.statementToCode(block, 'DO') || ''; }; // Instrument ----------------------------------------------- - javascriptGenerator.forBlock["instrument"] = function (block) { - const instrumentType = block.getFieldValue("INSTRUMENT_TYPE"); + javascriptGenerator.forBlock['instrument'] = function (block) { + const instrumentType = block.getFieldValue('INSTRUMENT_TYPE'); let instrumentCode; switch (instrumentType) { - case "piano": + case 'piano': instrumentCode = `createInstrument("square", { attack: 0.1, decay: 0.3, sustain: 0.7, release: 1.0 })`; break; - case "guitar": + case 'guitar': instrumentCode = `createInstrument("sawtooth", { attack: 0.1, decay: 0.2, sustain: 0.6, release: 0.9 })`; break; - case "violin": + case 'violin': instrumentCode = `createInstrument("triangle", { attack: 0.15, decay: 0.5, sustain: 0.8, release: 1.2 })`; break; default: @@ -214,111 +164,67 @@ export function registerSoundGenerators(javascriptGenerator) { }; // Create Instrument ----------------------------------------- - javascriptGenerator.forBlock["create_instrument"] = function (block) { + javascriptGenerator.forBlock['create_instrument'] = function (block) { const instrumentVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("INSTRUMENT"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('INSTRUMENT'), + Blockly.Names.NameType.VARIABLE ); - const type = block.getFieldValue("TYPE"); - const effect = block.getFieldValue("EFFECT"); + const type = block.getFieldValue('TYPE'); + const effect = block.getFieldValue('EFFECT'); const volume = - javascriptGenerator.valueToCode( - block, - "VOLUME", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'VOLUME', javascriptGenerator.ORDER_ATOMIC) || '1'; const effectRate = - javascriptGenerator.valueToCode( - block, - "EFFECT_RATE", - javascriptGenerator.ORDER_ATOMIC, - ) || "5"; + javascriptGenerator.valueToCode(block, 'EFFECT_RATE', javascriptGenerator.ORDER_ATOMIC) || + '5'; const effectDepth = - javascriptGenerator.valueToCode( - block, - "EFFECT_DEPTH", - javascriptGenerator.ORDER_ATOMIC, - ) || "0.5"; + javascriptGenerator.valueToCode(block, 'EFFECT_DEPTH', javascriptGenerator.ORDER_ATOMIC) || + '0.5'; const attack = - javascriptGenerator.valueToCode( - block, - "ATTACK", - javascriptGenerator.ORDER_ATOMIC, - ) || "0.1"; + javascriptGenerator.valueToCode(block, 'ATTACK', javascriptGenerator.ORDER_ATOMIC) || '0.1'; const decay = - javascriptGenerator.valueToCode( - block, - "DECAY", - javascriptGenerator.ORDER_ATOMIC, - ) || "0.5"; + javascriptGenerator.valueToCode(block, 'DECAY', javascriptGenerator.ORDER_ATOMIC) || '0.5'; const sustain = - javascriptGenerator.valueToCode( - block, - "SUSTAIN", - javascriptGenerator.ORDER_ATOMIC, - ) || "0.7"; + javascriptGenerator.valueToCode(block, 'SUSTAIN', javascriptGenerator.ORDER_ATOMIC) || '0.7'; const release = - javascriptGenerator.valueToCode( - block, - "RELEASE", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'RELEASE', javascriptGenerator.ORDER_ATOMIC) || '1'; // Assign the instrument to a variable return `${instrumentVar} = createInstrument('${type}', { volume: ${volume}, effect: '${effect}', effectRate: ${effectRate}, effectDepth: ${effectDepth}, attack: ${attack}, decay: ${decay}, sustain: ${sustain}, release: ${release} });\n`; }; // Speak ------------------------------------------------------ - javascriptGenerator.forBlock["speak"] = function (block) { + javascriptGenerator.forBlock['speak'] = function (block) { const text = - javascriptGenerator.valueToCode( - block, - "TEXT", - javascriptGenerator.ORDER_ATOMIC, - ) || '""'; + javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_ATOMIC) || '""'; - const voice = block.getFieldValue("VOICE") || "default"; + const voice = block.getFieldValue('VOICE') || 'default'; const rate = - javascriptGenerator.valueToCode( - block, - "RATE", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'RATE', javascriptGenerator.ORDER_ATOMIC) || '1'; const pitch = - javascriptGenerator.valueToCode( - block, - "PITCH", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'PITCH', javascriptGenerator.ORDER_ATOMIC) || '1'; const volume = - javascriptGenerator.valueToCode( - block, - "VOLUME", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'VOLUME', javascriptGenerator.ORDER_ATOMIC) || '1'; - const language = block.getFieldValue("LANGUAGE") || "en-US"; - const asyncMode = block.getFieldValue("ASYNC") || "START"; + const language = block.getFieldValue('LANGUAGE') || 'en-US'; + const asyncMode = block.getFieldValue('ASYNC') || 'START'; // Get the mesh variable name from the dynamic dropdown - same approach as play_sound block - const meshInput = block.getInput("MESH_INPUT"); + const meshInput = block.getInput('MESH_INPUT'); const meshDropdownField = meshInput - ? meshInput.fieldRow.find((field) => field.name === "MESH_NAME") + ? meshInput.fieldRow.find((field) => field.name === 'MESH_NAME') : null; - const meshValue = meshDropdownField - ? meshDropdownField.getValue() - : "__everywhere__"; + const meshValue = meshDropdownField ? meshDropdownField.getValue() : '__everywhere__'; const meshVariable = `"${meshValue}"`; // Safely handle asyncMode - ensure it's not null - const safeAsyncMode = asyncMode || "START"; - const asyncWrapper = safeAsyncMode === "AWAIT" ? "await " : ""; + const safeAsyncMode = asyncMode || 'START'; + const asyncWrapper = safeAsyncMode === 'AWAIT' ? 'await ' : ''; return `${asyncWrapper}speak(${meshVariable}, ${text}, { voice: "${voice}", rate: ${rate}, pitch: ${pitch}, volume: ${volume}, language: "${language}", mode: "${safeAsyncMode.toLowerCase()}" });\n`; }; diff --git a/generators/generators-text.js b/generators/generators-text.js index eb959aea..c782cba1 100644 --- a/generators/generators-text.js +++ b/generators/generators-text.js @@ -1,32 +1,24 @@ -import * as Blockly from "blockly"; -import { meshMap, meshBlockIdMap, generateUniqueId } from "./mesh-state.js"; +import * as Blockly from 'blockly'; +import { meshMap, meshBlockIdMap, generateUniqueId } from './mesh-state.js'; import { getFieldValue, sanitizeForCode, emitSafeTextArg, getVariableInfo, -} from "./generators-utilities.js"; +} from './generators-utilities.js'; export function registerTextGenerators(javascriptGenerator) { // ------------------------------- // TEXT // ------------------------------- // Print -------------------------------------------------- - javascriptGenerator.forBlock["print_text"] = function (block) { + javascriptGenerator.forBlock['print_text'] = function (block) { const textCode = - javascriptGenerator.valueToCode( - block, - "TEXT", - javascriptGenerator.ORDER_NONE, - ) || "''"; + javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_NONE) || "''"; const durationCode = - javascriptGenerator.valueToCode( - block, - "DURATION", - javascriptGenerator.ORDER_NONE, - ) || "0"; + javascriptGenerator.valueToCode(block, 'DURATION', javascriptGenerator.ORDER_NONE) || '0'; - const color = getFieldValue(block, "COLOR", '"#9932CC"'); + const color = getFieldValue(block, 'COLOR', '"#9932CC"'); const safeTextArg = emitSafeTextArg(textCode); @@ -34,43 +26,27 @@ export function registerTextGenerators(javascriptGenerator) { }; // Say ---------------------------------------------------- - javascriptGenerator.forBlock["say"] = function (block) { + javascriptGenerator.forBlock['say'] = function (block) { const textCode = - javascriptGenerator.valueToCode( - block, - "TEXT", - javascriptGenerator.ORDER_ATOMIC, - ) || '""'; + javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_ATOMIC) || '""'; const durationCode = - javascriptGenerator.valueToCode( - block, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'DURATION', javascriptGenerator.ORDER_ATOMIC) || '0'; const alphaCode = - javascriptGenerator.valueToCode( - block, - "ALPHA", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'ALPHA', javascriptGenerator.ORDER_ATOMIC) || '1'; const sizeCode = - javascriptGenerator.valueToCode( - block, - "SIZE", - javascriptGenerator.ORDER_ATOMIC, - ) || "24"; + javascriptGenerator.valueToCode(block, 'SIZE', javascriptGenerator.ORDER_ATOMIC) || '24'; const meshVariable = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const textColor = getFieldValue(block, "TEXT_COLOR", "#000000"); - const backgroundColor = getFieldValue(block, "BACKGROUND_COLOR", "#ffffff"); + const textColor = getFieldValue(block, 'TEXT_COLOR', '#000000'); + const backgroundColor = getFieldValue(block, 'BACKGROUND_COLOR', '#ffffff'); - const mode = block.getFieldValue("MODE") || ""; - const asyncMode = block.getFieldValue("ASYNC"); - const asyncWrapper = asyncMode === "AWAIT" ? "await " : ""; + const mode = block.getFieldValue('MODE') || ''; + const asyncMode = block.getFieldValue('ASYNC'); + const asyncWrapper = asyncMode === 'AWAIT' ? 'await ' : ''; const safeTextArg = emitSafeTextArg(textCode); @@ -78,47 +54,23 @@ export function registerTextGenerators(javascriptGenerator) { }; // UI Text ------------------------------------------------ - javascriptGenerator.forBlock["ui_text"] = function (block) { + javascriptGenerator.forBlock['ui_text'] = function (block) { const textCode = - javascriptGenerator.valueToCode( - block, - "TEXT", - javascriptGenerator.ORDER_ATOMIC, - ) || '""'; + javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_ATOMIC) || '""'; const xCode = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC) || '0'; const yCode = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC) || '0'; const fontSizeCode = - javascriptGenerator.valueToCode( - block, - "FONT_SIZE", - javascriptGenerator.ORDER_ATOMIC, - ) || "24"; + javascriptGenerator.valueToCode(block, 'FONT_SIZE', javascriptGenerator.ORDER_ATOMIC) || '24'; const durationCode = - javascriptGenerator.valueToCode( - block, - "DURATION", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'DURATION', javascriptGenerator.ORDER_ATOMIC) || '0'; const colorCode = - javascriptGenerator.valueToCode( - block, - "COLOR", - javascriptGenerator.ORDER_ATOMIC, - ) || '""'; + javascriptGenerator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC) || '""'; const textBlockVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("TEXTBLOCK_VAR"), - Blockly.VARIABLE_CATEGORY_NAME, + block.getFieldValue('TEXTBLOCK_VAR'), + Blockly.VARIABLE_CATEGORY_NAME ); const safeTextArg = emitSafeTextArg(textCode); @@ -135,40 +87,28 @@ export function registerTextGenerators(javascriptGenerator) { }; // UI Button ---------------------------------------------- - javascriptGenerator.forBlock["ui_button"] = function (block) { + javascriptGenerator.forBlock['ui_button'] = function (block) { // Retrieve values from the block - const text = javascriptGenerator.valueToCode( - block, - "TEXT", - javascriptGenerator.ORDER_ATOMIC, - ); - const x = javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ); - const y = javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ); - const width = `"${block.getFieldValue("SIZE")}"`; // Fix: Use "SIZE" instead of "WIDTH" - const textSize = `"${block.getFieldValue("TEXT_SIZE")}"`; // Fix: Add text size support + const text = javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_ATOMIC); + const x = javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC); + const y = javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC); + const width = `"${block.getFieldValue('SIZE')}"`; // Fix: Use "SIZE" instead of "WIDTH" + const textSize = `"${block.getFieldValue('TEXT_SIZE')}"`; // Fix: Add text size support const textColor = javascriptGenerator.valueToCode( block, - "TEXT_COLOR", - javascriptGenerator.ORDER_ATOMIC, + 'TEXT_COLOR', + javascriptGenerator.ORDER_ATOMIC ); const backgroundColor = javascriptGenerator.valueToCode( block, - "BACKGROUND_COLOR", - javascriptGenerator.ORDER_ATOMIC, + 'BACKGROUND_COLOR', + javascriptGenerator.ORDER_ATOMIC ); // Get the button variable const buttonVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("BUTTON_VAR"), - Blockly.VARIABLE_CATEGORY_NAME, + block.getFieldValue('BUTTON_VAR'), + Blockly.VARIABLE_CATEGORY_NAME ); const buttonId = `Button_${generateUniqueId()}`; @@ -187,50 +127,31 @@ export function registerTextGenerators(javascriptGenerator) { }; // UI Input ----------------------------------------------- - javascriptGenerator.forBlock["ui_input"] = function (block) { + javascriptGenerator.forBlock['ui_input'] = function (block) { const varName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("INPUT_VAR"), - Blockly.VARIABLE_CATEGORY_NAME, + block.getFieldValue('INPUT_VAR'), + Blockly.VARIABLE_CATEGORY_NAME ); const textCode = - javascriptGenerator.valueToCode( - block, - "TEXT", - javascriptGenerator.ORDER_ATOMIC, - ) || '""'; + javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_ATOMIC) || '""'; const xCode = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC) || '0'; const yCode = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC) || '0'; const fontSizeCode = - javascriptGenerator.valueToCode( - block, - "TEXT_SIZE", - javascriptGenerator.ORDER_ATOMIC, - ) || "24"; + javascriptGenerator.valueToCode(block, 'TEXT_SIZE', javascriptGenerator.ORDER_ATOMIC) || '24'; const textColorCode = - javascriptGenerator.valueToCode( - block, - "TEXT_COLOR", - javascriptGenerator.ORDER_ATOMIC, - ) || '"#000000"'; + javascriptGenerator.valueToCode(block, 'TEXT_COLOR', javascriptGenerator.ORDER_ATOMIC) || + '"#000000"'; const backgroundColorCode = javascriptGenerator.valueToCode( block, - "BACKGROUND_COLOR", - javascriptGenerator.ORDER_ATOMIC, + 'BACKGROUND_COLOR', + javascriptGenerator.ORDER_ATOMIC ) || '"#ffffff"'; - const size = block.getFieldValue("SIZE") || "medium"; + const size = block.getFieldValue('SIZE') || 'medium'; const safeTextArg = emitSafeTextArg(textCode); @@ -247,55 +168,26 @@ export function registerTextGenerators(javascriptGenerator) { }; // UI Slider ---------------------------------------------- - javascriptGenerator.forBlock["ui_slider"] = function (block) { + javascriptGenerator.forBlock['ui_slider'] = function (block) { const varName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("SLIDER_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('SLIDER_VAR'), + Blockly.Names.NameType.VARIABLE ); - const min = - javascriptGenerator.valueToCode( - block, - "MIN", - javascriptGenerator.ORDER_NONE, - ) || 0; + const min = javascriptGenerator.valueToCode(block, 'MIN', javascriptGenerator.ORDER_NONE) || 0; const max = - javascriptGenerator.valueToCode( - block, - "MAX", - javascriptGenerator.ORDER_NONE, - ) || 100; + javascriptGenerator.valueToCode(block, 'MAX', javascriptGenerator.ORDER_NONE) || 100; const value = - javascriptGenerator.valueToCode( - block, - "VALUE", - javascriptGenerator.ORDER_NONE, - ) || 50; - const x = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_NONE, - ) || 100; - const y = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_NONE, - ) || 50; + javascriptGenerator.valueToCode(block, 'VALUE', javascriptGenerator.ORDER_NONE) || 50; + const x = javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_NONE) || 100; + const y = javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_NONE) || 50; const color = - javascriptGenerator.valueToCode( - block, - "COLOR", - javascriptGenerator.ORDER_NONE, - ) || '"#000000"'; + javascriptGenerator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_NONE) || + '"#000000"'; const background = - javascriptGenerator.valueToCode( - block, - "BACKGROUND", - javascriptGenerator.ORDER_NONE, - ) || '"#ffffff"'; - const size = `"${block.getFieldValue("SIZE") || "MEDIUM"}"`; + javascriptGenerator.valueToCode(block, 'BACKGROUND', javascriptGenerator.ORDER_NONE) || + '"#ffffff"'; + const size = `"${block.getFieldValue('SIZE') || 'MEDIUM'}"`; const id = `"${varName}_slider"`; const code = ` @@ -323,17 +215,13 @@ export function registerTextGenerators(javascriptGenerator) { // Uses Blockly default // Comment ------------------------------------------------ - javascriptGenerator.forBlock["comment"] = function (block) { + javascriptGenerator.forBlock['comment'] = function (block) { /** * comment block -> single-line JS comment. * Sanitizes the displayed text so it cannot break out of comment context. */ let raw = - javascriptGenerator.valueToCode( - block, - "COMMENT", - javascriptGenerator.ORDER_ATOMIC, - ) || "''"; + javascriptGenerator.valueToCode(block, 'COMMENT', javascriptGenerator.ORDER_ATOMIC) || "''"; const m = raw.match(/^(['"`])(.*)\1$/s); const content = m ? m[2] : raw; @@ -342,17 +230,13 @@ export function registerTextGenerators(javascriptGenerator) { return `// ${safe}\n`; }; // Describe ----------------------------------------------- - javascriptGenerator.forBlock["describe"] = function (block) { + javascriptGenerator.forBlock['describe'] = function (block) { const textCode = - javascriptGenerator.valueToCode( - block, - "TEXT", - javascriptGenerator.ORDER_ATOMIC, - ) || '""'; + javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_ATOMIC) || '""'; const meshVariable = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); const safeTextArg = emitSafeTextArg(textCode); @@ -361,38 +245,34 @@ export function registerTextGenerators(javascriptGenerator) { }; // Add 3D text -------------------------------------------- - javascriptGenerator.forBlock["create_3d_text"] = function (block) { - const { generatedName: variableName, userVariableName } = getVariableInfo( - block, - "ID_VAR", - ); + javascriptGenerator.forBlock['create_3d_text'] = function (block) { + const { generatedName: variableName, userVariableName } = getVariableInfo(block, 'ID_VAR'); - let rawText = getFieldValue(block, "TEXT", "Hello World"); - if (typeof rawText !== "string") rawText = String(rawText ?? ""); + let rawText = getFieldValue(block, 'TEXT', 'Hello World'); + if (typeof rawText !== 'string') rawText = String(rawText ?? ''); const m = rawText.match(/^(['"`])(.*)\1$/s); const textLiteral = JSON.stringify(sanitizeForCode(m ? m[2] : rawText)); - const fontKey = block.getFieldValue("FONT") || "__fonts_FreeSans_Bold_json"; - const size = getFieldValue(block, "SIZE", "50"); - const depth = getFieldValue(block, "DEPTH", "1.0"); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); - const color = getFieldValue(block, "COLOR", '"#FFFFFF"'); + const fontKey = block.getFieldValue('FONT') || '__fonts_FreeSans_Bold_json'; + const size = getFieldValue(block, 'SIZE', '50'); + const depth = getFieldValue(block, 'DEPTH', '1.0'); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); + const color = getFieldValue(block, 'COLOR', '"#FFFFFF"'); - let font = "./fonts/FreeSans_Bold.json"; - if (fontKey === "__fonts_FreeSans_Bold_json") - font = "./fonts/FreeSans_Bold.json"; + let font = './fonts/FreeSans_Bold.json'; + if (fontKey === '__fonts_FreeSans_Bold_json') font = './fonts/FreeSans_Bold.json'; const meshId = `${userVariableName}__${block.id}`; meshMap[block.id] = block; meshBlockIdMap[block.id] = block.id; - let doCode = ""; - if (block.getInput("DO")) { - doCode = javascriptGenerator.statementToCode(block, "DO") || ""; + let doCode = ''; + if (block.getInput('DO')) { + doCode = javascriptGenerator.statementToCode(block, 'DO') || ''; } - doCode = doCode ? `async function() {\n${doCode}\n}` : ""; + doCode = doCode ? `async function() {\n${doCode}\n}` : ''; return `${variableName} = create3DText({ text: ${textLiteral}, @@ -401,7 +281,7 @@ export function registerTextGenerators(javascriptGenerator) { size: ${size}, depth: ${depth}, position: { x: ${x}, y: ${y}, z: ${z} }, - modelId: '${meshId}'${doCode ? `,\n callback: ${doCode}` : ""} + modelId: '${meshId}'${doCode ? `,\n callback: ${doCode}` : ''} });\n`; }; diff --git a/generators/generators-transform.js b/generators/generators-transform.js index ae7de62a..39065db8 100644 --- a/generators/generators-transform.js +++ b/generators/generators-transform.js @@ -1,6 +1,6 @@ -import * as Blockly from "blockly"; -import { meshMap, meshBlockIdMap } from "./mesh-state.js"; -import { getFieldValue } from "./generators-utilities.js"; +import * as Blockly from 'blockly'; +import { meshMap, meshBlockIdMap } from './mesh-state.js'; +import { getFieldValue } from './generators-utilities.js'; export function registerTransformGenerators(javascriptGenerator) { // ------------------------------- @@ -8,221 +8,179 @@ export function registerTransformGenerators(javascriptGenerator) { // ------------------------------- // Change position of object by xyz coordinates - javascriptGenerator.forBlock["move_by_xyz"] = function (block) { + javascriptGenerator.forBlock['move_by_xyz'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("BLOCK_NAME"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('BLOCK_NAME'), + Blockly.Names.NameType.VARIABLE ); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); return `await moveByVector(${modelName}, { x: ${x}, y: ${y}, z: ${z} });\n`; }; // Change position of object by single xyz coordinate - javascriptGenerator.forBlock["move_by_xyz_single"] = function (block) { + javascriptGenerator.forBlock['move_by_xyz_single'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("BLOCK_NAME"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('BLOCK_NAME'), + Blockly.Names.NameType.VARIABLE ); - const coordinate = block.getFieldValue("COORDINATE") || "x_coordinate"; - const value = getFieldValue(block, "VALUE", "0"); + const coordinate = block.getFieldValue('COORDINATE') || 'x_coordinate'; + const value = getFieldValue(block, 'VALUE', '0'); switch (coordinate) { - case "x_coordinate": + case 'x_coordinate': return `await moveByVector(${modelName}, { x: ${value}, y: 0, z: 0 });\n`; - case "y_coordinate": + case 'y_coordinate': return `await moveByVector(${modelName}, { x: 0, y: ${value}, z: 0 });\n`; - case "z_coordinate": + case 'z_coordinate': return `await moveByVector(${modelName}, { x: 0, y: 0, z: ${value} });\n`; } }; // Set position of object to xyz coordinates - javascriptGenerator.forBlock["move_to_xyz"] = function (block) { + javascriptGenerator.forBlock['move_to_xyz'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL"), - Blockly.Names.NameType.VARIABLE, - ); - - const x = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const y = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const z = - javascriptGenerator.valueToCode( - block, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - - const useY = block.getFieldValue("USE_Y") === "TRUE"; + block.getFieldValue('MODEL'), + Blockly.Names.NameType.VARIABLE + ); + + const x = javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC) || '0'; + const y = javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC) || '0'; + const z = javascriptGenerator.valueToCode(block, 'Z', javascriptGenerator.ORDER_ATOMIC) || '0'; + + const useY = block.getFieldValue('USE_Y') === 'TRUE'; return `await positionAt(${meshName}, { x: ${x}, y: ${y}, z: ${z}, useY: ${useY} });\n`; }; // Set position of object to single xyz coordinate - javascriptGenerator.forBlock["move_to_xyz_single"] = function (block) { + javascriptGenerator.forBlock['move_to_xyz_single'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL'), + Blockly.Names.NameType.VARIABLE ); - const coordinate = block.getFieldValue("COORDINATE") || "x_coordinate"; - const value = getFieldValue(block, "VALUE", "0"); + const coordinate = block.getFieldValue('COORDINATE') || 'x_coordinate'; + const value = getFieldValue(block, 'VALUE', '0'); return `await positionAtSingleCoordinate(${meshName}, "${coordinate}", ${value});\n`; }; // Set position of object to another object's position - javascriptGenerator.forBlock["move_to"] = function (block) { + javascriptGenerator.forBlock['move_to'] = function (block) { const meshName1 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL1"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL1'), + Blockly.Names.NameType.VARIABLE ); const meshName2 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL2"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL2'), + Blockly.Names.NameType.VARIABLE ); - const useY = block.getFieldValue("USE_Y") === "TRUE"; + const useY = block.getFieldValue('USE_Y') === 'TRUE'; return `await moveTo(${meshName1}, { target: ${meshName2}, useY: ${useY} });\n`; }; // Rotate object BY xyz coordinates - javascriptGenerator.forBlock["rotate_model_xyz"] = function (block) { + javascriptGenerator.forBlock['rotate_model_xyz'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL'), + Blockly.Names.NameType.VARIABLE ); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); return `await rotate(${meshName}, { x: ${x}, y: ${y}, z: ${z} });\n`; }; // Rotate object TO specific xyz coordinates - javascriptGenerator.forBlock["rotate_to"] = function (block) { + javascriptGenerator.forBlock['rotate_to'] = function (block) { const meshName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL"), - Blockly.Names.NameType.VARIABLE, - ); - - const x = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const y = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const z = - javascriptGenerator.valueToCode( - block, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + block.getFieldValue('MODEL'), + Blockly.Names.NameType.VARIABLE + ); + + const x = javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC) || '0'; + const y = javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC) || '0'; + const z = javascriptGenerator.valueToCode(block, 'Z', javascriptGenerator.ORDER_ATOMIC) || '0'; return `await rotateTo(${meshName}, { x: ${x}, y: ${y}, z: ${z} });\n`; }; // Look object at another object - javascriptGenerator.forBlock["look_at"] = function (block) { + javascriptGenerator.forBlock['look_at'] = function (block) { const meshName1 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL1"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL1'), + Blockly.Names.NameType.VARIABLE ); const meshName2 = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL2"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL2'), + Blockly.Names.NameType.VARIABLE ); - const useY = block.getFieldValue("USE_Y") === "TRUE"; + const useY = block.getFieldValue('USE_Y') === 'TRUE'; return `await lookAt(${meshName1}, { target: ${meshName2}, useY: ${useY} });\n`; }; // Scale object by xyz coordinates - javascriptGenerator.forBlock["scale"] = function (block) { + javascriptGenerator.forBlock['scale'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("BLOCK_NAME"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('BLOCK_NAME'), + Blockly.Names.NameType.VARIABLE ); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); // Retrieve the origin values for x, y, and z axes - const xOrigin = block.getFieldValue("X_ORIGIN") || "'CENTRE'"; - const yOrigin = block.getFieldValue("Y_ORIGIN") || "'CENTRE'"; - const zOrigin = block.getFieldValue("Z_ORIGIN") || "'CENTRE'"; + const xOrigin = block.getFieldValue('X_ORIGIN') || "'CENTRE'"; + const yOrigin = block.getFieldValue('Y_ORIGIN') || "'CENTRE'"; + const zOrigin = block.getFieldValue('Z_ORIGIN') || "'CENTRE'"; return `await scale(${modelName}, { x: ${x}, y: ${y}, z: ${z}, xOrigin: '${xOrigin}', yOrigin: '${yOrigin}', zOrigin: '${zOrigin}' });\n`; }; // Resize object by xyz coordinates - javascriptGenerator.forBlock["resize"] = function (block) { + javascriptGenerator.forBlock['resize'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("BLOCK_NAME"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('BLOCK_NAME'), + Blockly.Names.NameType.VARIABLE ); - const x = getFieldValue(block, "X", "0"); - const y = getFieldValue(block, "Y", "0"); - const z = getFieldValue(block, "Z", "0"); + const x = getFieldValue(block, 'X', '0'); + const y = getFieldValue(block, 'Y', '0'); + const z = getFieldValue(block, 'Z', '0'); // Retrieve the origin values for x, y, and z axes - const xOrigin = block.getFieldValue("X_ORIGIN") || "'CENTRE'"; - const yOrigin = block.getFieldValue("Y_ORIGIN") || "'CENTRE'"; - const zOrigin = block.getFieldValue("Z_ORIGIN") || "'CENTRE'"; + const xOrigin = block.getFieldValue('X_ORIGIN') || "'CENTRE'"; + const yOrigin = block.getFieldValue('Y_ORIGIN') || "'CENTRE'"; + const zOrigin = block.getFieldValue('Z_ORIGIN') || "'CENTRE'"; return `await resize(${modelName}, { width: ${x}, height: ${y}, depth: ${z}, xOrigin: '${xOrigin}', yOrigin: '${yOrigin}', zOrigin: '${zOrigin}' });\n`; }; // Set anchor of object by xyz coordinates - javascriptGenerator.forBlock["set_pivot"] = function (block) { + javascriptGenerator.forBlock['set_pivot'] = function (block) { const meshVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH'), + Blockly.Names.NameType.VARIABLE ); const xPivot = - javascriptGenerator.valueToCode( - block, - "X_PIVOT", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(block, 'X_PIVOT', javascriptGenerator.ORDER_ATOMIC) || 0; const yPivot = - javascriptGenerator.valueToCode( - block, - "Y_PIVOT", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(block, 'Y_PIVOT', javascriptGenerator.ORDER_ATOMIC) || 0; const zPivot = - javascriptGenerator.valueToCode( - block, - "Z_PIVOT", - javascriptGenerator.ORDER_ATOMIC, - ) || 0; + javascriptGenerator.valueToCode(block, 'Z_PIVOT', javascriptGenerator.ORDER_ATOMIC) || 0; return `await setAnchor(${meshVar}, { xPivot: ${xPivot}, yPivot: ${yPivot}, zPivot: ${zPivot} });\n`; }; @@ -232,94 +190,78 @@ export function registerTransformGenerators(javascriptGenerator) { // ------------------------------- // Add physics to object - javascriptGenerator.forBlock["add_physics"] = function (block) { + javascriptGenerator.forBlock['add_physics'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); - const physicsType = block.getFieldValue("PHYSICS_TYPE"); + const physicsType = block.getFieldValue('PHYSICS_TYPE'); // Note: Ensure that the execution environment supports async/await at this level return `await setPhysics(${modelName}, "${physicsType}");\n`; }; // Add physics shape to object - javascriptGenerator.forBlock["add_physics_shape"] = function (block) { + javascriptGenerator.forBlock['add_physics_shape'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); - const shapeType = block.getFieldValue("SHAPE_TYPE"); + const shapeType = block.getFieldValue('SHAPE_TYPE'); // Note: Ensure that the execution environment supports async/await at this level return `await setPhysicsShape(${modelName}, "${shapeType}");\n`; }; // Apply force to object - javascriptGenerator.forBlock["apply_force"] = function (block) { + javascriptGenerator.forBlock['apply_force'] = function (block) { // Get the name of the mesh variable const mesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); // Get the force values const forceX = - javascriptGenerator.valueToCode( - block, - "X", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'X', javascriptGenerator.ORDER_ATOMIC) || '0'; const forceY = - javascriptGenerator.valueToCode( - block, - "Y", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Y', javascriptGenerator.ORDER_ATOMIC) || '0'; const forceZ = - javascriptGenerator.valueToCode( - block, - "Z", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Z', javascriptGenerator.ORDER_ATOMIC) || '0'; // Generate the code return `applyForce(${mesh}, { forceX: ${forceX}, forceY: ${forceY}, forceZ: ${forceZ} });\n`; }; // Show physics shapes - javascriptGenerator.forBlock["show_physics"] = function (block) { - const show = block.getFieldValue("SHOW") === "TRUE"; + javascriptGenerator.forBlock['show_physics'] = function (block) { + const show = block.getFieldValue('SHOW') === 'TRUE'; return `showPhysics(${show});\n`; }; // Move object forward - javascriptGenerator.forBlock["move_forward"] = function (block) { + javascriptGenerator.forBlock['move_forward'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL'), + Blockly.Names.NameType.VARIABLE ); const speed = - javascriptGenerator.valueToCode( - block, - "SPEED", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; - const direction = block.getFieldValue("DIRECTION"); + javascriptGenerator.valueToCode(block, 'SPEED', javascriptGenerator.ORDER_ATOMIC) || '0'; + const direction = block.getFieldValue('DIRECTION'); // Choose the appropriate helper function based on the direction let helperFunction; switch (direction) { - case "sideways": - helperFunction = "moveSideways"; + case 'sideways': + helperFunction = 'moveSideways'; break; - case "strafe": - helperFunction = "strafe"; + case 'strafe': + helperFunction = 'strafe'; break; default: - helperFunction = "moveForward"; + helperFunction = 'moveForward'; } return `${helperFunction}(${modelName}, ${speed});\n`; @@ -330,14 +272,14 @@ export function registerTransformGenerators(javascriptGenerator) { // ------------------------------- // Parent child - javascriptGenerator.forBlock["parent"] = function (block) { + javascriptGenerator.forBlock['parent'] = function (block) { const parentMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("PARENT_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('PARENT_MESH'), + Blockly.Names.NameType.VARIABLE ); const childMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("CHILD_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('CHILD_MESH'), + Blockly.Names.NameType.VARIABLE ); // Establish the parent-child relationship with offset @@ -345,79 +287,55 @@ export function registerTransformGenerators(javascriptGenerator) { }; // Parent child with offset - javascriptGenerator.forBlock["parent_child"] = function (block) { + javascriptGenerator.forBlock['parent_child'] = function (block) { const parentMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("PARENT_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('PARENT_MESH'), + Blockly.Names.NameType.VARIABLE ); const childMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("CHILD_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('CHILD_MESH'), + Blockly.Names.NameType.VARIABLE ); const xOffset = - javascriptGenerator.valueToCode( - block, - "X_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'X_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const yOffset = - javascriptGenerator.valueToCode( - block, - "Y_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Y_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const zOffset = - javascriptGenerator.valueToCode( - block, - "Z_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Z_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; // Establish the parent-child relationship with offset return `parentChild(${parentMesh}, ${childMesh}, ${xOffset}, ${yOffset}, ${zOffset});\n`; }; // Remove parent from object - javascriptGenerator.forBlock["remove_parent"] = function (block) { + javascriptGenerator.forBlock['remove_parent'] = function (block) { const childMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("CHILD_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('CHILD_MESH'), + Blockly.Names.NameType.VARIABLE ); return `removeParent(${childMesh});\n`; }; // Make follower follow target - javascriptGenerator.forBlock["follow"] = function (block) { + javascriptGenerator.forBlock['follow'] = function (block) { const followerMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("FOLLOWER_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('FOLLOWER_MESH'), + Blockly.Names.NameType.VARIABLE ); const targetMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("TARGET_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('TARGET_MESH'), + Blockly.Names.NameType.VARIABLE ); - const followPosition = block.getFieldValue("FOLLOW_POSITION"); + const followPosition = block.getFieldValue('FOLLOW_POSITION'); const xOffset = - javascriptGenerator.valueToCode( - block, - "X_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'X_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const yOffset = - javascriptGenerator.valueToCode( - block, - "Y_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Y_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const zOffset = - javascriptGenerator.valueToCode( - block, - "Z_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Z_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; // Use the helper method makeFollow for following the target const code = ` @@ -427,10 +345,10 @@ export function registerTransformGenerators(javascriptGenerator) { }; // Stop following object - javascriptGenerator.forBlock["stop_follow"] = function (block) { + javascriptGenerator.forBlock['stop_follow'] = function (block) { const followerModelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("FOLLOWER_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('FOLLOWER_MESH'), + Blockly.Names.NameType.VARIABLE ); // Generate code to call the stopFollow helper function @@ -439,44 +357,32 @@ export function registerTransformGenerators(javascriptGenerator) { }; // Attach object to target at hold - javascriptGenerator.forBlock["attach"] = function (block) { + javascriptGenerator.forBlock['attach'] = function (block) { const meshToAttach = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_TO_ATTACH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_TO_ATTACH'), + Blockly.Names.NameType.VARIABLE ); const targetMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("TARGET_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('TARGET_MESH'), + Blockly.Names.NameType.VARIABLE ); - const boneName = block.getFieldValue("BONE_NAME"); + const boneName = block.getFieldValue('BONE_NAME'); const xOffset = - javascriptGenerator.valueToCode( - block, - "X_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'X_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const yOffset = - javascriptGenerator.valueToCode( - block, - "Y_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Y_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; const zOffset = - javascriptGenerator.valueToCode( - block, - "Z_OFFSET", - javascriptGenerator.ORDER_ATOMIC, - ) || "0"; + javascriptGenerator.valueToCode(block, 'Z_OFFSET', javascriptGenerator.ORDER_ATOMIC) || '0'; // Establish the attach action with bone name and offset return `await attach(${meshToAttach}, ${targetMesh}, { boneName: "${boneName}", x: ${xOffset}, y: ${yOffset}, z: ${zOffset} }); `; }; // Drop object - javascriptGenerator.forBlock["drop"] = function (block) { + javascriptGenerator.forBlock['drop'] = function (block) { const meshToDetach = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_TO_DETACH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_TO_DETACH'), + Blockly.Names.NameType.VARIABLE ); // Establish the drop action @@ -489,18 +395,14 @@ export function registerTransformGenerators(javascriptGenerator) { // ------------------------------- // Add merged as merge list - javascriptGenerator.forBlock["merge_meshes"] = function (block) { + javascriptGenerator.forBlock['merge_meshes'] = function (block) { const resultVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("RESULT_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('RESULT_VAR'), + Blockly.Names.NameType.VARIABLE ); const meshList = - javascriptGenerator.valueToCode( - block, - "MESH_LIST", - javascriptGenerator.ORDER_ATOMIC, - ) || "[]"; + javascriptGenerator.valueToCode(block, 'MESH_LIST', javascriptGenerator.ORDER_ATOMIC) || '[]'; const meshId = `${resultVar}__${block.id}`; meshMap[meshId] = block; @@ -511,22 +413,18 @@ export function registerTransformGenerators(javascriptGenerator) { }; // Add subtracted as object subtract list - javascriptGenerator.forBlock["subtract_meshes"] = function (block) { + javascriptGenerator.forBlock['subtract_meshes'] = function (block) { const resultVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("RESULT_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('RESULT_VAR'), + Blockly.Names.NameType.VARIABLE ); const baseMesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("BASE_MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('BASE_MESH'), + Blockly.Names.NameType.VARIABLE ); const meshList = - javascriptGenerator.valueToCode( - block, - "MESH_LIST", - javascriptGenerator.ORDER_ATOMIC, - ) || "[]"; + javascriptGenerator.valueToCode(block, 'MESH_LIST', javascriptGenerator.ORDER_ATOMIC) || '[]'; const meshId = `${resultVar}__${block.id}`; meshMap[meshId] = block; @@ -537,18 +435,14 @@ export function registerTransformGenerators(javascriptGenerator) { }; // Add intersection as intersect list - javascriptGenerator.forBlock["intersection_meshes"] = function (block) { + javascriptGenerator.forBlock['intersection_meshes'] = function (block) { const resultVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("RESULT_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('RESULT_VAR'), + Blockly.Names.NameType.VARIABLE ); const meshList = - javascriptGenerator.valueToCode( - block, - "MESH_LIST", - javascriptGenerator.ORDER_ATOMIC, - ) || "[]"; + javascriptGenerator.valueToCode(block, 'MESH_LIST', javascriptGenerator.ORDER_ATOMIC) || '[]'; const meshId = `${resultVar}__${block.id}`; meshMap[meshId] = block; @@ -559,18 +453,14 @@ export function registerTransformGenerators(javascriptGenerator) { }; // Add hull as hull list - javascriptGenerator.forBlock["hull_meshes"] = function (block) { + javascriptGenerator.forBlock['hull_meshes'] = function (block) { const resultVar = javascriptGenerator.nameDB_.getName( - block.getFieldValue("RESULT_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('RESULT_VAR'), + Blockly.Names.NameType.VARIABLE ); const meshList = - javascriptGenerator.valueToCode( - block, - "MESH_LIST", - javascriptGenerator.ORDER_ATOMIC, - ) || "[]"; + javascriptGenerator.valueToCode(block, 'MESH_LIST', javascriptGenerator.ORDER_ATOMIC) || '[]'; const meshId = `${resultVar}__${block.id}`; meshMap[meshId] = block; @@ -582,8 +472,8 @@ export function registerTransformGenerators(javascriptGenerator) { // Used as an input inside set_pivot // (not a block in its own right) - javascriptGenerator.forBlock["min_centre_max"] = function (block) { - const pivotOption = block.getFieldValue("PIVOT_OPTION"); + javascriptGenerator.forBlock['min_centre_max'] = function (block) { + const pivotOption = block.getFieldValue('PIVOT_OPTION'); // Return the string value as a quoted literal return [`"${pivotOption}"`, javascriptGenerator.ORDER_ATOMIC]; diff --git a/generators/generators-utilities.js b/generators/generators-utilities.js index c94f05d9..e04c1959 100644 --- a/generators/generators-utilities.js +++ b/generators/generators-utilities.js @@ -1,56 +1,56 @@ -import * as Blockly from "blockly"; -import { javascriptGenerator } from "blockly/javascript"; -import { meshMap, meshBlockIdMap } from "./mesh-state.js"; +import * as Blockly from 'blockly'; +import { javascriptGenerator } from 'blockly/javascript'; +import { meshMap, meshBlockIdMap } from './mesh-state.js'; const RESERVED_IDENTIFIERS = new Set([ - "await", - "break", - "case", - "catch", - "class", - "const", - "continue", - "debugger", - "default", - "delete", - "do", - "else", - "enum", - "export", - "extends", - "false", - "finally", - "for", - "function", - "if", - "implements", - "import", - "in", - "instanceof", - "interface", - "let", - "new", - "null", - "package", - "private", - "protected", - "public", - "return", - "static", - "super", - "switch", - "this", - "throw", - "true", - "try", - "typeof", - "var", - "void", - "while", - "with", - "yield", - "arguments", - "eval", + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'implements', + 'import', + 'in', + 'instanceof', + 'interface', + 'let', + 'new', + 'null', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', + 'arguments', + 'eval', ]); // --------------------------------- // Utility functions for generators @@ -58,11 +58,8 @@ const RESERVED_IDENTIFIERS = new Set([ export function getFieldValue(block, fieldName, defaultValue) { return ( - javascriptGenerator.valueToCode( - block, - fieldName, - javascriptGenerator.ORDER_ATOMIC, - ) || defaultValue + javascriptGenerator.valueToCode(block, fieldName, javascriptGenerator.ORDER_ATOMIC) || + defaultValue ); } @@ -70,73 +67,66 @@ export function getVariableInfo(block, fieldName) { const variableId = block.getFieldValue(fieldName); const generatedName = javascriptGenerator.nameDB_.getName( variableId, - Blockly.Names.NameType.VARIABLE, + Blockly.Names.NameType.VARIABLE ); - const variableModel = block.workspace - ?.getVariableMap?.() - ?.getVariableById(variableId); + const variableModel = block.workspace?.getVariableMap?.()?.getVariableById(variableId); const userVariableName = variableModel?.name || generatedName; return { generatedName, userVariableName }; } export function getPositionTuple(block) { - const posX = getFieldValue(block, "X", "0"); - const posY = getFieldValue(block, "Y", "0"); - const posZ = getFieldValue(block, "Z", "0"); + const posX = getFieldValue(block, 'X', '0'); + const posY = getFieldValue(block, 'Y', '0'); + const posZ = getFieldValue(block, 'Z', '0'); return `[${posX}, ${posY}, ${posZ}]`; } export function createMesh(block, meshType, params) { - const { generatedName: variableName, userVariableName } = getVariableInfo( - block, - "ID_VAR", - ); + const { generatedName: variableName, userVariableName } = getVariableInfo(block, 'ID_VAR'); const meshId = `${userVariableName}__${block.id}`; meshMap[block.id] = block; meshBlockIdMap[block.id] = block.id; - const doCode = block.getInput("DO") - ? javascriptGenerator.statementToCode(block, "DO") || "" - : ""; + const doCode = block.getInput('DO') ? javascriptGenerator.statementToCode(block, 'DO') || '' : ''; const options = [...params]; - return `${variableName} = create${meshType}("${meshId}", { ${options.join(", ")} });\n${doCode}`; + return `${variableName} = create${meshType}("${meshId}", { ${options.join(', ')} });\n${doCode}`; } export function emitSafeIdentifierLiteral(code) { if (!code) { - return "undefined"; + return 'undefined'; } // Match single, double, or template quoted literals const m = code.match(/^(['"`])(.*)\1$/s); if (!m) { - return "undefined"; + return 'undefined'; } const rawBody = m[2]; // Reject escapes entirely - if (rawBody.includes("\\")) { - return "undefined"; + if (rawBody.includes('\\')) { + return 'undefined'; } // Replace spaces and other whitespace with underscores - const normalized = rawBody.replace(/\s+/g, "_"); + const normalized = rawBody.replace(/\s+/g, '_'); // Validate identifier if (!/^[A-Za-z$_][A-Za-z0-9$_]*$/.test(normalized)) { - return "undefined"; + return 'undefined'; } // Check reserved keywords if (RESERVED_IDENTIFIERS.has(normalized)) { - return "undefined"; + return 'undefined'; } return JSON.stringify(normalized); @@ -146,19 +136,19 @@ export function sanitizeForCode(input) { let s = String(input); // Cut from the first *real* newline (\r, \n, or Unicode line separator) - s = s.replace(/[\r\n\u2028\u2029].*$/s, ""); + s = s.replace(/[\r\n\u2028\u2029].*$/s, ''); // Cut from the first *escaped* newline sequence (\n, \r, \u2028, \u2029, \x0A, \x0D) - s = s.replace(/\\(?:n|r|u(?:2028|2029|000a|000d)|x0(?:a|d)).*$/i, ""); + s = s.replace(/\\(?:n|r|u(?:2028|2029|000a|000d)|x0(?:a|d)).*$/i, ''); // Remove any trailing backslashes that could remain (edge cases) - s = s.replace(/\\+$/, ""); + s = s.replace(/\\+$/, ''); // Neutralize comment and template literal markers - s = s.replace(/\*\//g, "*∕").replace(/\/\//g, "∕∕").replace(/`/g, "ˋ"); + s = s.replace(/\*\//g, '*∕').replace(/\/\//g, '∕∕').replace(/`/g, 'ˋ'); // Strip control characters (optional, keeps tabs/spaces) // eslint-disable-next-line no-control-regex - s = s.replace(/[\u0000-\u001F\u007F]/g, ""); + s = s.replace(/[\u0000-\u001F\u007F]/g, ''); return s; } @@ -176,10 +166,7 @@ export function emitSafeTextArg(code) { try { decoded = JSON.parse(q + body + q); } catch { - decoded = body - .replace(/\\"/g, '"') - .replace(/\\'/g, "'") - .replace(/\\\\/g, "\\"); + decoded = body.replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\\\/g, '\\'); } return JSON.stringify(sanitizeForCode(decoded)); diff --git a/generators/generators.js b/generators/generators.js index aafb57f3..f390763b 100644 --- a/generators/generators.js +++ b/generators/generators.js @@ -1,40 +1,38 @@ -import * as Blockly from "blockly"; -import { javascriptGenerator } from "blockly/javascript"; -import "@blockly/block-plus-minus"; -import { clearMeshMaps } from "./mesh-state.js"; +import * as Blockly from 'blockly'; +import { javascriptGenerator } from 'blockly/javascript'; +import '@blockly/block-plus-minus'; +import { clearMeshMaps } from './mesh-state.js'; // Import the generator registration functions for different categories of blocks -import { registerSceneGenerators } from "./generators-scene.js"; -import { registerEventsGenerators } from "./generators-events.js"; -import { registerTransformGenerators } from "./generators-transform.js"; -import { registerAnimateGenerators } from "./generators-animate.js"; -import { registerControlGenerators } from "./generators-control.js"; -import { registerConditionGenerators } from "./generators-condition.js"; -import { registerSensingGenerators } from "./generators-sensing.js"; -import { registerTextGenerators } from "./generators-text.js"; -import { registerMaterialGenerators } from "./generators-material.js"; -import { registerSoundGenerators } from "./generators-sound.js"; -import { registerDataGenerators } from "./generators-data.js"; -import { registerMathGenerators } from "./generators-math.js"; -import { registerFunctionsGenerators } from "./generators-functions.js"; +import { registerSceneGenerators } from './generators-scene.js'; +import { registerEventsGenerators } from './generators-events.js'; +import { registerTransformGenerators } from './generators-transform.js'; +import { registerAnimateGenerators } from './generators-animate.js'; +import { registerControlGenerators } from './generators-control.js'; +import { registerConditionGenerators } from './generators-condition.js'; +import { registerSensingGenerators } from './generators-sensing.js'; +import { registerTextGenerators } from './generators-text.js'; +import { registerMaterialGenerators } from './generators-material.js'; +import { registerSoundGenerators } from './generators-sound.js'; +import { registerDataGenerators } from './generators-data.js'; +import { registerMathGenerators } from './generators-math.js'; +import { registerFunctionsGenerators } from './generators-functions.js'; // import { registerDeprecatedGenerators } from "./generators-deprecated.js"; // Used outside of this file -export * from "./mesh-state.js"; +export * from './mesh-state.js'; // Set up all generators from external files export function defineGenerators() { // Allow Flock users to use "name" as a variable name - const reservedWordsWithoutName = javascriptGenerator.RESERVED_WORDS_.split( - ",", - ) + const reservedWordsWithoutName = javascriptGenerator.RESERVED_WORDS_.split(',') .map((word) => word.trim()) - .filter((word) => word && word !== "name") - .join(","); + .filter((word) => word && word !== 'name') + .join(','); // Force re-initialization of animation generators - delete javascriptGenerator.forBlock["play_animation"]; - delete javascriptGenerator.forBlock["switch_animation"]; + delete javascriptGenerator.forBlock['play_animation']; + delete javascriptGenerator.forBlock['switch_animation']; // Register generators for each category of blocks registerSceneGenerators(javascriptGenerator); @@ -74,8 +72,8 @@ export function defineGenerators() { defvars.push( javascriptGenerator.nameDB_.getName( devVarList[i], - Blockly.Names.NameType.DEVELOPER_VARIABLE, - ), + Blockly.Names.NameType.DEVELOPER_VARIABLE + ) ); } @@ -85,7 +83,7 @@ export function defineGenerators() { const variableModel = variables[i]; const generatedName = javascriptGenerator.nameDB_.getName( variableModel.getId(), - Blockly.Names.NameType.VARIABLE, + Blockly.Names.NameType.VARIABLE ); defvars.push(generatedName); userVariableDefaults.set(generatedName, variableModel.name); @@ -94,13 +92,11 @@ export function defineGenerators() { // Declare all of the variables. if (defvars.length) { let defvarsmesh = defvars.map(function (name) { - const initialValue = userVariableDefaults.has(name) - ? userVariableDefaults.get(name) - : name; + const initialValue = userVariableDefaults.has(name) ? userVariableDefaults.get(name) : name; return `let ${name} = ${JSON.stringify(initialValue)};`; }); - javascriptGenerator.definitions_["variables"] = - `// Made with Flock XR\n` + defvarsmesh.join(" ") + "\n"; + javascriptGenerator.definitions_['variables'] = + `// Made with Flock XR\n` + defvarsmesh.join(' ') + '\n'; } javascriptGenerator.isInitialized = true; diff --git a/generators/mesh-state.js b/generators/mesh-state.js index b22c4e64..2a3d7d9c 100644 --- a/generators/mesh-state.js +++ b/generators/mesh-state.js @@ -29,10 +29,7 @@ const _meshBlockIdMap = Object.create(null); // ---------------------------------------------------- export const meshMap = makeTrackedMap(_meshMap, blockKeyByBlock); -export const meshBlockIdMap = makeTrackedMap( - _meshBlockIdMap, - blockKeyByBlockId, -); +export const meshBlockIdMap = makeTrackedMap(_meshBlockIdMap, blockKeyByBlockId); export function clearMeshMaps() { for (const key of Object.keys(_meshMap)) delete meshMap[key]; @@ -41,7 +38,7 @@ export function clearMeshMaps() { let uniqueIdCounter = 0; -export function generateUniqueId(prefix = "") { +export function generateUniqueId(prefix = '') { // Increment the counter for each call uniqueIdCounter++; // Return a string with the prefix and the counter value diff --git a/index.html b/index.html index 9048220b..6044f28c 100644 --- a/index.html +++ b/index.html @@ -31,19 +31,16 @@ - + @@ -134,9 +131,7 @@ font-size: 18px; font-weight: 500; margin: 0; - font-family: - "Atkinson Hyperlegible Next", "Asap", Helvetica, Arial, Lucida, - sans-serif; + font-family: 'Atkinson Hyperlegible Next', 'Asap', Helvetica, Arial, Lucida, sans-serif; order: 4; } @@ -180,49 +175,47 @@ - -