diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 92b1294b7..91f1a7727 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -13,7 +13,7 @@ the script lives outside the repo). ## Launch - Chromium needs `headless: false` plus args `--no-sandbox - --disable-setuid-sandbox --headless=old`. This is not a contradiction: +--disable-setuid-sandbox --headless=old`. This is not a contradiction: `headless: false` stops Playwright adding its own headless flags (its headless mode uses the new headless shell, which drops WebGL), and `--headless=old` then puts Chromium itself in legacy headless mode, @@ -53,8 +53,10 @@ the script lives outside the repo). flock.js listens on the top document. Stub and dispatch: ```js -Object.defineProperty(document, "visibilityState", - { configurable: true, get: () => document.__fakeVis || "visible" }); -document.__fakeVis = "hidden"; // or "visible" -document.dispatchEvent(new Event("visibilitychange")); +Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: () => document.__fakeVis || 'visible', +}); +document.__fakeVis = 'hidden'; // or "visible" +document.dispatchEvent(new Event('visibilitychange')); ``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 55fa77646..d153ba570 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 8eec841f2..a080037a4 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 f63ff2c64..7b3d7d3b8 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 27bcd98e7..5c5531023 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 348289d76..d4dfef157 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 88cdeb7cc..d21095d55 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 b0582359d..42e62ce59 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' }); } }); ``` @@ -638,7 +638,7 @@ Shows a 5ร—5 image on a micro:bit's LED display. The micro:bit must be connected ```javascript // A sun on every plugged-in micro:bit -microbitShowImage("", "9090909990999990999090909"); +microbitShowImage('', '9090909990999990999090909'); ``` ## Examples @@ -648,19 +648,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); ``` @@ -668,14 +668,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' }); } }); ``` @@ -684,23 +684,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 d250fcfdf..bcb6d35ea 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 5fa2d8995..607bfa3ea 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 a05d6d421..eb825a43d 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,36 @@ 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 - Create [spatial computing](https://www.techagekids.com/2026/06/spatial-computing-skills-children.html) projects that link the digital and physical worlds -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/). @@ -53,9 +54,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. @@ -75,4 +76,5 @@ Flock XR is based on Blockly, the Babylon JS library and the Havok physics engin This project is tested with BrowserStack. **๐Ÿ“š Documentation:** + - [Getting Started](docs/GETTING_STARTED.md) - Quick start for improving API quality diff --git a/TRADEMARK.md b/TRADEMARK.md index 166257ddc..9c0dfd6ef 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 8bf0f4d20..14789f33b 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 2b05a1179..6fdbb9e13 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 @@ -41,18 +40,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); } @@ -60,17 +59,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); } @@ -78,14 +77,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; @@ -100,7 +99,7 @@ export function announceObjectSay(text, options = {}) { const region = createSayLiveRegion(); const mySeq = ++sayAnnounceSeq; - region.textContent = ""; + region.textContent = ''; setTimeout(() => { if (mySeq !== sayAnnounceSeq) return; region.textContent = spoken; @@ -111,7 +110,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(); @@ -128,10 +127,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; }); @@ -139,15 +139,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; @@ -156,41 +158,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) { @@ -198,7 +195,7 @@ function getEntityRoot(mesh) { let lastValid = mesh; while (node) { - const n = node.name || ""; + const n = node.name || ''; if (!looksLikeInternalMeshName(n)) { lastValid = node; } @@ -225,23 +222,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; } @@ -249,40 +246,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) { @@ -296,8 +288,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; } @@ -317,9 +309,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; } @@ -344,43 +336,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; } @@ -396,7 +386,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 { @@ -409,40 +399,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') ); } @@ -466,7 +456,6 @@ function getInteractionHint(mesh) { return String(hint).trim(); } } - } function closestBoundingBoxDistance(mesh, point) { @@ -477,7 +466,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; @@ -489,9 +480,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; } @@ -502,8 +498,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; @@ -519,7 +518,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 { @@ -533,21 +532,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); } } @@ -563,17 +557,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; } @@ -596,16 +590,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) { @@ -638,19 +632,19 @@ 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 { /* no bounding info; fall through to representative position */ } 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 }, }; @@ -730,9 +724,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; @@ -766,7 +758,7 @@ function getSceneObjects(scene, options = {}) { mesh.actionManager || root?.metadata?.interactive || root?.metadata?.clickable || - interactionHint, + interactionHint ); const textLabels = collectNearbyTextForObject(scene, p, root); @@ -798,21 +790,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}`; } @@ -820,13 +809,13 @@ function objectToSentence( } 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(); @@ -840,12 +829,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; } @@ -859,11 +848,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 = []; @@ -872,20 +860,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]}.`; @@ -895,18 +883,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)) { @@ -938,21 +930,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) { @@ -965,23 +961,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}.`; @@ -993,15 +987,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); } @@ -1014,15 +1010,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; }) @@ -1035,23 +1033,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 }) @@ -1059,7 +1055,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]; @@ -1070,15 +1066,16 @@ 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, fwdZ = 1; + let fwdX = 0, + fwdZ = 1; try { const dir = camera.getForwardRay?.(1)?.direction; if (dir && Number.isFinite(dir.x) && Number.isFinite(dir.y) && Number.isFinite(dir.z)) { @@ -1086,13 +1083,15 @@ export function describeFacingObject(scene) { fwdX = dir.x / 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. @@ -1178,28 +1177,25 @@ 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)}.`; } 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}`; } @@ -1207,7 +1203,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; @@ -1215,17 +1211,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; @@ -1233,7 +1223,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}`; @@ -1250,9 +1240,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); @@ -1260,7 +1248,7 @@ function announceMeshAction(mesh, actionWord, options = {}) { } if (textLabels.length) { - announce(`${label}. ${textLabels.join(". ")}`, options); + announce(`${label}. ${textLabels.join('. ')}`, options); return; } @@ -1273,14 +1261,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; @@ -1295,23 +1283,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 } @@ -1320,11 +1306,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; @@ -1336,15 +1324,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) { @@ -1352,7 +1344,7 @@ function getCachedPromptTextForMesh(mesh) { if (hit) return hit; } - return ""; + return ''; } export function announceSayText(text, options = {}) { @@ -1363,8 +1355,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; } @@ -1422,10 +1414,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); @@ -1442,10 +1434,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 }); } } @@ -1456,9 +1448,9 @@ export function enableSceneDescription(scene, inputManager) { createLiveRegion(); // Reset per-world state - lastInteractionKey = ""; + lastInteractionKey = ''; lastInteractionTime = 0; - lastAnnouncedText = ""; + lastAnnouncedText = ''; lastAnnouncedAt = 0; announceSeq += 1; @@ -1474,43 +1466,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 }); @@ -1521,9 +1512,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 }); }); } } @@ -1532,6 +1523,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/uiA11y.js b/accessibility/uiA11y.js index 601b97171..74f8f4cc8 100644 --- a/accessibility/uiA11y.js +++ b/accessibility/uiA11y.js @@ -32,8 +32,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; } @@ -52,9 +51,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 @@ -89,12 +92,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, }; } @@ -121,9 +124,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'; } @@ -168,7 +171,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 */ + } }, }); } @@ -208,15 +215,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); @@ -233,9 +255,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({}); @@ -251,12 +271,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;` + @@ -288,8 +307,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(); } }; @@ -319,7 +342,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 */ + } }, }); } @@ -362,9 +389,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 decad4ae5..65cd5af7c 100644 --- a/api/animate.js +++ b/api/animate.js @@ -393,7 +393,13 @@ export const flockAnimate = { // interrupted final frame) can't leave the mesh slightly off: the target for a // one-way glide, or back at the start for a reverse (there-and-back) glide. if (!loop) mesh.position = (reverse ? startPosition : 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 d675fa884..ff3b8ae8a 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 }); @@ -188,13 +182,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 }); @@ -205,7 +199,7 @@ export const flockCamera = { flock.BABYLON.Vector3.Zero(), new flock.BABYLON.Quaternion(0, 0, 0, 1), flock.BABYLON.Vector3.One(), - scene, + scene ); } } @@ -225,7 +219,7 @@ export const flockCamera = { constraintBox.physics.disablePreStep = false; constraintBox.physics.setTargetTransform( constraintBox.position, - constraintBox.rotationQuaternion, + constraintBox.rotationQuaternion ); } @@ -249,7 +243,7 @@ export const flockCamera = { maxLimit: 0, }, ], - scene, + scene ); try { @@ -257,37 +251,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 = { @@ -295,17 +279,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; } @@ -330,47 +314,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; } @@ -385,22 +369,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 bfc18e14b..bdce69cbc 100644 --- a/api/control.js +++ b/api/control.js @@ -22,22 +22,22 @@ export const flockControl = { return; } const timeoutId = setTimeout(() => { - signal?.removeEventListener("abort", onAbort); + signal?.removeEventListener('abort', onAbort); resolve(); }, ms); const onAbort = () => { clearTimeout(timeoutId); - signal?.removeEventListener("abort", onAbort); + signal?.removeEventListener('abort', onAbort); reject(flock.makeAbortError()); }; - signal?.addEventListener("abort", onAbort); + signal?.addEventListener('abort', onAbort); }); }, makeAbortError() { - const err = new Error("Run stopped"); - err.name = "AbortError"; + const err = new Error('Run stopped'); + err.name = 'AbortError'; return err; }, async safeLoop( @@ -45,7 +45,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; @@ -64,8 +64,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; @@ -95,12 +95,12 @@ export const flockControl = { const observer = flock.scene.onBeforeRenderObservable.add(checkCondition); signal?.addEventListener( - "abort", + 'abort', () => { flock.scene?.onBeforeRenderObservable?.remove(observer); reject(flock.makeAbortError()); }, - { once: true }, + { once: true } ); }); }, diff --git a/api/csg.js b/api/csg.js index 68334f4b0..af658de26 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); - const isDefaultMaterial = (material) => { - return ( - material instanceof flock.BABYLON.StandardMaterial && - material.name === "default material" + 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' + ); + }; + + 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,150 +717,119 @@ 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) { - flock.reportBlockError({ - key: "csg_empty_result", - api: "subtractMeshesMerge", - values: { - hint: "CSG requires watertight (manifold) geometry; 3D text and merged meshes are typically non-manifold.", - }, - error: e, - }); + console.warn(`[subtractMeshesMerge] Subtraction ${idx} failed:`, e.message); + } + }); - // Clean up any empty meshes - flock.scene.meshes - .filter( - (m) => m.name === "resultMesh" && m.getTotalVertices() === 0, - ) - .forEach((m) => m.dispose()); + let resultMesh; + try { + resultMesh = outerCSG.toMesh('resultMesh', scene, { + centerMesh: false, + }); - baseDuplicate.dispose(); - subtractDuplicates.forEach((m) => m.dispose()); - return resolve(null); + if (!resultMesh || resultMesh.getTotalVertices() === 0) { + throw new Error('CSG produced empty mesh'); } - - 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, + } catch (e) { + flock.reportBlockError({ + key: 'csg_empty_result', + api: 'subtractMeshesMerge', + values: { + hint: 'CSG requires watertight (manifold) geometry; 3D text and merged meshes are typically non-manifold.', }, - ); - if ( - shouldApplyBoxProjection(resultMesh, { - ...options, - uvProjection: inferredUvProjection, - }) - ) { - applyBoxProjectionUV(resultMesh, options.uvScale); - } + error: e, + }); + + // 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) => { @@ -945,12 +838,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]); } @@ -962,408 +850,323 @@ 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) { - flock.reportBlockError({ - key: "csg_empty_result", - api: "subtractMeshesIndividual", - values: { - hint: "CSG requires watertight (manifold) geometry; 3D text and merged meshes are typically non-manifold.", - }, - error: e, - }); + console.warn(e); + } + }); - // Clean up any empty meshes - flock.scene.meshes - .filter( - (m) => m.name === "resultMesh" && m.getTotalVertices() === 0, - ) - .forEach((m) => m.dispose()); + let resultMesh; + try { + resultMesh = outerCSG.toMesh('resultMesh', scene, { + centerMesh: false, + }); - baseDuplicate.dispose(); - allToolParts.forEach((t) => t.dispose()); - return resolve(null); + if (!resultMesh || resultMesh.getTotalVertices() === 0) { + throw new Error('CSG produced empty mesh'); } - - 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, + } catch (e) { + flock.reportBlockError({ + key: 'csg_empty_result', + api: 'subtractMeshesIndividual', + values: { + hint: 'CSG requires watertight (manifold) geometry; 3D text and merged meshes are typically non-manifold.', }, - ); - if ( - shouldApplyBoxProjection(resultMesh, { - ...options, - uvProjection: inferredUvProjection, - }) - ) { - applyBoxProjectionUV(resultMesh, options.uvScale); - } + error: e, + }); + + // 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) { + flock.reportBlockError({ + key: 'csg_empty_result', + api: 'intersectMeshes', + values: { + hint: 'CSG requires watertight (manifold) geometry.', + }, + error: e, + }); - // 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) { - flock.reportBlockError({ - key: "csg_empty_result", - api: "intersectMeshes", - values: { - hint: "CSG requires watertight (manifold) geometry.", - }, - error: e, - }); - - // 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(); @@ -1390,7 +1193,7 @@ export const flockCSG = { } }); }); - }), + }) ).then((meshes) => meshes.filter((mesh) => mesh !== null)); }, applyResultMeshProperties( @@ -1398,10 +1201,7 @@ export const flockCSG = { referenceMesh, modelId, blockId, - { - forceReferenceMaterial = false, - flattenNonReferenceSubMaterials = false, - } = {}, + { forceReferenceMaterial = false, flattenNonReferenceSubMaterials = false } = {} ) { // Copy transformation properties referenceMesh.material.backFaceCulling = false; @@ -1413,35 +1213,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(); @@ -1456,21 +1248,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; } @@ -1479,26 +1269,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 3bbd276ab..d528d1a64 100644 --- a/api/effects.js +++ b/api/effects.js @@ -10,30 +10,23 @@ 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__'; }, enableShadows({ enabled = true } = {}) { if (!flock.shadowLight) { - console.warn( - "Shadow light is not defined. Please ensure flock.shadowLight exists.", - ); + console.warn('Shadow light is not defined. Please ensure flock.shadowLight exists.'); return; } if (enabled) { @@ -67,8 +60,7 @@ export const flockEffects = { return new Promise((resolve) => { flock.whenModelReady(meshName, (mesh) => { const meshes = [mesh, ...mesh.getDescendants()].filter( - (m) => - m instanceof flock.BABYLON.Mesh && m.getTotalVertices() > 0, + (m) => m instanceof flock.BABYLON.Mesh && m.getTotalVertices() > 0 ); // Marking a caster does NOT turn shadows on โ€” enableShadows is the // master switch. Track the intent and apply it live if a generator @@ -99,7 +91,7 @@ export const flockEffects = { gravity, direction, rotation, - } = {}, + } = {} ) { const resultName = flock._reserveName(name); @@ -107,8 +99,8 @@ export const flockEffects = { flock.whenModelReady(emitterMesh, (meshInstance) => { if (!(meshInstance instanceof flock.BABYLON.AbstractMesh)) { flock.reportBlockError({ - key: "particle_emitter_not_mesh", - api: "createParticleEffect", + key: 'particle_emitter_not_mesh', + api: 'createParticleEffect', values: { effect: resultName, emitter: emitterMesh }, }); resolve(null); @@ -127,21 +119,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; @@ -152,19 +135,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; @@ -180,38 +163,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; } } @@ -219,8 +195,8 @@ export const flockEffects = { resolve(particleSystem); } catch (error) { flock.reportBlockError({ - key: "particle_effect_failed", - api: "createParticleEffect", + key: 'particle_effect_failed', + api: 'createParticleEffect', values: { effect: resultName }, error, }); @@ -233,9 +209,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 { @@ -243,9 +217,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(); @@ -254,37 +226,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 36d6f5e3b..eb373bee7 100644 --- a/api/events.js +++ b/api/events.js @@ -1,4 +1,4 @@ -import { getMicrobitManager } from "../microbit/manager.js"; +import { getMicrobitManager } from '../microbit/manager.js'; let flock; @@ -12,15 +12,15 @@ export const flockEvents = { */ onEvent(eventName, handler, once = false) { - if (typeof handler !== "function") { - flock.reportBlockError({ key: "invalid_callback", api: "onEvent" }); + if (typeof handler !== 'function') { + flock.reportBlockError({ key: 'invalid_callback', api: 'onEvent' }); return; } eventName = flock.sanitizeEventName(eventName); if (!flock.isAllowedEventName(eventName)) { flock.reportBlockError({ - key: "event_name_reserved", - api: "onEvent", + key: 'event_name_reserved', + api: 'onEvent', values: { event: eventName }, }); return; @@ -43,19 +43,19 @@ 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)) { flock.reportBlockError({ - key: "event_name_reserved", - api: "broadcastEvent", + key: 'event_name_reserved', + api: 'broadcastEvent', values: { event: eventName }, }); return; @@ -65,8 +65,8 @@ export const flockEvents = { } }, whenActionEvent(action, callback, isReleased = false) { - if (typeof callback !== "function") { - flock.reportBlockError({ key: "invalid_callback", api: "whenActionEvent" }); + if (typeof callback !== 'function') { + flock.reportBlockError({ key: 'invalid_callback', api: 'whenActionEvent' }); return; } const signal = flock.abortController?.signal; @@ -79,7 +79,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 { @@ -90,29 +90,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") { - flock.reportBlockError({ key: "invalid_callback", api: "whenKeyEvent" }); + if (typeof callback !== 'function') { + flock.reportBlockError({ key: 'invalid_callback', api: 'whenKeyEvent' }); 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 { @@ -123,18 +125,18 @@ 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 } ); } }, onMicrobitEvent(variableName, eventChar, callback) { - if (typeof callback !== "function") { - flock.reportBlockError({ key: "invalid_callback", api: "onMicrobitEvent" }); + if (typeof callback !== 'function') { + flock.reportBlockError({ key: 'invalid_callback', api: 'onMicrobitEvent' }); return; } const signal = flock.abortController?.signal; @@ -145,7 +147,7 @@ export const flockEvents = { const unsubscribe = getMicrobitManager().subscribe(variableName, (char) => { if (char === eventChar) callback(); }); - signal?.addEventListener("abort", unsubscribe, { once: true }); + signal?.addEventListener('abort', unsubscribe, { once: true }); }, start(action) { flock.scene.onBeforeRenderObservable.addOnce(action); @@ -157,7 +159,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 } @@ -174,8 +176,8 @@ export const flockEvents = { await action(); } catch (error) { flock.reportBlockError({ - key: "forever_block_failed", - api: "forever", + key: 'forever_block_failed', + api: 'forever', error, }); } finally { @@ -199,7 +201,7 @@ export const flockEvents = { flock.scene.onDisposeObservable.addOnce(disposeHandler); }, isAllowedEventName(eventName) { - if (!eventName || typeof eventName !== "string") { + if (!eventName || typeof eventName !== 'string') { return false; } @@ -208,14 +210,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; } @@ -228,15 +223,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 19c83a227..0807d2b84 100644 --- a/api/material.js +++ b/api/material.js @@ -57,8 +57,7 @@ export const flockMaterial = { // Babylon's Color3.fromHexString only accepts #RRGGBB, so expand the // #RGB shorthand (e.g. #f0c -> #ff00cc) rather than passing it through. - const expandShortHex = (hex) => - hex.length === 3 ? hex.replace(/(.)/g, '$1$1') : hex; + const expandShortHex = (hex) => (hex.length === 3 ? hex.replace(/(.)/g, '$1$1') : hex); if (/^([0-9A-F]{3}|[0-9A-F]{6})$/i.test(colourString)) { return `#${expandShortHex(colourString).toLowerCase()}`; diff --git a/api/mesh.js b/api/mesh.js index 6ffa5316e..2d33d98cb 100644 --- a/api/mesh.js +++ b/api/mesh.js @@ -654,8 +654,7 @@ export const flockMesh = { // double-tile them). Returns true if the mesh was a recognised primitive. retilePrimitiveUVs(mesh, { width, height, depth }, scale = null, texturePhysicalSize = 4) { if (!mesh) return false; - const shape = - mesh.metadata?.shapeType || (mesh.metadata?.shape === 'plane' ? 'Plane' : null); + const shape = mesh.metadata?.shapeType || (mesh.metadata?.shape === 'plane' ? 'Plane' : null); switch (shape) { case 'Box': flock.setSizeBasedBoxUVs(mesh, width, height, depth, texturePhysicalSize, scale); diff --git a/api/microbit.js b/api/microbit.js index a7ab86145..aa7462f4a 100644 --- a/api/microbit.js +++ b/api/microbit.js @@ -1,4 +1,4 @@ -import { getMicrobitManager } from "../microbit/manager.js"; +import { getMicrobitManager } from '../microbit/manager.js'; // Kept for consistency with the other api modules; addMicrobit itself only // needs the manager. @@ -20,19 +20,18 @@ export const flockMicrobit = { * tethers. */ addMicrobit(variableName, channel = 1) { - if (typeof variableName !== "string" || variableName === "") { - console.warn("addMicrobit: variableName must be a non-empty string"); + if (typeof variableName !== 'string' || variableName === '') { + console.warn('addMicrobit: variableName must be a non-empty string'); return; } - const channelValue = - typeof channel === "string" ? Number(channel) : channel; + const channelValue = typeof channel === 'string' ? Number(channel) : channel; if ( - typeof channelValue !== "number" || + typeof channelValue !== 'number' || !Number.isSafeInteger(channelValue) || channelValue < 0 || channelValue > 255 ) { - console.warn("addMicrobit: channel must be an integer between 0 and 255"); + console.warn('addMicrobit: channel must be an integer between 0 and 255'); return; } getMicrobitManager().setVariableChannel(variableName, channelValue, { @@ -47,24 +46,24 @@ export const flockMicrobit = { * are a silent no-op โ€” images can only be sent over USB. */ microbitShowImage(deviceName, pattern) { - if (typeof deviceName !== "string") { - console.warn("microbitShowImage: deviceName must be a string"); + if (typeof deviceName !== 'string') { + console.warn('microbitShowImage: deviceName must be a string'); return; } - if (typeof pattern !== "string") { - console.warn("microbitShowImage: pattern must be a string"); + if (typeof pattern !== 'string') { + console.warn('microbitShowImage: pattern must be a string'); return; } const manager = getMicrobitManager(); - if (deviceName !== "" && !manager.isMicrobitVariable(deviceName)) { + if (deviceName !== '' && !manager.isMicrobitVariable(deviceName)) { _flock?.reportBlockError?.({ - key: "not_a_microbit", - api: "microbitShowImage", + key: 'not_a_microbit', + api: 'microbitShowImage', values: { object: deviceName }, }); return; } - manager.showImage(deviceName === "" ? null : deviceName, pattern); + manager.showImage(deviceName === '' ? null : deviceName, pattern); }, /** @@ -75,23 +74,23 @@ export const flockMicrobit = { * are a silent no-op โ€” text can only be sent over USB. */ microbitScrollText(deviceName, text) { - if (typeof deviceName !== "string") { - console.warn("microbitScrollText: deviceName must be a string"); + if (typeof deviceName !== 'string') { + console.warn('microbitScrollText: deviceName must be a string'); return; } - if (typeof text !== "string" && typeof text !== "number") { - console.warn("microbitScrollText: text must be a string"); + if (typeof text !== 'string' && typeof text !== 'number') { + console.warn('microbitScrollText: text must be a string'); return; } const manager = getMicrobitManager(); - if (deviceName !== "" && !manager.isMicrobitVariable(deviceName)) { + if (deviceName !== '' && !manager.isMicrobitVariable(deviceName)) { _flock?.reportBlockError?.({ - key: "not_a_microbit", - api: "microbitScrollText", + key: 'not_a_microbit', + api: 'microbitScrollText', values: { object: deviceName }, }); return; } - manager.scrollText(deviceName === "" ? null : deviceName, String(text)); + manager.scrollText(deviceName === '' ? null : deviceName, String(text)); }, }; diff --git a/api/models.js b/api/models.js index 5f939f78a..0d88c77a4 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 32eacda64..be8d3ab21 100644 --- a/api/movement.js +++ b/api/movement.js @@ -40,9 +40,7 @@ function sceneGravityMagnitude() { } function nowMs() { - return typeof performance !== 'undefined' && performance.now - ? performance.now() - : Date.now(); + return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); } export const flockMovement = { diff --git a/api/physics.js b/api/physics.js index cd5853a00..bf773c605 100644 --- a/api/physics.js +++ b/api/physics.js @@ -793,8 +793,8 @@ export const flockPhysics = { currentIndex = (currentIndex + 1) % callbacks.length; } catch (e) { flock.reportBlockError({ - key: "trigger_block_failed", - api: "onTrigger", + key: 'trigger_block_failed', + api: 'onTrigger', error: e, }); } finally { diff --git a/api/sensing.js b/api/sensing.js index 2db6b410d..09729f868 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,96 +68,81 @@ 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; } // Speed per world axis, and overall speed. 0 with no live physics body. - case "SPEED_X": - case "SPEED_Y": - case "SPEED_Z": - case "SPEED": { + case 'SPEED_X': + case 'SPEED_Y': + case 'SPEED_Z': + case 'SPEED': { const body = mesh.physics; - const v = - body && body._pluginData?.hpBodyId - ? body.getLinearVelocity() - : null; + const v = body && body._pluginData?.hpBodyId ? body.getLinearVelocity() : null; if (!v) { propertyValue = 0; - } else if (propertyName === "SPEED_X") { + } else if (propertyName === 'SPEED_X') { propertyValue = parseFloat(v.x.toFixed(2)); - } else if (propertyName === "SPEED_Y") { + } else if (propertyName === 'SPEED_Y') { propertyValue = parseFloat(v.y.toFixed(2)); - } else if (propertyName === "SPEED_Z") { + } else if (propertyName === 'SPEED_Z') { propertyValue = parseFloat(v.z.toFixed(2)); } else { propertyValue = parseFloat(v.length().toFixed(2)); @@ -171,48 +152,48 @@ export const flockSensing = { // 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(); @@ -220,11 +201,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); @@ -233,7 +214,7 @@ export const flockSensing = { } break; - case "COLOUR": + case 'COLOUR': allMeshes = [mesh].concat(mesh.getDescendants?.() ?? []); materialNodes = allMeshes.filter((node) => node.material); @@ -251,17 +232,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; @@ -273,14 +251,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) { @@ -297,11 +275,11 @@ export const flockSensing = { }, convertTime(ms, unit) { switch (unit) { - case "milliseconds": + case 'milliseconds': return ms; - case "minutes": + case 'minutes': return Math.floor(ms / 60000); - case "seconds": + case 'seconds': default: return Math.floor(ms / 1000); } diff --git a/api/sound.js b/api/sound.js index bedb66940..4fa4b619d 100644 --- a/api/sound.js +++ b/api/sound.js @@ -23,14 +23,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 const _gestureWaits = new WeakMap(); @@ -47,7 +51,7 @@ async function loadAudioBuffer(url, context) { .catch((err) => { soundBufferCache.delete(url); throw err; - }), + }) ); } return soundBufferCache.get(url); @@ -68,8 +72,16 @@ function playBufferEverywhere(context, buffer, soundName, { loop, volume, playba const finish = () => { if (done) return; done = true; - try { source.stop(); } catch { /* already stopped */ } - try { gainNode.disconnect(); } catch { /* already disconnected */ } + try { + source.stop(); + } catch { + /* already stopped */ + } + try { + gainNode.disconnect(); + } catch { + /* already disconnected */ + } const idx = flock.globalSounds.indexOf(soundRef); if (idx !== -1) flock.globalSounds.splice(idx, 1); }; @@ -86,7 +98,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; @@ -97,7 +112,11 @@ function playBufferOnMesh(context, mesh, buffer, soundName, { loop, volume, play const currentSound = mesh.metadata.currentSound; if (currentSound) { - try { currentSound.stop(); } catch { /* already stopped */ } + try { + currentSound.stop(); + } catch { + /* already stopped */ + } } const panner = context.createPanner(); @@ -119,11 +138,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); @@ -143,10 +170,26 @@ function playBufferOnMesh(context, mesh, buffer, soundName, { loop, volume, play if (done) return; done = true; flock.scene?.onBeforeRenderObservable?.remove(observer); - try { source.stop(); } catch { /* already stopped */ } - try { source.disconnect(); } catch { /* already disconnected */ } - try { gainNode.disconnect(); } catch { /* already disconnected */ } - try { panner.disconnect(); } catch { /* already disconnected */ } + try { + source.stop(); + } catch { + /* already stopped */ + } + try { + source.disconnect(); + } catch { + /* already disconnected */ + } + try { + gainNode.disconnect(); + } catch { + /* already disconnected */ + } + try { + panner.disconnect(); + } catch { + /* already disconnected */ + } if (mesh.metadata?.currentSound === soundRef) delete mesh.metadata.currentSound; const idx = flock.globalSounds.indexOf(soundRef); if (idx !== -1) flock.globalSounds.splice(idx, 1); @@ -166,7 +209,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; @@ -222,13 +268,19 @@ 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); }); _gestureWaits.set(context, wait); } @@ -283,17 +335,12 @@ function getNoteBus(context) { } 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; } @@ -309,17 +356,17 @@ export const flockSound = { buffer = await loadAudioBuffer(soundUrl, context); } catch (err) { flock.reportBlockError({ - key: "sound_load_failed", - api: "playSound", + key: 'sound_load_failed', + api: 'playSound', values: { sound: soundName, url: soundUrl }, error: 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 }); } @@ -330,19 +377,20 @@ export const flockSound = { return new Promise((resolve) => { flock.whenModelReady(meshName, async (resolvedMesh) => { - if (flock.audioContext !== context) { resolve(); return; } + if (flock.audioContext !== context) { + resolve(); + return; + } // Spatial playback reads resolvedMesh.position every frame. - if (!flock.requireMesh(resolvedMesh, { api: "playSound", name: meshName })) { + if (!flock.requireMesh(resolvedMesh, { api: 'playSound', name: meshName })) { resolve(); return; } - const result = await playBufferOnMesh( - context, - resolvedMesh, - buffer, - soundName, - { loop, volume, playbackRate }, - ); + const result = await playBufferOnMesh(context, resolvedMesh, buffer, soundName, { + loop, + volume, + playbackRate, + }); resolve(result); }); }); @@ -353,7 +401,11 @@ export const flockSound = { flock._speechGeneration = (flock._speechGeneration ?? 0) + 1; flock._speechPausedByVisibility = false; } - try { window.speechSynthesis?.cancel(); } catch { /* unsupported */ } + try { + window.speechSynthesis?.cancel(); + } catch { + /* unsupported */ + } if (!flock?.globalSounds) return; soundBufferCache.clear(); @@ -363,14 +415,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; } @@ -383,14 +439,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); }); } }, @@ -406,7 +462,11 @@ export const flockSound = { const synth = window.speechSynthesis; if (synth?.speaking && !synth.paused) { flock._speechPausedByVisibility = true; - try { synth.pause(); } catch { /* unsupported */ } + try { + synth.pause(); + } catch { + /* unsupported */ + } } // An interruption before visibilitychange leaves 'interrupted', not 'running'. @@ -428,7 +488,11 @@ export const flockSound = { if (flock._speechPausedByVisibility) { flock._speechPausedByVisibility = false; - try { window.speechSynthesis?.resume(); } catch { /* unsupported */ } + try { + window.speechSynthesis?.resume(); + } catch { + /* unsupported */ + } } if (!flock._audioSuspendedByVisibility) return; @@ -442,33 +506,25 @@ export const flockSound = { }, async playNotes( meshName, - { - notes = [], - durations = [], - instrument = flock.createInstrument("square"), - } = {}, + { notes = [], durations = [], instrument = flock.createInstrument('square') } = {} ) { // Clear any prior stopAllSounds abort; stop must not disable future playback. 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; @@ -488,7 +544,7 @@ export const flockSound = { duration, bpm, baseTime + offsetTime, - instrument, + instrument ); } @@ -505,10 +561,14 @@ export const flockSound = { }); }; - if (meshName === "__everywhere__") { + if (meshName === '__everywhere__') { // context.close() is async; a lingering prior gain would briefly double the output. if (flock._everywhereGain) { - try { flock._everywhereGain.disconnect(); } catch { /* already detached */ } + try { + flock._everywhereGain.disconnect(); + } catch { + /* already detached */ + } flock._everywhereGain = null; } const gain = context.createGain(); @@ -516,7 +576,11 @@ export const flockSound = { gain.connect(getNoteBus(context)); 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 */ + } }); } @@ -527,7 +591,7 @@ 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; } @@ -536,18 +600,26 @@ export const flockSound = { if (!mesh.metadata.panner || mesh.metadata.panner.context !== context) { if (mesh.metadata.panner) { - try { mesh.metadata.panner.disconnect(); } catch { /* already disconnected */ } + try { + mesh.metadata.panner.disconnect(); + } catch { + /* already disconnected */ + } } 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; panner.connect(getNoteBus(context)); mesh.onDisposeObservable?.addOnce(() => { - try { panner.disconnect(); } catch { /* already disconnected */ } + try { + panner.disconnect(); + } catch { + /* already disconnected */ + } if (mesh.metadata?.panner === panner) delete mesh.metadata.panner; }); } @@ -561,10 +633,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(); @@ -575,19 +644,11 @@ 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; if (!isFinite(duration) || !isFinite(playTime) || !isFinite(bpm)) { - console.warn("playMidiNote: Invalid parameters", { + console.warn('playMidiNote: Invalid parameters', { duration, playTime, bpm, @@ -601,25 +662,23 @@ export const flockSound = { gainNode.gain.setValueAtTime(0, context.currentTime); // silent until envelope starts const panner = mesh.metadata.panner; - osc.type = instrument?.type ?? "sine"; + osc.type = instrument?.type ?? 'sine'; osc.frequency.value = flock.midiToFrequency(note); - 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); @@ -683,15 +742,22 @@ export const flockSound = { const cleanupDelay = Math.max(100, (stopTime - context.currentTime + 0.5) * 1000); cancelCleanup = audioTimer(context, cleanupDelay, doDisconnect); - 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) { @@ -705,18 +771,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))); @@ -742,8 +808,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; @@ -754,15 +820,15 @@ export const flockSound = { flock.whenModelReady(meshName, async function (mesh) { if (!mesh) { flock.reportBlockError({ - key: "object_not_found", - api: "setBPM", + key: 'object_not_found', + api: 'setBPM', values: { object: meshName }, }); resolve(); return; } - if (!mesh.metadata || typeof mesh.metadata !== "object") { + if (!mesh.metadata || typeof mesh.metadata !== 'object') { mesh.metadata = {}; } @@ -772,11 +838,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); @@ -784,10 +847,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) => { @@ -800,7 +863,7 @@ export const flockSound = { }); }; - if (meshName === "__everywhere__") { + if (meshName === '__everywhere__') { return playForMesh(null); } @@ -814,8 +877,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; @@ -824,7 +887,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; @@ -836,18 +899,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); @@ -880,19 +949,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; } // cancel() cannot reach an utterance that is not queued yet. @@ -913,11 +975,8 @@ export const flockSound = { // 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 @@ -950,110 +1009,108 @@ 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 - "nathan", - "alex", - "daniel", - "fred", - "jorge", - "tom", + 'nathan', + '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", - "zoe", - "karen", - "moira", - "tessa", - "veera", - "victoria", + 'samantha', + 'susan', + 'allison', + 'ava', + 'zoe', + '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", - "jamie", + 'daniel', + 'oliver', + 'jamie', // 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', ], }, - "es-ES": { + 'es-ES': { male: [ // macOS/iOS - "jorge", - "juan", - "diego", - "carlos", - "eddy", - "grandpa", - "reed", - "rocko", + 'jorge', + 'juan', + 'diego', + 'carlos', + 'eddy', + 'grandpa', + 'reed', + 'rocko', ], female: [ // macOS/iOS - "marisol", - "paulina", - "isabela", - "francisca", - "jimena", - "soledad", - "angelica", - "monica", - "flo", - "grandma", - "sandy", - "shelly", + 'marisol', + 'paulina', + 'isabela', + 'francisca', + 'jimena', + 'soledad', + 'angelica', + 'monica', + 'flo', + 'grandma', + 'sandy', + 'shelly', ], - } + }, }; // Filter voices by requested language 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]; } @@ -1063,7 +1120,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; } @@ -1071,89 +1128,83 @@ 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", - "marisol", - "paulina", - "isabela", - "francisca", - "jimena", - "soledad", - "angelica", - "monica", - "flo", - "grandma", - "sandy", - "shelly", - "zoe", + 'female', + 'woman', + 'lady', + 'girl', + 'samantha', + 'susan', + 'kate', + 'zira', + 'hazel', + 'helen', + 'karen', + 'moira', + 'tessa', + 'fiona', + 'allison', + 'ava', + 'veera', + 'victoria', + 'stephanie', + 'serena', + 'marisol', + 'paulina', + 'isabela', + 'francisca', + 'jimena', + 'soledad', + 'angelica', + 'monica', + 'flo', + 'grandma', + 'sandy', + 'shelly', + 'zoe', ]; 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", - "eddy", - "nathan", - "jamie", - "juan", - "diego", - "carlos", - "grandpa", - "reed", - "rocko", + 'male', + 'man', + 'david', + 'alex', + 'daniel', + 'mark', + 'tom', + 'george', + 'peter', + 'john', + 'michael', + 'robert', + 'fred', + 'jorge', + 'james', + 'oliver', + 'eddy', + 'nathan', + 'jamie', + 'juan', + 'diego', + 'carlos', + 'grandpa', + 'reed', + 'rocko', ]; selectedVoice = languageVoices.find( - (v) => - !maleTerms.some((term) => - v.name.toLowerCase().includes(term.toLowerCase()), - ), + (v) => !maleTerms.some((term) => v.name.toLowerCase().includes(term.toLowerCase())) ); } @@ -1171,7 +1222,7 @@ 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'); } } @@ -1193,8 +1244,7 @@ export const flockSound = { // even if onend is dropped (~14 chars/sec at rate 1, floor 2s, +1s buffer). // Use utterance.rate (already clamped to 0.1โ€“10) so the estimate matches // the rate actually spoken. - const seconds = - Math.max(2, String(text).length / 14) / utterance.rate + 1; + const seconds = Math.max(2, String(text).length / 14) / utterance.rate + 1; flock.showSubtitle(text, seconds); subtitleToken = flock._subtitleToken; }; @@ -1206,7 +1256,7 @@ export const flockSound = { }; const onSpeechError = (event) => { - console.warn("Speech synthesis error:", event.error); + console.warn('Speech synthesis error:', event.error); if (SPEECH_UNAVAILABLE.has(event.error)) { showBanner('speech', { message: translate('error_speech') }); } @@ -1215,7 +1265,7 @@ export const flockSound = { dismissBanner('speech'); }; - if (mode === "await") { + if (mode === 'await') { return new Promise((resolve) => { utterance.onend = () => { onSpeechEnd(); @@ -1272,7 +1322,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. @@ -1287,7 +1337,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/transform.js b/api/transform.js index fc92eb6d9..beb25f8f8 100644 --- a/api/transform.js +++ b/api/transform.js @@ -72,7 +72,7 @@ function toFinite(v, fallback = 0) { // Exactly ยฑ90ยฐ pitch makes a camera's look-at up vector degenerate; screen // roll then depends on platform libm rounding and can render upside down. function clampCameraPitchDegrees(x) { - const wrapped = ((x % 360) + 540) % 360 - 180; + const wrapped = (((x % 360) + 540) % 360) - 180; if (Math.abs(Math.abs(wrapped) - 90) < 0.05) { return Math.sign(wrapped) * (Math.abs(wrapped) > 90 ? 90.05 : 89.95); } @@ -500,10 +500,7 @@ export const flockTransform = { resolve(); return; } - if ( - !(mesh instanceof flock.BABYLON.AbstractMesh) && - mesh.name !== 'hemisphericLight' - ) { + if (!(mesh instanceof flock.BABYLON.AbstractMesh) && mesh.name !== 'hemisphericLight') { flock.reportBlockError({ key: 'target_not_a_mesh', api: 'rotateTo', diff --git a/api/xr.js b/api/xr.js index a5f7cbdde..5667e05ec 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, @@ -139,14 +126,12 @@ export const flockXR = { return new Promise((resolve) => { flock.whenModelReady(meshName, async function (mesh) { - if (!flock.requireMesh(mesh, { api: "exportMesh", name: meshName })) { + if (!flock.requireMesh(mesh, { api: 'exportMesh', name: meshName })) { resolve(); return; } 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) { @@ -156,39 +141,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; @@ -204,13 +177,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 9b9085003..43111bc1a 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 4acc16745..377f81b86 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 0bcd3b6ef..d1487179f 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)) @@ -481,25 +467,25 @@ const MICROBIT_GLYPHS = { plug: { width: 384, height: 512, - d: "M96 0C78.3 0 64 14.3 64 32l0 96 64 0 0-96c0-17.7-14.3-32-32-32zM288 0c-17.7 0-32 14.3-32 32l0 96 64 0 0-96c0-17.7-14.3-32-32-32zM32 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l0 32c0 77.4 55 142 128 156.8l0 67.2c0 17.7 14.3 32 32 32s32-14.3 32-32l0-67.2C297 398 352 333.4 352 256l0-32c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 160z", + d: 'M96 0C78.3 0 64 14.3 64 32l0 96 64 0 0-96c0-17.7-14.3-32-32-32zM288 0c-17.7 0-32 14.3-32 32l0 96 64 0 0-96c0-17.7-14.3-32-32-32zM32 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l0 32c0 77.4 55 142 128 156.8l0 67.2c0 17.7 14.3 32 32 32s32-14.3 32-32l0-67.2C297 398 352 333.4 352 256l0-32c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 160z', }, waves: { width: 640, height: 512, - d: "M54.2 202.9C123.2 136.7 216.8 96 320 96s196.8 40.7 265.8 106.9c12.8 12.2 33 11.8 45.2-.9s11.8-33-.9-45.2C549.7 79.5 440.4 32 320 32S90.3 79.5 9.8 156.7C-2.9 169-3.3 189.2 8.9 202s32.5 13.2 45.2 .9zM320 256c56.8 0 108.6 21.1 148.2 56c13.3 11.7 33.5 10.4 45.2-2.8s10.4-33.5-2.8-45.2C459.8 219.2 393 192 320 192s-139.8 27.2-190.5 72c-13.3 11.7-14.5 32-2.8 45.2s32 14.5 45.2 2.8c39.5-34.9 91.3-56 148.2-56zm64 160a64 64 0 1 0 -128 0 64 64 0 1 0 128 0z", + d: 'M54.2 202.9C123.2 136.7 216.8 96 320 96s196.8 40.7 265.8 106.9c12.8 12.2 33 11.8 45.2-.9s11.8-33-.9-45.2C549.7 79.5 440.4 32 320 32S90.3 79.5 9.8 156.7C-2.9 169-3.3 189.2 8.9 202s32.5 13.2 45.2 .9zM320 256c56.8 0 108.6 21.1 148.2 56c13.3 11.7 33.5 10.4 45.2-2.8s10.4-33.5-2.8-45.2C459.8 219.2 393 192 320 192s-139.8 27.2-190.5 72c-13.3 11.7-14.5 32-2.8 45.2s32 14.5 45.2 2.8c39.5-34.9 91.3-56 148.2-56zm64 160a64 64 0 1 0 -128 0 64 64 0 1 0 128 0z', }, hourglass: { width: 384, height: 512, - d: "M32 0C14.3 0 0 14.3 0 32S14.3 64 32 64l0 11c0 42.4 16.9 83.1 46.9 113.1L146.7 256 78.9 323.9C48.9 353.9 32 394.6 32 437l0 11c-17.7 0-32 14.3-32 32s14.3 32 32 32l32 0 256 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-11c0-42.4-16.9-83.1-46.9-113.1L237.3 256l67.9-67.9c30-30 46.9-70.7 46.9-113.1l0-11c17.7 0 32-14.3 32-32s-14.3-32-32-32L320 0 64 0 32 0zM96 75l0-11 192 0 0 11c0 25.5-10.1 49.9-28.1 67.9L192 210.7l-67.9-67.9C106.1 124.9 96 100.5 96 75z", + d: 'M32 0C14.3 0 0 14.3 0 32S14.3 64 32 64l0 11c0 42.4 16.9 83.1 46.9 113.1L146.7 256 78.9 323.9C48.9 353.9 32 394.6 32 437l0 11c-17.7 0-32 14.3-32 32s14.3 32 32 32l32 0 256 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-11c0-42.4-16.9-83.1-46.9-113.1L237.3 256l67.9-67.9c30-30 46.9-70.7 46.9-113.1l0-11c17.7 0 32-14.3 32-32s-14.3-32-32-32L320 0 64 0 32 0zM96 75l0-11 192 0 0 11c0 25.5-10.1 49.9-28.1 67.9L192 210.7l-67.9-67.9C106.1 124.9 96 100.5 96 75z', }, }; const MICROBIT_STATUS_SPECS = { - unbound: { color: "#757575", glyph: "plug" }, - busy: { color: "#e6a817", glyph: "hourglass" }, - tethered: { color: "#2e8b57", glyph: "plug" }, - radio: { color: "#2e8b57", glyph: "waves" }, + unbound: { color: '#757575', glyph: 'plug' }, + busy: { color: '#e6a817', glyph: 'hourglass' }, + tethered: { color: '#2e8b57', glyph: 'plug' }, + radio: { color: '#2e8b57', glyph: 'waves' }, }; export function makeMicrobitStatusIcon(status) { diff --git a/blocks/blocks.js b/blocks/blocks.js index e43d48fc7..08a8e928e 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(); }; } @@ -86,10 +84,9 @@ if (!fieldColourPrototype[flockFocusPatchKey]) { // loading a saved workspace) and our own refreshReporterAriaLabels both call // recomputeAriaContext on fields that may not be rendered yet. Guard the same // way applyColour() already does upstream. -const flockAriaPatchKey = Symbol.for("flock.fieldColourAriaPatch"); +const flockAriaPatchKey = Symbol.for('flock.fieldColourAriaPatch'); if (!fieldColourPrototype[flockAriaPatchKey]) { - const originalRecomputeAriaContext = - fieldColourPrototype.recomputeAriaContext; + const originalRecomputeAriaContext = fieldColourPrototype.recomputeAriaContext; fieldColourPrototype.recomputeAriaContext = function () { if (!this.fieldGroup_) return true; return originalRecomputeAriaContext.call(this); @@ -102,12 +99,12 @@ if (!fieldColourPrototype[flockAriaPatchKey]) { // 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(); @@ -121,11 +118,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); @@ -184,10 +181,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. @@ -199,12 +196,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 @@ -212,10 +209,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 @@ -247,7 +265,7 @@ export function applyInputAriaLabels(block, overrides) { label = derived; } } - if (label && typeof input.setAriaLabelProvider === "function") { + if (label && typeof input.setAriaLabelProvider === 'function') { input.setAriaLabelProvider(label); } } @@ -259,7 +277,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?.(); @@ -270,13 +288,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 @@ -287,14 +305,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); } @@ -315,12 +333,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'); } } } @@ -333,8 +351,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, @@ -343,29 +361,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(); @@ -374,20 +383,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(); @@ -395,17 +400,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(); } } @@ -432,21 +433,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); } @@ -456,10 +449,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(() => { @@ -472,8 +464,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(); @@ -483,10 +474,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]; @@ -513,9 +501,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; @@ -526,10 +512,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; } @@ -559,19 +542,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); } @@ -583,23 +561,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; } @@ -614,12 +592,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 @@ -643,45 +616,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) { @@ -694,9 +656,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) { @@ -709,13 +669,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) { @@ -730,13 +684,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) { @@ -747,7 +700,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+)$/); @@ -768,10 +721,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}`; @@ -787,13 +737,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 @@ -853,7 +797,7 @@ function adoptIsolatedDefaultVarsTo( workspace, BlocklyNS, createdIds, - allBlocks, + allBlocks ) { const descendantIds = buildDescendantIdSet(rootBlock); let adopted = 0; @@ -913,7 +857,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); } } } @@ -934,12 +878,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); @@ -949,13 +893,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, @@ -963,12 +901,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 { @@ -988,19 +924,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 ); } @@ -1015,28 +952,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( @@ -1047,7 +972,7 @@ export function ensureFreshVarOnDuplicate( ws, BlocklyNS, createdIds, - allBlocks, + allBlocks ); // If more children will connect later, remember to finish on subsequent events. @@ -1092,11 +1017,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) { @@ -1109,8 +1034,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++) { @@ -1120,7 +1045,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) { @@ -1144,21 +1069,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_(); @@ -1170,12 +1093,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; @@ -1183,19 +1106,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_(); @@ -1209,22 +1129,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); } } @@ -1233,61 +1149,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); } @@ -1312,8 +1221,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/`; } @@ -1321,10 +1230,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: { @@ -1376,7 +1285,7 @@ export function initializeVariableIndexes() { plane: 1, wall: 1, text: 1, - "3dtext": 1, + '3dtext': 1, sound: 1, character: 1, object: 1, @@ -1402,7 +1311,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, ''); }); } @@ -1430,68 +1339,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) => { @@ -1502,23 +1409,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(); @@ -1530,180 +1432,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 () { @@ -1712,7 +1614,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) { @@ -1730,10 +1632,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(); @@ -1764,10 +1663,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); @@ -1782,11 +1681,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; @@ -1807,10 +1706,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); } @@ -1820,37 +1716,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); } } @@ -1897,8 +1793,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; @@ -1923,41 +1818,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(''); } }; } @@ -1967,7 +1862,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 @@ -1977,8 +1872,8 @@ export function handleBlockCreateEvent( variableNamePrefix, nextVariableIndexes, { - fieldName: "ID_VAR", - }, + fieldName: 'ID_VAR', + } ); if (handledDuplicate) return; if (blockInstance.id !== changeEvent.blockId) return; @@ -1996,13 +1891,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 @@ -2012,12 +1905,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() @@ -2030,7 +1921,7 @@ export function handleBlockCreateEvent( if (newSuffix !== null) { nextVariableIndexes[variableNamePrefix] = Math.max( nextVariableIndexes[variableNamePrefix] || 1, - newSuffix + 1, + newSuffix + 1 ); } } @@ -2039,18 +1930,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() @@ -2060,7 +1946,7 @@ export function handleBlockCreateEvent( } nextVariableIndexes[variableNamePrefix] = Math.max( nextVariableIndexes[variableNamePrefix], - currentSuffix + 1, + currentSuffix + 1 ); } } @@ -2071,7 +1957,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); @@ -2079,10 +1965,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; @@ -2095,28 +1981,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); @@ -2124,18 +2005,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; @@ -2144,10 +2022,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; }; @@ -2174,7 +2049,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 @@ -2183,7 +2058,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; @@ -2192,12 +2067,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; } @@ -2227,16 +2102,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 5d32a98d0..6ef5aa3ed 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 717da59a0..87c99a1de 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/colourvalidation.js b/blocks/colourvalidation.js index 62b3f237e..0d0179568 100644 --- a/blocks/colourvalidation.js +++ b/blocks/colourvalidation.js @@ -1,12 +1,12 @@ // CSS keywords that resolve against surrounding context and so have no // meaning for a Babylon material colour; treated as invalid. const CONTEXT_DEPENDENT_KEYWORDS = new Set([ - "currentcolor", - "inherit", - "initial", - "unset", - "revert", - "revert-layer", + 'currentcolor', + 'inherit', + 'initial', + 'unset', + 'revert', + 'revert-layer', ]); // True when the string is something colour_from_string can turn into a real @@ -14,7 +14,7 @@ const CONTEXT_DEPENDENT_KEYWORDS = new Set([ // rgb(), hsl(), transparent, ...), excluding the context-dependent keywords // and var() references (which resolve against surrounding custom properties). export function isValidColourInput(raw) { - const value = typeof raw === "string" ? raw.trim().replace(/^#/, "") : ""; + const value = typeof raw === 'string' ? raw.trim().replace(/^#/, '') : ''; if (!value) return false; if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value)) return true; // var() references only resolve against a surrounding context we don't have, @@ -22,7 +22,7 @@ export function isValidColourInput(raw) { if (/var\s*\(/i.test(value)) return false; if (CONTEXT_DEPENDENT_KEYWORDS.has(value.toLowerCase())) return false; const probe = new Option().style; - probe.color = ""; + probe.color = ''; probe.color = value; - return probe.color !== ""; + return probe.color !== ''; } diff --git a/blocks/combine.js b/blocks/combine.js index 1da2bd23a..be8d47c5f 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 e6354d6ad..0d1559338 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 83d7a77c1..b5dee4773 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 a48f78b4e..89c00f422 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 487e903a9..41456ff0f 100644 --- a/blocks/effects.js +++ b/blocks/effects.js @@ -1,137 +1,133 @@ -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["enable_shadows"] = { + Blockly.Blocks['enable_shadows'] = { init: function () { this.jsonInit({ - type: "enable_shadows", - message0: translate("enable_shadows"), + type: 'enable_shadows', + message0: translate('enable_shadows'), args0: [ { - type: "field_checkbox", - name: "ENABLED", + type: 'field_checkbox', + name: 'ENABLED', checked: true, }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("enable_shadows"), + colour: categoryColours['Scene'], + tooltip: getTooltip('enable_shadows'), }); 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 7984a5fe9..e1a1f0060 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/fieldMicrobitImage.js b/blocks/fieldMicrobitImage.js index 79a4758fb..f55a1d419 100644 --- a/blocks/fieldMicrobitImage.js +++ b/blocks/fieldMicrobitImage.js @@ -3,8 +3,8 @@ // preset buttons and a paintable grid. The value is the wire pattern itself // (25 brightness digits, row-major; the editor writes only 0 and 9), so // XML/JSON save, undo/redo, and copy/paste all use the plain string. -import * as Blockly from "blockly"; -import { translate } from "../main/translation.js"; +import * as Blockly from 'blockly'; +import { translate } from '../main/translation.js'; import { MICROBIT_IMAGE_GRID_SIZE, MICROBIT_IMAGE_DEFAULT, @@ -18,7 +18,7 @@ import { setImageCell, toggleImageCell, makeMicrobitImageDataUri, -} from "./microbitImagePattern.js"; +} from './microbitImagePattern.js'; const FIELD_IMAGE_SIZE = 50; // px on the block face, same as material previews const PRESET_PREVIEW_SIZE = 28; @@ -30,7 +30,7 @@ const CELL_OFF_COLOUR = MICROBIT_IMAGE_UNLIT_COLOUR; export class FieldMicrobitImage extends Blockly.Field { SERIALIZABLE = true; EDITABLE = true; - CURSOR = "pointer"; + CURSOR = 'pointer'; constructor(value = MICROBIT_IMAGE_DEFAULT) { super(value); @@ -56,7 +56,7 @@ export class FieldMicrobitImage extends Blockly.Field { height: `${FIELD_IMAGE_SIZE}px`, width: `${FIELD_IMAGE_SIZE}px`, }, - this.fieldGroup_, + this.fieldGroup_ ); this._updateFaceImage(); } @@ -75,28 +75,25 @@ export class FieldMicrobitImage extends Blockly.Field { _updateFaceImage() { this.imageElement_?.setAttributeNS( Blockly.utils.dom.XLINK_NS, - "xlink:href", - makeMicrobitImageDataUri(this.getValue()), + 'xlink:href', + makeMicrobitImageDataUri(this.getValue()) ); } // The 25-digit value is meaningless read aloud; describe the field instead. getText_() { - return translate("microbit_image_field_label"); + return translate('microbit_image_field_label'); } computeAriaLabel() { - return translate("microbit_image_field_label"); + return translate('microbit_image_field_label'); } // ------------------------------------------------------------------ editor showEditor_() { Blockly.DropDownDiv.getContentDiv().appendChild(this._createEditor()); - Blockly.DropDownDiv.showPositionedByField( - this, - this._disposeEditor.bind(this), - ); + Blockly.DropDownDiv.showPositionedByField(this, this._disposeEditor.bind(this)); this._cellButtons?.[0]?.focus(); } @@ -106,18 +103,16 @@ export class FieldMicrobitImage extends Blockly.Field { } _createEditor() { - const container = document.createElement("div"); - container.style.padding = "8px"; - - const presetRow = document.createElement("div"); - presetRow.style.display = "flex"; - presetRow.style.gap = "6px"; - presetRow.style.marginBottom = "8px"; - presetRow.style.alignItems = "stretch"; - for (const name of ["clear", "full"]) { - presetRow.appendChild( - this._createPresetButton(name, MICROBIT_IMAGE_PRESETS[name]), - ); + const container = document.createElement('div'); + container.style.padding = '8px'; + + const presetRow = document.createElement('div'); + presetRow.style.display = 'flex'; + presetRow.style.gap = '6px'; + presetRow.style.marginBottom = '8px'; + presetRow.style.alignItems = 'stretch'; + for (const name of ['clear', 'full']) { + presetRow.appendChild(this._createPresetButton(name, MICROBIT_IMAGE_PRESETS[name])); } presetRow.appendChild(this._createPictureMenu()); container.appendChild(presetRow); @@ -129,75 +124,72 @@ export class FieldMicrobitImage extends Blockly.Field { // the material dropdowns: each menu entry is the picture itself, rendered // by the same SVG used on the block face. _createPictureMenu() { - const wrapper = document.createElement("div"); - wrapper.style.position = "relative"; - - const trigger = document.createElement("button"); - trigger.type = "button"; - trigger.setAttribute("aria-label", translate("microbit_image_picture_menu")); - trigger.setAttribute("aria-haspopup", "true"); - trigger.setAttribute("aria-expanded", "false"); - trigger.style.cursor = "pointer"; - trigger.style.height = "100%"; - trigger.style.padding = "2px 4px"; - trigger.style.border = "1px solid #666"; - trigger.style.borderRadius = "4px"; - trigger.style.background = "transparent"; - trigger.style.display = "flex"; - trigger.style.alignItems = "center"; - trigger.style.gap = "2px"; - const triggerPreview = document.createElement("img"); + const wrapper = document.createElement('div'); + wrapper.style.position = 'relative'; + + const trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.setAttribute('aria-label', translate('microbit_image_picture_menu')); + trigger.setAttribute('aria-haspopup', 'true'); + trigger.setAttribute('aria-expanded', 'false'); + trigger.style.cursor = 'pointer'; + trigger.style.height = '100%'; + trigger.style.padding = '2px 4px'; + trigger.style.border = '1px solid #666'; + trigger.style.borderRadius = '4px'; + trigger.style.background = 'transparent'; + trigger.style.display = 'flex'; + trigger.style.alignItems = 'center'; + trigger.style.gap = '2px'; + const triggerPreview = document.createElement('img'); triggerPreview.src = makeMicrobitImageDataUri( - MICROBIT_IMAGE_PRESETS[MICROBIT_IMAGE_PICTURE_NAMES[0]], + MICROBIT_IMAGE_PRESETS[MICROBIT_IMAGE_PICTURE_NAMES[0]] ); - triggerPreview.alt = ""; + triggerPreview.alt = ''; triggerPreview.width = PRESET_PREVIEW_SIZE; triggerPreview.height = PRESET_PREVIEW_SIZE; - triggerPreview.style.display = "block"; + triggerPreview.style.display = 'block'; trigger.appendChild(triggerPreview); - trigger.appendChild(document.createTextNode("โ–พ")); - - const panel = document.createElement("div"); - panel.style.position = "absolute"; - panel.style.top = "calc(100% + 2px)"; - panel.style.left = "0"; - panel.style.zIndex = "1"; - panel.style.padding = "4px"; - panel.style.background = "#fff"; - panel.style.border = "1px solid #666"; - panel.style.borderRadius = "4px"; - panel.style.display = "none"; + trigger.appendChild(document.createTextNode('โ–พ')); + + const panel = document.createElement('div'); + panel.style.position = 'absolute'; + panel.style.top = 'calc(100% + 2px)'; + panel.style.left = '0'; + panel.style.zIndex = '1'; + panel.style.padding = '4px'; + panel.style.background = '#fff'; + panel.style.border = '1px solid #666'; + panel.style.borderRadius = '4px'; + panel.style.display = 'none'; panel.style.gridTemplateColumns = `repeat(${Math.min( 4, - MICROBIT_IMAGE_PICTURE_NAMES.length, + MICROBIT_IMAGE_PICTURE_NAMES.length )}, auto)`; - panel.style.gap = "4px"; + panel.style.gap = '4px'; const setOpen = (open) => { - panel.style.display = open ? "grid" : "none"; - trigger.setAttribute("aria-expanded", String(open)); + panel.style.display = open ? 'grid' : 'none'; + trigger.setAttribute('aria-expanded', String(open)); }; for (const name of MICROBIT_IMAGE_PICTURE_NAMES) { - const option = document.createElement("button"); - option.type = "button"; - option.setAttribute( - "aria-label", - translate(`microbit_image_preset_${name}`), - ); - option.style.cursor = "pointer"; - option.style.padding = "2px"; - option.style.border = "none"; - option.style.borderRadius = "4px"; - option.style.background = "transparent"; - const preview = document.createElement("img"); + const option = document.createElement('button'); + option.type = 'button'; + option.setAttribute('aria-label', translate(`microbit_image_preset_${name}`)); + option.style.cursor = 'pointer'; + option.style.padding = '2px'; + option.style.border = 'none'; + option.style.borderRadius = '4px'; + option.style.background = 'transparent'; + const preview = document.createElement('img'); preview.src = makeMicrobitImageDataUri(MICROBIT_IMAGE_PRESETS[name]); - preview.alt = ""; + preview.alt = ''; preview.width = PICTURE_PREVIEW_SIZE; preview.height = PICTURE_PREVIEW_SIZE; - preview.style.display = "block"; + preview.style.display = 'block'; option.appendChild(preview); - option.addEventListener("click", () => { + option.addEventListener('click', () => { this.setValue(MICROBIT_IMAGE_PRESETS[name]); this._refreshCells(); setOpen(false); @@ -206,14 +198,14 @@ export class FieldMicrobitImage extends Blockly.Field { panel.appendChild(option); } - trigger.addEventListener("click", () => { - const open = panel.style.display === "none"; + trigger.addEventListener('click', () => { + const open = panel.style.display === 'none'; setOpen(open); - if (open) panel.querySelector("button")?.focus(); + if (open) panel.querySelector('button')?.focus(); }); // Escape closes just the picture menu, not the whole field editor. - wrapper.addEventListener("keydown", (event) => { - if (event.key !== "Escape" || panel.style.display === "none") return; + wrapper.addEventListener('keydown', (event) => { + if (event.key !== 'Escape' || panel.style.display === 'none') return; event.stopPropagation(); setOpen(false); trigger.focus(); @@ -225,25 +217,22 @@ export class FieldMicrobitImage extends Blockly.Field { } _createPresetButton(name, pattern) { - const button = document.createElement("button"); - button.type = "button"; - button.setAttribute( - "aria-label", - translate(`microbit_image_preset_${name}`), - ); - button.style.cursor = "pointer"; - button.style.padding = "2px"; - button.style.border = "1px solid #666"; - button.style.borderRadius = "4px"; - button.style.background = "transparent"; - const preview = document.createElement("img"); + const button = document.createElement('button'); + button.type = 'button'; + button.setAttribute('aria-label', translate(`microbit_image_preset_${name}`)); + button.style.cursor = 'pointer'; + button.style.padding = '2px'; + button.style.border = '1px solid #666'; + button.style.borderRadius = '4px'; + button.style.background = 'transparent'; + const preview = document.createElement('img'); preview.src = makeMicrobitImageDataUri(pattern); - preview.alt = ""; + preview.alt = ''; preview.width = PRESET_PREVIEW_SIZE; preview.height = PRESET_PREVIEW_SIZE; - preview.style.display = "block"; + preview.style.display = 'block'; button.appendChild(preview); - button.addEventListener("click", () => { + button.addEventListener('click', () => { this.setValue(pattern); this._refreshCells(); }); @@ -251,17 +240,17 @@ export class FieldMicrobitImage extends Blockly.Field { } _createGrid() { - const grid = document.createElement("div"); - grid.setAttribute("role", "group"); - grid.setAttribute("aria-label", translate("microbit_image_grid_label")); - grid.style.display = "grid"; + const grid = document.createElement('div'); + grid.setAttribute('role', 'group'); + grid.setAttribute('aria-label', translate('microbit_image_grid_label')); + grid.style.display = 'grid'; grid.style.gridTemplateColumns = `repeat(${MICROBIT_IMAGE_GRID_SIZE}, ${CELL_SIZE}px)`; - grid.style.gap = "4px"; - grid.style.padding = "6px"; + grid.style.gap = '4px'; + grid.style.padding = '6px'; grid.style.background = MICROBIT_IMAGE_PLATE_COLOUR; - grid.style.borderRadius = "6px"; - grid.style.width = "fit-content"; - grid.style.touchAction = "none"; + grid.style.borderRadius = '6px'; + grid.style.width = 'fit-content'; + grid.style.touchAction = 'none'; this._cellButtons = []; for (let row = 0; row < MICROBIT_IMAGE_GRID_SIZE; row++) { @@ -273,54 +262,48 @@ export class FieldMicrobitImage extends Blockly.Field { // Pointer-drag painting: the first cell toggles and every cell swept // afterwards takes the digit the first cell became. - grid.addEventListener("pointerdown", (event) => { + grid.addEventListener('pointerdown', (event) => { const cell = this._cellAt(event); if (!cell) return; event.preventDefault(); // no text-selection/drag artefacts while painting - const { pattern, digit } = toggleImageCell( - this.getValue(), - cell.row, - cell.col, - ); + const { pattern, digit } = toggleImageCell(this.getValue(), cell.row, cell.col); this._paintDigit = digit; this.setValue(pattern); this._refreshCells(); }); - grid.addEventListener("pointermove", (event) => { + grid.addEventListener('pointermove', (event) => { if (this._paintDigit === null) return; const cell = this._cellAt(event); if (!cell) return; - this.setValue( - setImageCell(this.getValue(), cell.row, cell.col, this._paintDigit), - ); + this.setValue(setImageCell(this.getValue(), cell.row, cell.col, this._paintDigit)); this._refreshCells(); }); const endPaint = () => { this._paintDigit = null; }; - grid.addEventListener("pointerup", endPaint); - grid.addEventListener("pointercancel", endPaint); + grid.addEventListener('pointerup', endPaint); + grid.addEventListener('pointercancel', endPaint); - grid.addEventListener("keydown", (event) => this._onGridKeyDown(event)); + grid.addEventListener('keydown', (event) => this._onGridKeyDown(event)); return grid; } _createCell(row, col) { - const button = document.createElement("button"); - button.type = "button"; + const button = document.createElement('button'); + button.type = 'button'; button.dataset.row = String(row); button.dataset.col = String(col); // Roving tabindex: one tab stop for the whole grid, arrows move within. button.tabIndex = row === 0 && col === 0 ? 0 : -1; button.style.width = `${CELL_SIZE}px`; button.style.height = `${CELL_SIZE}px`; - button.style.border = "none"; - button.style.borderRadius = "4px"; - button.style.cursor = "pointer"; - button.style.padding = "0"; + button.style.border = 'none'; + button.style.borderRadius = '4px'; + button.style.cursor = 'pointer'; + button.style.padding = '0'; // Keyboard activation only: pointer input is handled by the grid's // painting listeners, and a mouse click would otherwise toggle twice. - button.addEventListener("click", (event) => { + button.addEventListener('click', (event) => { if (event.detail !== 0) return; this.setValue(toggleImageCell(this.getValue(), row, col).pattern); this._refreshCells(); @@ -334,7 +317,7 @@ export class FieldMicrobitImage extends Blockly.Field { // captures the pointer on the pressed cell, so during a drag the events // keep targeting that first cell rather than the one under the finger. const element = document.elementFromPoint(event.clientX, event.clientY); - const button = element?.closest?.("button[data-row]"); + const button = element?.closest?.('button[data-row]'); if (!button || !this._cellButtons?.includes(button)) return null; return { row: Number(button.dataset.row), col: Number(button.dataset.col) }; } @@ -348,34 +331,31 @@ export class FieldMicrobitImage extends Blockly.Field { const on = isImageCellOn(pattern, row, col); button.style.background = on ? CELL_ON_COLOUR : CELL_OFF_COLOUR; button.setAttribute( - "aria-label", - translate("microbit_image_cell_label") - .replace("%1", String(row + 1)) - .replace("%2", String(col + 1)) - .replace( - "%3", - translate(on ? "microbit_image_cell_on" : "microbit_image_cell_off"), - ), + 'aria-label', + translate('microbit_image_cell_label') + .replace('%1', String(row + 1)) + .replace('%2', String(col + 1)) + .replace('%3', translate(on ? 'microbit_image_cell_on' : 'microbit_image_cell_off')) ); } } _onGridKeyDown(event) { - const target = event.target?.closest?.("button[data-row]"); + const target = event.target?.closest?.('button[data-row]'); if (!target || !this._cellButtons) return; let row = Number(target.dataset.row); let col = Number(target.dataset.col); switch (event.key) { - case "ArrowUp": + case 'ArrowUp': row = Math.max(0, row - 1); break; - case "ArrowDown": + case 'ArrowDown': row = Math.min(MICROBIT_IMAGE_GRID_SIZE - 1, row + 1); break; - case "ArrowLeft": + case 'ArrowLeft': col = Math.max(0, col - 1); break; - case "ArrowRight": + case 'ArrowRight': col = Math.min(MICROBIT_IMAGE_GRID_SIZE - 1, col + 1); break; default: @@ -390,4 +370,4 @@ export class FieldMicrobitImage extends Blockly.Field { } } -Blockly.fieldRegistry.register("field_microbit_image", FieldMicrobitImage); +Blockly.fieldRegistry.register('field_microbit_image', FieldMicrobitImage); diff --git a/blocks/materials.js b/blocks/materials.js index 1b5ea1786..6e7372198 100644 --- a/blocks/materials.js +++ b/blocks/materials.js @@ -1,10 +1,10 @@ -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 { isValidColourInput } from "./colourvalidation.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 { isValidColourInput } from './colourvalidation.js'; import { makeTouchesInputSubtree, wasBlockDeleted, @@ -13,48 +13,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`, @@ -67,570 +67,566 @@ 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["set_shadow"] = { + Blockly.Blocks['set_shadow'] = { init: function () { this.jsonInit({ - type: "set_shadow", - message0: translate("set_shadow"), + type: 'set_shadow', + message0: translate('set_shadow'), args0: [ { - type: "field_variable", - name: "MESH", + type: 'field_variable', + name: 'MESH', variable: window.currentMesh, }, { - type: "field_checkbox", - name: "CAST", + type: 'field_checkbox', + name: 'CAST', checked: true, }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Materials"], - tooltip: getTooltip("set_shadow"), + colour: categoryColours['Materials'], + tooltip: getTooltip('set_shadow'), }); 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'); } }; const updateInvalidWarning = (block, value) => { if (!block || block.isInFlyout) return; block.setWarningText( - isValidColourInput(value) - ? null - : translate("colour_from_string_invalid"), + isValidColourInput(value) ? null : translate('colour_from_string_invalid') ); }; 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); updateInvalidWarning(this.sourceBlock_, newVal); 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; } }); @@ -638,45 +634,45 @@ 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); updateInvalidWarning(this, initialVal); } 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`, @@ -689,63 +685,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 = [ @@ -760,11 +756,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; @@ -772,7 +767,7 @@ export function defineMaterialsBlocks() { promoteMaterialContainerFromShadow(block); - if (!block.getInputTargetBlock("MATERIAL")) { + if (!block.getInputTargetBlock('MATERIAL')) { respawnMaterialShadow(block); return; } @@ -781,32 +776,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/microbitImagePattern.js b/blocks/microbitImagePattern.js index 010699deb..9651c4319 100644 --- a/blocks/microbitImagePattern.js +++ b/blocks/microbitImagePattern.js @@ -4,13 +4,13 @@ // row-major from the top-left; the editor only ever writes 0 (off) and 9 // (on). On the wire the pattern travels as five short row lines โ€” see // serialiseImageRows in microbit/protocol.js. -import { normaliseImagePattern } from "../microbit/protocol.js"; +import { normaliseImagePattern } from '../microbit/protocol.js'; export const MICROBIT_IMAGE_GRID_SIZE = 5; -export const MICROBIT_IMAGE_OFF = "0"; -export const MICROBIT_IMAGE_ON = "9"; +export const MICROBIT_IMAGE_OFF = '0'; +export const MICROBIT_IMAGE_ON = '9'; export const MICROBIT_IMAGE_DEFAULT = MICROBIT_IMAGE_OFF.repeat( - MICROBIT_IMAGE_GRID_SIZE * MICROBIT_IMAGE_GRID_SIZE, + MICROBIT_IMAGE_GRID_SIZE * MICROBIT_IMAGE_GRID_SIZE ); // Named preset patterns. clear and full appear as one-click buttons in the @@ -19,27 +19,26 @@ export const MICROBIT_IMAGE_DEFAULT = MICROBIT_IMAGE_OFF.repeat( export const MICROBIT_IMAGE_PRESETS = Object.freeze({ clear: MICROBIT_IMAGE_DEFAULT, full: MICROBIT_IMAGE_ON.repeat(MICROBIT_IMAGE_DEFAULT.length), - sun: "9090909990999990999090909", + sun: '9090909990999990999090909', }); // Dropdown pictures, in menu order. To add one: add its pattern to // MICROBIT_IMAGE_PRESETS, its name here, and a microbit_image_preset_ // string to the locales. -export const MICROBIT_IMAGE_PICTURE_NAMES = Object.freeze(["sun"]); +export const MICROBIT_IMAGE_PICTURE_NAMES = Object.freeze(['sun']); export { normaliseImagePattern }; // High-contrast LED colours, shared by the block-face SVG and the editor // grid: bright red on near-black reads at block-icon sizes. -export const MICROBIT_IMAGE_LIT_COLOUR = "#ff8a80"; -export const MICROBIT_IMAGE_UNLIT_COLOUR = "#303030"; -export const MICROBIT_IMAGE_PLATE_COLOUR = "#0d0d0d"; +export const MICROBIT_IMAGE_LIT_COLOUR = '#ff8a80'; +export const MICROBIT_IMAGE_UNLIT_COLOUR = '#303030'; +export const MICROBIT_IMAGE_PLATE_COLOUR = '#0d0d0d'; /** True when the LED at (row, col) is lit (any non-zero brightness). */ export function isImageCellOn(pattern, row, col) { return ( - normaliseImagePattern(pattern)[row * MICROBIT_IMAGE_GRID_SIZE + col] !== - MICROBIT_IMAGE_OFF + normaliseImagePattern(pattern)[row * MICROBIT_IMAGE_GRID_SIZE + col] !== MICROBIT_IMAGE_OFF ); } @@ -47,9 +46,7 @@ export function isImageCellOn(pattern, row, col) { export function setImageCell(pattern, row, col, digit) { const normalised = normaliseImagePattern(pattern); const index = row * MICROBIT_IMAGE_GRID_SIZE + col; - return ( - normalised.slice(0, index) + digit + normalised.slice(index + 1) - ); + return normalised.slice(0, index) + digit + normalised.slice(index + 1); } /** @@ -58,9 +55,7 @@ export function setImageCell(pattern, row, col, digit) { * every other cell it sweeps. */ export function toggleImageCell(pattern, row, col) { - const digit = isImageCellOn(pattern, row, col) - ? MICROBIT_IMAGE_OFF - : MICROBIT_IMAGE_ON; + const digit = isImageCellOn(pattern, row, col) ? MICROBIT_IMAGE_OFF : MICROBIT_IMAGE_ON; return { pattern: setImageCell(pattern, row, col, digit), digit }; } @@ -74,16 +69,11 @@ export function makeMicrobitImageDataUri(pattern) { const cell = 16; const gap = 4; const padding = 6; - const size = - MICROBIT_IMAGE_GRID_SIZE * cell + - (MICROBIT_IMAGE_GRID_SIZE - 1) * gap + - 2 * padding; - let squares = ""; + const size = MICROBIT_IMAGE_GRID_SIZE * cell + (MICROBIT_IMAGE_GRID_SIZE - 1) * gap + 2 * padding; + let squares = ''; for (let row = 0; row < MICROBIT_IMAGE_GRID_SIZE; row++) { for (let col = 0; col < MICROBIT_IMAGE_GRID_SIZE; col++) { - const lit = - normalised[row * MICROBIT_IMAGE_GRID_SIZE + col] !== - MICROBIT_IMAGE_OFF; + const lit = normalised[row * MICROBIT_IMAGE_GRID_SIZE + col] !== MICROBIT_IMAGE_OFF; const x = padding + col * (cell + gap); const y = padding + row * (cell + gap); squares += @@ -96,5 +86,5 @@ export function makeMicrobitImageDataUri(pattern) { `` + squares + ``; - return "data:image/svg+xml," + encodeURIComponent(svg); + return 'data:image/svg+xml,' + encodeURIComponent(svg); } diff --git a/blocks/models.js b/blocks/models.js index ea0fb1c57..ce7f8385e 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 6f1ac8c32..4309d612c 100644 --- a/blocks/physics.js +++ b/blocks/physics.js @@ -1,265 +1,261 @@ -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["jump"] = { + Blockly.Blocks['jump'] = { init: function () { this.jsonInit({ - type: "jump", - message0: translate("jump"), + type: 'jump', + message0: translate('jump'), args0: [ { - type: "field_variable", - name: "MODEL_VAR", + type: 'field_variable', + name: 'MODEL_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "JUMP_HEIGHT", - check: "Number", + type: 'input_value', + name: 'JUMP_HEIGHT', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("jump"), + colour: categoryColours['Transform'], + tooltip: getTooltip('jump'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["set_speed"] = { + Blockly.Blocks['set_speed'] = { init: function () { this.jsonInit({ - type: "set_speed", - message0: translate("set_speed"), + type: 'set_speed', + message0: translate('set_speed'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "field_dropdown", - name: "DIRECTION", + type: 'field_dropdown', + name: 'DIRECTION', options: [ - getDropdownOption("forward"), - getDropdownOption("sideways"), - getDropdownOption("up"), - getDropdownOption("x_coordinate"), - getDropdownOption("y_coordinate"), - getDropdownOption("z_coordinate"), - getDropdownOption("all"), + getDropdownOption('forward'), + getDropdownOption('sideways'), + getDropdownOption('up'), + getDropdownOption('x_coordinate'), + getDropdownOption('y_coordinate'), + getDropdownOption('z_coordinate'), + getDropdownOption('all'), ], }, { - 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("set_speed"), + colour: categoryColours['Transform'], + tooltip: getTooltip('set_speed'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("transform_blocks"); + this.setStyle('transform_blocks'); }, }; - Blockly.Blocks["set_bounciness"] = { + Blockly.Blocks['set_bounciness'] = { init: function () { this.jsonInit({ - type: "set_bounciness", - message0: translate("set_bounciness"), + type: 'set_bounciness', + message0: translate('set_bounciness'), args0: [ { - type: "field_variable", - name: "MESH_VAR", + type: 'field_variable', + name: 'MESH_VAR', variable: window.currentMesh, }, { - type: "input_value", - name: "BOUNCINESS", - check: "Number", + type: 'input_value', + name: 'BOUNCINESS', + check: 'Number', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Transform"], - tooltip: getTooltip("set_bounciness"), + colour: categoryColours['Transform'], + tooltip: getTooltip('set_bounciness'), }); 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 fc51da326..1fb86fd36 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_(); @@ -260,8 +258,8 @@ export function respawnContainer(block, opts) { if (refillContainerFromCache(block, opts)) return; const fresh = opts.makeDefault(ws); - if (typeof fresh.initSvg === "function") fresh.initSvg(); - if (typeof fresh.render === "function") fresh.render(); + if (typeof fresh.initSvg === 'function') fresh.initSvg(); + if (typeof fresh.render === 'function') fresh.render(); if (fresh?.outputConnection) { fresh.outputConnection.connect(conn); @@ -286,8 +284,7 @@ export function attachShadowContainerOnChange(block, opts) { 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 === opts.inputName); @@ -309,10 +306,10 @@ export function attachShadowContainerOnChange(block, opts) { // Material-specific wrappers (preserve the original API used by materials.js). const MATERIAL_OPTS = { - inputName: "MATERIAL", - containerType: "material", - cacheKey: "_cachedMaterialState", - makeDefault: (ws) => ws.newBlock("material"), + inputName: 'MATERIAL', + containerType: 'material', + cacheKey: '_cachedMaterialState', + makeDefault: (ws) => ws.newBlock('material'), }; export function cacheMaterialState(mapBlock) { @@ -333,7 +330,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 = [ @@ -348,11 +345,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; @@ -360,7 +356,7 @@ function attachCreateMapOnChange(block) { promoteMaterialContainerFromShadow(block); - if (!block.getInputTargetBlock("MATERIAL")) { + if (!block.getInputTargetBlock('MATERIAL')) { respawnMaterialShadow(block); return; } @@ -375,66 +371,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, }, ], @@ -443,14 +433,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, }, ], @@ -459,14 +449,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, }, ], @@ -474,45 +464,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 7cf5a2cf2..10d2b40d2 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,21 +9,16 @@ 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'; import { makeMicrobitStatusIcon } from './blockIcons.js'; import { getMicrobitManager, VariableStatus } from '../microbit/manager.js'; import { showBanner } from '../ui/notifications.js'; // Gamepad-only accessibility actions have no keyboard equivalent, so they're // excluded from the keyboard-facing action dropdowns below. -const KEYBOARD_ACTIONS = ACTIONS.filter((a) => !a.startsWith("A11Y_")); +const KEYBOARD_ACTIONS = ACTIONS.filter((a) => !a.startsWith('A11Y_')); const MICROBIT_STATUS_FIELD = 'STATUS'; export const MICROBIT_ANY_DEVICE = '__any__'; @@ -52,9 +47,7 @@ function microbitDeviceDropdownOptions(workspace, selectedVariableId) { // when its add_microbit block is gone or hasn't loaded yet โ€” the menu is // derived from add_microbit blocks, but the selection's validity is not. if (selectedVariableId && !seenVariableIds.has(selectedVariableId)) { - const variable = workspace - .getVariableMap() - .getVariableById(selectedVariableId); + const variable = workspace.getVariableMap().getVariableById(selectedVariableId); if (variable) options.push([variable.name, variable.getId()]); } return options; @@ -68,10 +61,7 @@ export class MicrobitDeviceDropdown extends Blockly.FieldDropdown { constructor() { super(function () { const workspace = this.sourceBlock_?.workspace; - return microbitDeviceDropdownOptions( - workspace, - this._candidateValue ?? this.getValue(), - ); + return microbitDeviceDropdownOptions(workspace, this._candidateValue ?? this.getValue()); }); this._candidateValue = null; } @@ -86,9 +76,7 @@ export class MicrobitDeviceDropdown extends Blockly.FieldDropdown { const variable = variableMap?.getVariableById(newValue) ?? // Legacy saves stored the variable name rather than its id. - variableMap - ?.getAllVariables() - .find((candidate) => candidate.name === newValue) ?? + variableMap?.getAllVariables().find((candidate) => candidate.name === newValue) ?? null; if (variable) { // Refresh the cached options with the accepted value so the label @@ -173,10 +161,7 @@ export function isMicrobitRefreshEvent(changeEvent) { return true; } if (changeEvent.type !== Blockly.Events.BLOCK_CHANGE) return false; - return ( - changeEvent.element !== 'field' || - MICROBIT_REFRESH_FIELDS.has(changeEvent.name) - ); + return changeEvent.element !== 'field' || MICROBIT_REFRESH_FIELDS.has(changeEvent.name); } let refreshingMicrobitBlocks = false; @@ -250,8 +235,7 @@ export function refreshMicrobitBlocks(workspace) { const variable = workspace.getVariableMap().getVariableById(device); tethered = !!variable && - manager.getStatusForVariable(variable.name).state === - VariableStatus.TETHERED; + manager.getStatusForVariable(variable.name).state === VariableStatus.TETHERED; } block.setWarningText(tethered ? null : translate(warningKey)); } @@ -285,449 +269,446 @@ function wireMicrobitManager() { } 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: KEYBOARD_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: KEYBOARD_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("SPEED_X"), - getDropdownOption("SPEED_Y"), - getDropdownOption("SPEED_Z"), - getDropdownOption("SPEED"), - 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('SPEED_X'), + getDropdownOption('SPEED_Y'), + getDropdownOption('SPEED_Z'), + getDropdownOption('SPEED'), + 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'); }, }; @@ -790,55 +771,55 @@ export function defineSensingBlocks() { }, }; - 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'); // Device dropdown (dynamic_mesh_dropdown style): "any" plus the // micro:bit variables defined via add_microbit blocks. Appended outside // message0 so legacy XML โ€” which has no DEVICE field โ€” loads unchanged // and defaults to "any". - this.inputList[0].appendField(new MicrobitDeviceDropdown(), "DEVICE"); + this.inputList[0].appendField(new MicrobitDeviceDropdown(), 'DEVICE'); syncMicrobitDeviceField(this); this.setOnChange((changeEvent) => { if ( @@ -864,82 +845,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 7eedb111b..831ac1966 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 f67d3bdbf..31265fc80 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'; } } @@ -143,25 +133,37 @@ const ABC_MAX_INPUT_LENGTH = 20000; 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; } @@ -171,24 +173,32 @@ function abcParseFraction(str) { // bar. A trailing - marks a syllable that joins the next one into one word. function abcParseLyrics(raw) { const events = []; - let syllable = ""; + let syllable = ''; const flush = (joinNext) => { if (!syllable) return; - events.push({ t: "syl", text: syllable, joinNext }); - syllable = ""; + events.push({ t: 'syl', text: syllable, joinNext }); + syllable = ''; }; for (let i = 0; i < raw.length; i++) { const c = raw[i]; - if (c === "\\" && raw[i + 1] === "-") { syllable += "-"; i++; } - else if (/\s/.test(c)) flush(false); - else if (c === "-") flush(true); - else if (c === "~") syllable += " "; - else if (c === "*") { flush(false); events.push({ t: "skip" }); } - else if (c === "_") { flush(false); events.push({ t: "hold" }); } - else if (c === "|") { flush(false); events.push({ t: "bar" }); } - else syllable += c; + if (c === '\\' && raw[i + 1] === '-') { + syllable += '-'; + i++; + } else if (/\s/.test(c)) flush(false); + else if (c === '-') flush(true); + else if (c === '~') syllable += ' '; + else if (c === '*') { + flush(false); + events.push({ t: 'skip' }); + } else if (c === '_') { + flush(false); + events.push({ t: 'hold' }); + } else if (c === '|') { + flush(false); + events.push({ t: 'bar' }); + } else syllable += c; } flush(false); @@ -201,15 +211,15 @@ function abcAssignLyrics(lineTokens, events) { let idx = 0; for (const ev of events) { - if (ev.t === "bar") { - while (idx < lineTokens.length && lineTokens[idx].t !== "bar") idx++; + if (ev.t === 'bar') { + while (idx < lineTokens.length && lineTokens[idx].t !== 'bar') idx++; idx++; continue; } - while (idx < lineTokens.length && lineTokens[idx].t !== "note") idx++; + while (idx < lineTokens.length && lineTokens[idx].t !== 'note') idx++; const note = lineTokens[idx]; if (!note) return; - if (ev.t === "syl") { + if (ev.t === 'syl') { note.syl = ev.text; note.joinNext = ev.joinNext; } @@ -218,9 +228,9 @@ function abcAssignLyrics(lineTokens, events) { } 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 = {}; @@ -230,31 +240,29 @@ function parseAbc(abcText) { for (const line of lines) { const t = line.trim(); - if (!t || t.startsWith("%")) continue; + if (!t || t.startsWith('%')) continue; const hm = t.match(/^[a-z]*([A-Za-z]):(.*)/); 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 === "w") { + if (key === 'T') title = v; + else if (key === 'C') composer = v; + else if (key === 'L') lFraction = abcParseFraction(v); + else if (key === 'w') { // Aligned lyrics belong to the music line just above them; each // further w: line under the same music line is another verse. const target = musicLines[musicLines.length - 1]; const events = abcParseLyrics(v); if (target && events.length > 0) target.verses.push(events); - } else if (key === "W") { + } else if (key === 'W') { const events = abcParseLyrics(v); if (events.length > 0) unalignedLyrics.push(events); - } else if (key === "Q") { + } else if (key === 'Q') { const qm = v.match(/(\d+)$/); if (qm) bpm = parseInt(qm[1]); - } else if (key === "K") { - const keyName = v.split(" ")[0]; - keyAcc = Object.hasOwn(ABC_KEY_ACCIDENTALS, keyName) - ? ABC_KEY_ACCIDENTALS[keyName] - : {}; + } else if (key === 'K') { + const keyName = v.split(' ')[0]; + keyAcc = Object.hasOwn(ABC_KEY_ACCIDENTALS, keyName) ? ABC_KEY_ACCIDENTALS[keyName] : {}; } } else { // [V:2] at the head of a line switches voice, and holds until the next @@ -267,9 +275,7 @@ function parseAbc(abcText) { const melodyVoice = musicLines.find((l) => l.voice !== null)?.voice ?? null; const melody = - melodyVoice === null - ? musicLines - : musicLines.filter((l) => l.voice === melodyVoice); + melodyVoice === null ? musicLines : musicLines.filter((l) => l.voice === melodyVoice); // W: lines have no alignment of their own, so fall back to one per music // line in order โ€” only when the tune used no aligned lyrics at all. @@ -284,7 +290,7 @@ function parseAbc(abcText) { const verseCount = Math.max(1, ...melody.map((l) => l.verses.length)); // One pass per verse: same notes, different words on them. const verses = Array.from({ length: verseCount }, (_, verseIdx) => - abcParseSections(melody, noteBase, keyAcc, verseIdx), + abcParseSections(melody, noteBase, keyAcc, verseIdx) ); return { title: fullTitle, bpm, verses }; } @@ -297,54 +303,81 @@ function abcParseSections(musicLines, noteBase, keyAcc, verseIdx = 0) { const verses = musicLines[lineIdx].verses; // A verse whose line is all skips (w:*****) is the chorus: the words are // written out once, under the first verse, and sung again each time. - const sung = (events) => events?.some((e) => e.t === "syl"); + const sung = (events) => events?.some((e) => e.t === 'syl'); const events = sung(verses[verseIdx]) ? verses[verseIdx] : verses[0]; - const text = musicLines[lineIdx].text.replace(/%.*/, ""); + const text = musicLines[lineIdx].text.replace(/%.*/, ''); const lineStart = tokens.length; let i = 0; while (i < text.length) { const c = text[i]; - if (/\s/.test(c)) { i++; continue; } + if (/\s/.test(c)) { + i++; + continue; + } // Chord symbols ("G") and decorations (!f!) hold letters that would // otherwise be read as notes, so skip them whole. - if (c === '"' || c === "!" || c === "+") { + if (c === '"' || c === '!' || c === '+') { const end = text.indexOf(c, i + 1); i = end === -1 ? text.length : end + 1; 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 if (/^\[[A-Za-z]:/.test(text.slice(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 if (/^\[[A-Za-z]:/.test(text.slice(i))) { // Inline field such as [V:1] โ€” not music - const end = text.indexOf("]", i); + const end = text.indexOf(']', i); i = end === -1 ? text.length : end + 1; - } else { i++; } + } else { + i++; + } } else { const sub = text.slice(i); const m = sub.match(/^([_^=]*)([A-Ga-gz])([',]*)(\d*)(\/\d*)?/); @@ -352,32 +385,34 @@ function abcParseSections(musicLines, noteBase, keyAcc, verseIdx = 0) { 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, lyricId: lineIdx }); + tokens.push({ t: 'note', base, midi, beats, hasAcc, acc, lyricId: lineIdx }); } i += full.length; - } else { i++; } + } else { + i++; + } } } @@ -389,7 +424,7 @@ function abcParseSections(musicLines, noteBase, keyAcc, verseIdx = 0) { let currentBars = []; let currentBar = []; let barAcc = {}; - let state = "plain"; + let state = 'plain'; let repeatBars = null; let volta1Bars = null; @@ -423,15 +458,15 @@ function abcParseSections(musicLines, noteBase, keyAcc, verseIdx = 0) { 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], @@ -439,29 +474,29 @@ function abcParseSections(musicLines, noteBase, keyAcc, verseIdx = 0) { 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 = []; @@ -469,99 +504,100 @@ function abcParseSections(musicLines, noteBase, keyAcc, verseIdx = 0) { // 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; } @@ -572,8 +608,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(); @@ -588,8 +624,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); @@ -606,19 +642,19 @@ function propagateInstrument(block, instrumentState) { } function makeTextBlock(ws, value) { - const b = ws.newBlock("text"); - b.setFieldValue(value, "TEXT"); + const b = ws.newBlock('text'); + b.setFieldValue(value, 'TEXT'); b.initSvg(); b.render(); return b; } function buildSubtitleBlock(ws, words, seconds) { - const b = ws.newBlock("subtitle"); + const b = ws.newBlock('subtitle'); b.initSvg(); b.render(); - b.getInput("TEXT").connection.connect(makeTextBlock(ws, words).outputConnection); - b.getInput("DURATION").connection.connect(makeNumBlock(ws, seconds).outputConnection); + b.getInput('TEXT').connection.connect(makeTextBlock(ws, words).outputConnection); + b.getInput('DURATION').connection.connect(makeNumBlock(ws, seconds).outputConnection); return b; } @@ -630,12 +666,12 @@ const TAIL_CAPTION_WORDS = 3; // Join the syllables sung across a run of bars back into words: a syllable // marked joinNext runs straight into the one after it (mid-word hyphen). function barsCaption(bars) { - let text = ""; + let text = ''; let join = false; for (const bar of bars) { for (const note of bar) { if (!note.syl) continue; - if (text && !join) text += " "; + if (text && !join) text += ' '; text += note.syl; join = note.joinNext; } @@ -645,7 +681,7 @@ function barsCaption(bars) { function captionWordCount(bars) { const caption = barsCaption(bars); - return caption ? caption.split(" ").length : 0; + return caption ? caption.split(' ').length : 0; } // Build the statement chain for a run of bars, split into subtitle + play @@ -695,7 +731,7 @@ function buildPlayChain(ws, bars, meshName, speed) { if (caption && speed > 0) { const beats = group.bars.reduce( (total, bar) => total + bar.reduce((sum, note) => sum + note.beats, 0), - 0, + 0 ); const seconds = Math.round((beats / speed) * 1000) / 1000; head = buildSubtitleBlock(ws, caption, seconds); @@ -711,8 +747,8 @@ function buildPlayChain(ws, bars, meshName, speed) { } 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; @@ -722,84 +758,84 @@ function makeNumBlock(ws, value) { // carrying that verse's words, so the tune body is laid out once per verse. function abcBuildDoBlocks(ws, doInput, verses, bpm, meshName) { const hasBars = verses[0]?.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 verses.flat()) { - if (section.type === "plain") { + if (section.type === 'plain') { const chain = buildPlayChain(ws, section.bars, meshName, speed); if (!chain) continue; prevNext.connect(chain.first.previousConnection); prevNext = chain.last.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 chain = buildPlayChain(ws, section.bars, meshName, speed); if (chain) { - repeatBlock.getInput("DO").connection.connect(chain.first.previousConnection); + repeatBlock.getInput('DO').connection.connect(chain.first.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 firstChain = buildPlayChain(ws, section.firstBars, meshName, speed); if (firstChain) { - ifBlock.getInput("DO0").connection.connect(firstChain.first.previousConnection); + ifBlock.getInput('DO0').connection.connect(firstChain.first.previousConnection); } const secondChain = buildPlayChain(ws, section.secondBars, meshName, speed); if (secondChain) { - ifBlock.getInput("ELSE").connection.connect(secondChain.first.previousConnection); + ifBlock.getInput('ELSE').connection.connect(secondChain.first.previousConnection); } const commonChain = buildPlayChain(ws, section.commonBars, meshName, speed); @@ -807,7 +843,7 @@ function abcBuildDoBlocks(ws, doInput, verses, bpm, meshName) { if (commonChain) { commonChain.last.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; @@ -816,305 +852,289 @@ function abcBuildDoBlocks(ws, doInput, verses, 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 } }, }, }, }, @@ -1124,217 +1144,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( @@ -1342,61 +1360,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); @@ -1406,21 +1424,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); @@ -1435,10 +1453,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); @@ -1456,62 +1474,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; @@ -1519,12 +1547,21 @@ export function defineSoundBlocks() { importAbc: function (abcText) { if (abcText.length > ABC_MAX_INPUT_LENGTH) { - console.warn("[play_tune] ABC input too long, not importing:", abcText.length); - announceToScreenReader("tune too long to import"); + console.warn('[play_tune] ABC input too long, not importing:', abcText.length); + announceToScreenReader('tune too long to import'); return; } const parsed = parseAbc(abcText); - console.log("[play_tune] importing:", parsed.title, "verses:", parsed.verses.length, "sections:", parsed.verses[0].length, "bpm:", parsed.bpm); + console.log( + '[play_tune] importing:', + parsed.title, + 'verses:', + parsed.verses.length, + 'sections:', + parsed.verses[0].length, + 'bpm:', + parsed.bpm + ); Blockly.Events.setGroup(true); try { @@ -1533,8 +1570,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); @@ -1542,7 +1579,7 @@ export function defineSoundBlocks() { try { abcBuildDoBlocks(ws, doInput, parsed.verses, parsed.bpm, meshName); } catch (e) { - console.error("[play_tune] DO block creation failed:", e); + console.error('[play_tune] DO block creation failed:', e); } } } finally { @@ -1553,91 +1590,91 @@ 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", + type: 'field_dropdown', + name: 'LANGUAGE', // Bare "es" so voice matching (v.lang.startsWith) accepts es-ES, // es-MX and the rest, not just Spain. options: [ - getDropdownOption("en-GB"), - getDropdownOption("en-US"), - getDropdownOption("es"), + getDropdownOption('en-GB'), + getDropdownOption('en-US'), + getDropdownOption('es'), ], }, { - 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'); }, }; - Blockly.Blocks["enable_subtitles"] = { + Blockly.Blocks['enable_subtitles'] = { init: function () { this.jsonInit({ - type: "enable_subtitles", - message0: translate("enable_subtitles"), + type: 'enable_subtitles', + message0: translate('enable_subtitles'), args0: [ { - type: "field_checkbox", - name: "ENABLED", + type: 'field_checkbox', + name: 'ENABLED', checked: true, }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Sound"], - tooltip: getTooltip("enable_subtitles"), + colour: categoryColours['Sound'], + tooltip: getTooltip('enable_subtitles'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("sound_blocks"); + this.setStyle('sound_blocks'); }, }; } diff --git a/blocks/text.js b/blocks/text.js index 7e8d08d19..1c1a6d74e 100644 --- a/blocks/text.js +++ b/blocks/text.js @@ -1,263 +1,258 @@ -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"; -import { attachShadowContainerOnChange } from "./scene.js"; +} from './blocks.js'; +import { translate, getTooltip, getDropdownOption } from '../main/translation.js'; +import { attachShadowContainerOnChange } from './scene.js'; // print_text keeps a text_join in its TEXT input as a "shadow container": it is // promoted to a real block on the canvas (so blocks can be dropped into its // items) and respawned when dragged out. See attachShadowContainerOnChange. const PRINT_TEXT_LIST_OPTS = { - inputName: "TEXT", - containerType: "text_join", - cacheKey: "_cachedTextListState", + inputName: 'TEXT', + containerType: 'text_join', + cacheKey: '_cachedTextListState', makeDefault: (ws) => { - const list = ws.newBlock("text_join"); + const list = ws.newBlock('text_join'); if (list.loadExtraState) list.loadExtraState({ itemCount: 1 }); list.setInputsInline(true); - const item = ws.newBlock("text"); + const item = ws.newBlock('text'); item.setShadow(true); - item.setFieldValue("Hello ๐ŸŒˆ", "TEXT"); - if (typeof item.initSvg === "function") item.initSvg(); - const add0 = list.getInput("ADD0"); + item.setFieldValue('Hello ๐ŸŒˆ', 'TEXT'); + if (typeof item.initSvg === 'function') item.initSvg(); + const add0 = list.getInput('ADD0'); if (add0?.connection) add0.connection.connect(item.outputConnection); return list; }, }; 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'); attachShadowContainerOnChange(this, PRINT_TEXT_LIST_OPTS); }, }; - Blockly.Blocks["subtitle"] = { + Blockly.Blocks['subtitle'] = { init: function () { this.jsonInit({ - type: "subtitle", - message0: translate("subtitle"), + type: 'subtitle', + message0: translate('subtitle'), 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', }, ], // Label each input explicitly so the message words read well. - ariaLabels: { TEXT: "text", DURATION: "seconds" }, + ariaLabels: { TEXT: 'text', DURATION: 'seconds' }, inputsInline: true, previousStatement: null, nextStatement: null, colour: 160, - tooltip: getTooltip("subtitle"), + tooltip: getTooltip('subtitle'), }); 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', }, { - 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', }, ], 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( @@ -265,80 +260,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( @@ -346,76 +340,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( @@ -423,108 +416,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/text_join_shadow.js b/blocks/text_join_shadow.js index cbab0350f..d1791bdf5 100644 --- a/blocks/text_join_shadow.js +++ b/blocks/text_join_shadow.js @@ -8,21 +8,21 @@ * registration, and before any workspace/blocks are created. */ -import * as Blockly from "blockly"; -import { createPlusField } from "@blockly/block-plus-minus/src/field_plus.js"; -import { createMinusField } from "@blockly/block-plus-minus/src/field_minus.js"; +import * as Blockly from 'blockly'; +import { createPlusField } from '@blockly/block-plus-minus/src/field_plus.js'; +import { createMinusField } from '@blockly/block-plus-minus/src/field_minus.js'; const textJoinMutator = { itemCount_: 0, mutationToDom: function () { - const container = Blockly.utils.xml.createElement("mutation"); - container.setAttribute("items", this.itemCount_); + const container = Blockly.utils.xml.createElement('mutation'); + container.setAttribute('items', this.itemCount_); return container; }, domToMutation: function (xmlElement) { - const targetCount = parseInt(xmlElement.getAttribute("items"), 10); + const targetCount = parseInt(xmlElement.getAttribute('items'), 10); this.updateShape_(targetCount); }, @@ -33,7 +33,7 @@ const textJoinMutator = { }, loadExtraState: function (state) { - this.updateShape_(state["itemCount"]); + this.updateShape_(state['itemCount']); }, updateShape_: function (targetCount) { @@ -53,9 +53,9 @@ const textJoinMutator = { */ plus: function () { this.addPart_(); - const input = this.getInput("ADD" + (this.itemCount_ - 1)); + const input = this.getInput('ADD' + (this.itemCount_ - 1)); if (input && input.connection && !input.connection.targetBlock()) { - const shadow = this.workspace.newBlock("text"); // TEXT defaults to "" + const shadow = this.workspace.newBlock('text'); // TEXT defaults to "" shadow.setShadow(true); shadow.initSvg(); shadow.render(); @@ -74,14 +74,14 @@ const textJoinMutator = { addPart_: function () { if (this.itemCount_ == 0) { - if (this.getInput("EMPTY")) { - this.removeInput("EMPTY"); + if (this.getInput('EMPTY')) { + this.removeInput('EMPTY'); } - this.topInput_ = this.appendValueInput("ADD" + this.itemCount_) - .appendField(createPlusField(), "PLUS") - .appendField(Blockly.Msg["TEXT_JOIN_TITLE_CREATEWITH"]); + this.topInput_ = this.appendValueInput('ADD' + this.itemCount_) + .appendField(createPlusField(), 'PLUS') + .appendField(Blockly.Msg['TEXT_JOIN_TITLE_CREATEWITH']); } else { - this.appendValueInput("ADD" + this.itemCount_); + this.appendValueInput('ADD' + this.itemCount_); } // Because item inputs are 0-index we decrement first, increment last. this.itemCount_++; @@ -89,35 +89,31 @@ const textJoinMutator = { removePart_: function () { this.itemCount_--; - this.removeInput("ADD" + this.itemCount_); + this.removeInput('ADD' + this.itemCount_); if (this.itemCount_ == 0) { - this.topInput_ = this.appendDummyInput("EMPTY") - .appendField(createPlusField(), "PLUS") + this.topInput_ = this.appendDummyInput('EMPTY') + .appendField(createPlusField(), 'PLUS') .appendField(this.newQuote_(true)) .appendField(this.newQuote_(false)); } }, updateMinus_: function () { - const minusField = this.getField("MINUS"); + const minusField = this.getField('MINUS'); if (!minusField && this.itemCount_ > 0) { - this.topInput_.insertFieldAt(1, createMinusField(), "MINUS"); + this.topInput_.insertFieldAt(1, createMinusField(), 'MINUS'); } else if (minusField && this.itemCount_ < 1) { - this.topInput_.removeField("MINUS"); + this.topInput_.removeField('MINUS'); } }, }; const textJoinHelper = function () { - Blockly.Extensions.apply("quote_image_mixin", this, false); + Blockly.Extensions.apply('quote_image_mixin', this, false); this.updateShape_(2); }; -if (Blockly.Extensions.isRegistered("text_join_mutator")) { - Blockly.Extensions.unregister("text_join_mutator"); +if (Blockly.Extensions.isRegistered('text_join_mutator')) { + Blockly.Extensions.unregister('text_join_mutator'); } -Blockly.Extensions.registerMutator( - "text_join_mutator", - textJoinMutator, - textJoinHelper, -); +Blockly.Extensions.registerMutator('text_join_mutator', textJoinMutator, textJoinHelper); diff --git a/blocks/transform.js b/blocks/transform.js index 3c8e196f7..116f95775 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 6ec72050c..3b5895d1c 100644 --- a/blocks/xr.js +++ b/blocks/xr.js @@ -1,17 +1,13 @@ -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 { syncMicrobitDeviceField, refreshMicrobitBlocks, isMicrobitRefreshEvent, -} from "./sensing.js"; -import "./fieldMicrobitImage.js"; // registers field_microbit_image +} from './sensing.js'; +import './fieldMicrobitImage.js'; // registers field_microbit_image // Shared by the micro:bit display blocks (show image / scroll text): keep // the device menu in sync with variable renames/deletes and the @@ -34,227 +30,224 @@ function wireMicrobitDeviceBlock(block) { } export function defineXRBlocks() { - Blockly.Blocks["microbit_show_image"] = { + Blockly.Blocks['microbit_show_image'] = { init: function () { this.jsonInit({ - type: "microbit_show_image", - message0: translate("microbit_show_image"), + type: 'microbit_show_image', + message0: translate('microbit_show_image'), args0: [ { // Same device menu as microbit_input: "any" plus the variables // defined by add_microbit blocks. - type: "field_microbit_device", - name: "DEVICE", + type: 'field_microbit_device', + name: 'DEVICE', }, { - type: "field_microbit_image", - name: "IMAGE", + type: 'field_microbit_image', + name: 'IMAGE', }, ], previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("microbit_show_image"), + colour: categoryColours['Scene'], + tooltip: getTooltip('microbit_show_image'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); wireMicrobitDeviceBlock(this); }, }; - Blockly.Blocks["microbit_scroll_text"] = { + Blockly.Blocks['microbit_scroll_text'] = { init: function () { this.jsonInit({ - type: "microbit_scroll_text", - message0: translate("microbit_scroll_text"), + type: 'microbit_scroll_text', + message0: translate('microbit_scroll_text'), args0: [ { - type: "field_microbit_device", - name: "DEVICE", + type: 'field_microbit_device', + name: 'DEVICE', }, { - type: "input_value", - name: "TEXT", - check: "String", + type: 'input_value', + name: 'TEXT', + check: 'String', }, ], inputsInline: true, previousStatement: null, nextStatement: null, - colour: categoryColours["Scene"], - tooltip: getTooltip("microbit_scroll_text"), + colour: categoryColours['Scene'], + tooltip: getTooltip('microbit_scroll_text'), }); this.setHelpUrl(getHelpUrlFor(this.type)); - this.setStyle("scene_blocks"); + this.setStyle('scene_blocks'); wireMicrobitDeviceBlock(this); }, }; - 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 df8db19c8..e5cd90cf5 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,135 +71,133 @@ 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', ]; 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", - "boardwalk_straight.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', + 'boardwalk_straight.glb', + 'flower.glb', + 'flower2.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) { @@ -215,262 +212,237 @@ 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", + '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', ], - "boat.glb": [ - "#4F8A46", - "#E7D48E", - "#E76635", - "#E76C69", - "#5E64E7", - "#4A4A4A", - "#AAAAAA", - "#E711CD", - ], - - "airplane.glb": ["#E75D43", "#6A6A6A", "#E7C777", "#979797", "#A033E7"], + 'airplane.glb': ['#E75D43', '#6A6A6A', '#E7C777', '#979797', '#A033E7'], - "airplane2.glb": ["#E75D43", "#6A6A6A", "#E7C777", "#979797", "#A033E7"], + 'airplane2.glb': ['#E75D43', '#6A6A6A', '#E7C777', '#979797', '#A033E7'], - "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", "#E76F31", "#7E5024"], - "boardwalk_straight.glb": ["#7E5024", "#7E5024", "#7E5024"], - "jetty.glb": ["#FFA869", "#7E5024", "#E76F31"], - "flower.glb": ["#E73F9F", "#4AB700", "#E7D535"], - "flower2.glb": ["#7C38E7", "#4AB700", "#E7D535"], + 'humped.glb': ['#FFA869', '#E76F31', '#7E5024'], + 'boardwalk_straight.glb': ['#7E5024', '#7E5024', '#7E5024'], + 'jetty.glb': ['#FFA869', '#7E5024', '#E76F31'], + 'flower.glb': ['#E73F9F', '#4AB700', '#E7D535'], + 'flower2.glb': ['#7C38E7', '#4AB700', '#E7D535'], }; -export const modelNames = [ - "Flock.glb", - "lion.glb", - "rhino.glb", -]; +export const modelNames = ['Flock.glb', 'lion.glb', 'rhino.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', ]; export const modelAnimationNames = [ - "Flock.glb", - "rhino.glb", - "lion.glb", - "airplane.glb", - "airplane2.glb", + 'Flock.glb', + 'rhino.glb', + 'lion.glb', + 'airplane.glb', + 'airplane2.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:HeadTop_End", - 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:HeadTop_End', + 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 199e8994c..d196c2036 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 262aa81ee..78fbfe330 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 c48ff0d24..9ee21769f 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/endless.md b/docs/endless.md index 76547fd98..88b994f1d 100644 --- a/docs/endless.md +++ b/docs/endless.md @@ -23,8 +23,9 @@ The latest release version of Flock XR can be found by going to https://app.floc From both Chromium and Epiphany, on Endless OS, it is possible to install Flock XR as a web application. This app can then be used without an Internet connection, as shown in the following screenshot. ![Flock XR running in a web app with flight mode on](images/EndlessWebApp.png) + ## Testing Flock XR -Flock XRโ€™s basic functionality has been tested on Endless OS using both of the browsers which come pre-installed with the distribution. That is, we tested Flock on Chromium and on GNOME Web (Epiphany). We found that Flock XR functioned well on both, with better compatibility being found on Chromium, so we recommend using Chromium to run Flock XR on Endless OS. +Flock XRโ€™s basic functionality has been tested on Endless OS using both of the browsers which come pre-installed with the distribution. That is, we tested Flock on Chromium and on GNOME Web (Epiphany). We found that Flock XR functioned well on both, with better compatibility being found on Chromium, so we recommend using Chromium to run Flock XR on Endless OS. If you find any issues using Flock XR on Endless OS, let us know at [Flock XR](https://flockxr.com/). diff --git a/docs/screen-reader.md b/docs/screen-reader.md index 8118f230f..284d1a8e6 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 1beb9abd7..b122871d8 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 1be2ec509..6e9a170b8 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/flock.js b/flock.js index ece397130..a597697e9 100644 --- a/flock.js +++ b/flock.js @@ -53,7 +53,10 @@ import { flockMesh, setFlockReference as setFlockMesh } from './api/mesh'; import { flockCamera, setFlockReference as setFlockCamera } from './api/camera'; import { flockEvents, setFlockReference as setFlockEvents } from './api/events'; import { flockMicrobit, setFlockReference as setFlockMicrobit } from './api/microbit'; -import { getMicrobitManager, setFlockReference as setFlockMicrobitManager } from './microbit/manager.js'; +import { + getMicrobitManager, + setFlockReference as setFlockMicrobitManager, +} from './microbit/manager.js'; import { flockMath, setFlockReference as setFlockMath } from './api/math'; import { flockSensing, setFlockReference as setFlockSensing } from './api/sensing'; import { translate } from './main/translation.js'; @@ -190,8 +193,7 @@ export const flock = { isDebugLoggingEnabled() { if (flock._debugLogging === undefined) { try { - flock._debugLogging = - new URLSearchParams(window.location.search).get('debug') !== 'false'; + flock._debugLogging = new URLSearchParams(window.location.search).get('debug') !== 'false'; } catch { flock._debugLogging = true; } @@ -246,11 +248,8 @@ export const flock = { }; // A thrown primitive (e.g. `throw "bad input"`) has no .message, so use the // value itself rather than losing the text. - const isPrimitive = - error == null || (typeof error !== 'object' && typeof error !== 'function'); - const safe = new Error( - isPrimitive ? read(() => error) : read(() => error?.message) - ); + const isPrimitive = error == null || (typeof error !== 'object' && typeof error !== 'function'); + const safe = new Error(isPrimitive ? read(() => error) : read(() => error?.message)); safe.name = isPrimitive ? 'Error' : read(() => error?.name) || 'Error'; safe.stack = isPrimitive ? '' : read(() => error?.stack); return safe; @@ -833,8 +832,7 @@ export const flock = { // `.constructor` (sandbox escape). Must run before lockdown. const wrapScript = doc.createElement('script'); wrapScript.type = 'text/javascript'; - wrapScript.text = - 'window.__flockWrapHostFn = (fn) => (...args) => fn(...args);'; + wrapScript.text = 'window.__flockWrapHostFn = (fn) => (...args) => fn(...args);'; doc.head.appendChild(wrapScript); // Lock down the iframe realm. Disable SES's own unhandled-rejection @@ -894,7 +892,7 @@ export const flock = { // Wrapped so the endowment carries no host `.constructor`. const hostRequestAnimationFrame = window.requestAnimationFrame.bind(window); endowments.requestAnimationFrame = win.__flockWrapHostFn((callback) => - hostRequestAnimationFrame(guard(callback)), + hostRequestAnimationFrame(guard(callback)) ); endowments.Date = new win.Object(); diff --git a/generators/generators-animate.js b/generators/generators-animate.js index 13016792d..9becba8b3 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"); - return [JSON.stringify(animationName ?? ""), javascriptGenerator.ORDER_ATOMIC]; + javascriptGenerator.forBlock['animation_name'] = function (block) { + const animationName = block.getFieldValue('ANIMATION_NAME'); + return [JSON.stringify(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 dc8787c1e..6e47ea624 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 82fb9f090..edf006a00 100644 --- a/generators/generators-control.js +++ b/generators/generators-control.js @@ -1,95 +1,75 @@ -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; } // Yield a real render frame per iteration (not setTimeout, which spins many // times per frame): guarantees exactly one physics/render step before the @@ -97,37 +77,34 @@ export function registerControlGenerators(javascriptGenerator) { // advance reliably without the user adding a manual wait. Still stoppable โ€” // rAF yields to the event loop just like a timer. return ( - "while (" + + 'while (' + argument0 + - ") {\n" + + ') {\n' + branch + `\nawait new Promise(resolve => requestAnimationFrame(resolve));\n` + - "}\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 ` @@ -145,54 +122,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; }; @@ -201,11 +159,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 @@ -214,13 +172,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 66ef97ef2..c7f3fc984 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 374e17ba0..1e91b5712 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 = JSON.stringify(meshNameField); 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 23b0a6ca8..dcde8eb45 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 706065477..ebdfb7c12 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 1028d4603..e2596d90c 100644 --- a/generators/generators-material.js +++ b/generators/generators-material.js @@ -1,91 +1,91 @@ -import * as Blockly from "blockly"; -import { getFieldValue } from "./generators-utilities.js"; -import { isValidColourInput } from "../blocks/colourvalidation.js"; +import * as Blockly from 'blockly'; +import { getFieldValue } from './generators-utilities.js'; +import { isValidColourInput } from '../blocks/colourvalidation.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`; }; // Set shadow casting of object ------------------------------- - javascriptGenerator.forBlock["set_shadow"] = function (block) { + javascriptGenerator.forBlock['set_shadow'] = function (block) { const modelName = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH'), + Blockly.Names.NameType.VARIABLE ); - const cast = block.getFieldValue("CAST") === "TRUE"; + const cast = block.getFieldValue('CAST') === 'TRUE'; return `await setShadow(${modelName}, { cast: ${cast} });\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]; }; @@ -95,23 +95,23 @@ 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(); if (!isValidColourInput(rawColourValue)) { - return [JSON.stringify("#000000"), javascriptGenerator.ORDER_ATOMIC]; + return [JSON.stringify('#000000'), javascriptGenerator.ORDER_ATOMIC]; } - const hexBody = rawColourValue.replace(/^#/, ""); + const hexBody = rawColourValue.replace(/^#/, ''); let colourValue; if (/^[0-9a-fA-F]{3}$/.test(hexBody)) { colourValue = `#${hexBody - .split("") + .split('') .map((component) => component + component) - .join("") + .join('') .toLowerCase()}`; } else if (/^[0-9a-fA-F]{6}$/.test(hexBody)) { colourValue = `#${hexBody}`; @@ -122,39 +122,28 @@ export function registerMaterialGenerators(javascriptGenerator) { }; // 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. @@ -169,27 +158,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 5a7ac13e1..fc44dc811 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 97bf18d45..c38c4a7f0 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: ${JSON.stringify(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: ${JSON.stringify(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: ${JSON.stringify(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,200 +324,142 @@ 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`; }; // Enable shadows ------------------------------------------------- - javascriptGenerator.forBlock["enable_shadows"] = function (block) { - const enabled = block.getFieldValue("ENABLED") === "TRUE"; + javascriptGenerator.forBlock['enable_shadows'] = function (block) { + const enabled = block.getFieldValue('ENABLED') === 'TRUE'; return `enableShadows({ enabled: ${enabled} });\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 { userVariableName: emitterMeshName } = getVariableInfo( - block, - "EMITTER_MESH", - ); + const { 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 = ` { @@ -577,38 +500,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`; }; @@ -617,38 +528,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`; }; @@ -657,26 +564,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 e13259455..4d3b0559b 100644 --- a/generators/generators-sensing.js +++ b/generators/generators-sensing.js @@ -1,5 +1,5 @@ -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) { // ------------------------------- diff --git a/generators/generators-sound.js b/generators/generators-sound.js index 4aebcfa49..cbcdbbbaf 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 meshName = JSON.stringify(meshNameField ?? "__everywhere__"); + const meshNameField = block.getFieldValue('MESH_NAME'); + const meshName = JSON.stringify(meshNameField ?? '__everywhere__'); - 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 meshName = JSON.stringify(meshNameField ?? "__everywhere__"); + const meshNameField = block.getFieldValue('MESH_NAME'); + const meshName = JSON.stringify(meshNameField ?? '__everywhere__'); - 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"); - const meshName = JSON.stringify(meshNameField ?? "__everywhere__"); + javascriptGenerator.forBlock['play_tune_notes'] = function (block) { + const meshNameField = block.getFieldValue('MESH_NAME'); + const meshName = JSON.stringify(meshNameField ?? '__everywhere__'); 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"); - const meshName = JSON.stringify(meshNameField ?? "__everywhere__"); + javascriptGenerator.forBlock['set_music_speed'] = function (block) { + const meshNameField = block.getFieldValue('MESH_NAME'); + const meshName = JSON.stringify(meshNameField ?? '__everywhere__'); 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,117 +164,73 @@ 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 = JSON.stringify(meshValue ?? "__everywhere__"); + const meshVariable = JSON.stringify(meshValue ?? '__everywhere__'); // 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`; }; - javascriptGenerator.forBlock["enable_subtitles"] = function (block) { - const enabled = block.getFieldValue("ENABLED") === "TRUE"; + javascriptGenerator.forBlock['enable_subtitles'] = function (block) { + const enabled = block.getFieldValue('ENABLED') === 'TRUE'; return `enableSubtitles({ enabled: ${enabled} });\n`; }; } diff --git a/generators/generators-text.js b/generators/generators-text.js index 503bd0c1d..c03f2f7eb 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,19 +26,11 @@ export function registerTextGenerators(javascriptGenerator) { }; // Subtitle ----------------------------------------------- - javascriptGenerator.forBlock["subtitle"] = function (block) { + javascriptGenerator.forBlock['subtitle'] = 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 safeTextArg = emitSafeTextArg(textCode); @@ -54,43 +38,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); @@ -98,59 +66,31 @@ 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 backgroundColorCode = javascriptGenerator.valueToCode( block, - "BACKGROUND_COLOR", - javascriptGenerator.ORDER_ATOMIC, + 'BACKGROUND_COLOR', + javascriptGenerator.ORDER_ATOMIC ) || '"#ffffff"'; const alphaCode = - javascriptGenerator.valueToCode( - block, - "ALPHA", - javascriptGenerator.ORDER_ATOMIC, - ) || "1"; + javascriptGenerator.valueToCode(block, 'ALPHA', javascriptGenerator.ORDER_ATOMIC) || '1'; 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); @@ -169,40 +109,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()}`; @@ -221,50 +149,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); @@ -281,55 +190,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 = ` @@ -357,17 +237,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; @@ -376,17 +252,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); @@ -395,38 +267,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}, @@ -435,7 +303,7 @@ export function registerTextGenerators(javascriptGenerator) { size: ${size}, depth: ${depth}, position: { x: ${x}, y: ${y}, z: ${z} }, - modelId: ${JSON.stringify(meshId)}${doCode ? `,\n callback: ${doCode}` : ""} + modelId: ${JSON.stringify(meshId)}${doCode ? `,\n callback: ${doCode}` : ''} });\n`; }; diff --git a/generators/generators-transform.js b/generators/generators-transform.js index 7163a6ac8..b778abf69 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,58 +190,46 @@ 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`; @@ -291,78 +237,72 @@ export function registerTransformGenerators(javascriptGenerator) { // Make a character jump to a target height, keeping current horizontal speed // for momentum. A direct, height-based replacement for a vertical force. - javascriptGenerator.forBlock["jump"] = function (block) { + javascriptGenerator.forBlock['jump'] = function (block) { const mesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MODEL_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MODEL_VAR'), + Blockly.Names.NameType.VARIABLE ); const jumpHeight = - javascriptGenerator.valueToCode( - block, - "JUMP_HEIGHT", - javascriptGenerator.ORDER_ATOMIC, - ) || "1.5"; + javascriptGenerator.valueToCode(block, 'JUMP_HEIGHT', javascriptGenerator.ORDER_ATOMIC) || + '1.5'; return `jump(${mesh}, { jumpHeight: ${jumpHeight} });\n`; }; // Set the speed an object travels at in a direction (forward/sideways/up or // world x/y/z). Maintained until changed; 'all' to 0 stops it. - javascriptGenerator.forBlock["set_speed"] = function (block) { + javascriptGenerator.forBlock['set_speed'] = function (block) { const mesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); - const direction = block.getFieldValue("DIRECTION") || "forward"; + const direction = block.getFieldValue('DIRECTION') || 'forward'; const speed = - javascriptGenerator.valueToCode(block, "SPEED", javascriptGenerator.ORDER_ATOMIC) || "0"; + javascriptGenerator.valueToCode(block, 'SPEED', javascriptGenerator.ORDER_ATOMIC) || '0'; return `setSpeed(${mesh}, '${direction}', ${speed});\n`; }; // Set how bouncy an object is (restitution, 0..1) - javascriptGenerator.forBlock["set_bounciness"] = function (block) { + javascriptGenerator.forBlock['set_bounciness'] = function (block) { const mesh = javascriptGenerator.nameDB_.getName( - block.getFieldValue("MESH_VAR"), - Blockly.Names.NameType.VARIABLE, + block.getFieldValue('MESH_VAR'), + Blockly.Names.NameType.VARIABLE ); const bounciness = - javascriptGenerator.valueToCode(block, "BOUNCINESS", javascriptGenerator.ORDER_ATOMIC) || "0.5"; + javascriptGenerator.valueToCode(block, 'BOUNCINESS', javascriptGenerator.ORDER_ATOMIC) || + '0.5'; return `setBounciness(${mesh}, ${bounciness});\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`; @@ -373,14 +313,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 @@ -388,79 +328,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 = ` @@ -470,10 +386,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 @@ -482,44 +398,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: ${JSON.stringify(boneName ?? "")}, x: ${xOffset}, y: ${yOffset}, z: ${zOffset} }); + return `await attach(${meshToAttach}, ${targetMesh}, { boneName: ${JSON.stringify(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 @@ -532,18 +436,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; @@ -554,22 +454,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; @@ -580,18 +476,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; @@ -602,18 +494,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; @@ -625,8 +513,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 96be9181a..fc38b1674 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}(${JSON.stringify(meshId)}, { ${options.join(", ")} });\n${doCode}`; + return `${variableName} = create${meshType}(${JSON.stringify(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,17 +166,14 @@ export function emitSafeTextArg(code) { // quote โ€” e.g. a concatenation like `'a' + 'b'` produced by text_join. In // that case leave it untouched so the expression is emitted as real code // (otherwise the literal ` + ` ends up baked into the displayed string). - if (body.replace(/\\./g, "").includes(q)) return code; + if (body.replace(/\\./g, '').includes(q)) return code; // Decode literal safely (handles \', \\ , \n, \uXXXX, etc.) let decoded; 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 4d0886341..c81194387 100644 --- a/generators/generators.js +++ b/generators/generators.js @@ -1,41 +1,39 @@ -import * as Blockly from "blockly"; -import { javascriptGenerator } from "blockly/javascript"; -import "@blockly/block-plus-minus"; -import "../blocks/text_join_shadow.js"; -import { clearMeshMaps } from "./mesh-state.js"; +import * as Blockly from 'blockly'; +import { javascriptGenerator } from 'blockly/javascript'; +import '@blockly/block-plus-minus'; +import '../blocks/text_join_shadow.js'; +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); @@ -75,8 +73,8 @@ export function defineGenerators() { defvars.push( javascriptGenerator.nameDB_.getName( devVarList[i], - Blockly.Names.NameType.DEVELOPER_VARIABLE, - ), + Blockly.Names.NameType.DEVELOPER_VARIABLE + ) ); } @@ -86,7 +84,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); @@ -95,13 +93,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 b22c4e64f..2a3d7d9cc 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 60a729df3..cbac5fef2 100644 --- a/index.html +++ b/index.html @@ -31,19 +31,16 @@ - + @@ -62,9 +59,9 @@ if (!this.isConnected) return false; const style = window.getComputedStyle(this); return ( - style.display !== "none" && - style.visibility !== "hidden" && - style.visibility !== "collapse" && + style.display !== 'none' && + style.visibility !== 'hidden' && + style.visibility !== 'collapse' && parseFloat(style.opacity) !== 0 ); }; @@ -151,9 +148,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; } @@ -197,49 +192,47 @@ - -