Building Bidirectional Systems From Scratch - Complete Beginner's Guide

What is Bidirectional Sync?

The whole point of Otto is that you can edit code and see shapes, OR edit shapes and see code. This "bidirectional sync" means changes flow both ways - code changes update visuals, and visual changes update code.

Simple Analogy: Think of a two-way mirror:

  • When you change the code (one side), the shapes update (the other side)
  • When you drag a shape (one side), the code updates (the other side)
  • Both sides always stay in sync

Real-World Example: Like Google Docs with collaborative editing:

  • Person A types something → Person B sees it update
  • Person B types something → Person A sees it update
  • Both sides stay synchronized

But in Otto, it's code ↔ shapes instead of person ↔ person.

Why Bidirectional Sync Matters - Explained Much More Simply

What is the Problem with Traditional Tools?

Traditional CAD (Computer-Aided Design) tools force you to choose ONE way to work:

Option 1: Edit Code (Parametric Design)

  • You write code like circle(r=50) → shape appears
  • Pros: Powerful, flexible, precise
  • Cons: Can't drag shapes directly - you have to type new code every time
  • Real-world analogy: Like cooking from a recipe - you can follow it exactly, but you can't just grab ingredients and taste as you go

Option 2: Edit Visually (Direct Manipulation)

  • You drag shapes around with your mouse → shape moves
  • Pros: Easy, intuitive, visual feedback
  • Cons: Code doesn't update - you lose the parametric benefits
  • Real-world analogy: Like freehand drawing - you can draw anything, but you can't easily edit or reuse it

Why This is a Problem:

You have to pick ONE way:

  • Want precision? Use code (but lose visual editing)
  • Want ease? Use visual (but lose code benefits)

It's like choosing between a hammer and a screwdriver - you need BOTH for different tasks!

The Otto Solution - Explained Simply:

Otto does BOTH at the same time! This is like having a magic tool that's both a hammer AND a screwdriver.

How Otto Works - Three Powerful Workflows:

Workflow 1: Write Code → See Shapes Immediately

What you do:

  • Type code in the editor: shape circle c1 { radius: 50 }

What happens:

  • Code runs automatically (after you stop typing)
  • Circle appears on canvas immediately
  • You see instant visual feedback

Why this is powerful:

  • Get visual confirmation as you code
  • Catch mistakes immediately (wrong shape? Wrong size? You see it right away)
  • Work faster (see results instantly)

Real-world analogy: Like typing in a word processor with live preview - you see what it looks like as you type, not just when you're done!

Example:

You type: "shape circle c1 { radius: 50 }"
    ↓
[System processes code]
    ↓
Circle appears on canvas!

Workflow 2: Drag Shapes → Code Updates Automatically

What you do:

  • Drag a circle's corner handle to resize it
  • Or drag the circle to move it

What happens:

  • Shape changes immediately (you see it move/resize)
  • Code updates automatically in the background (after you stop dragging)
  • Code now matches what you see

Why this is powerful:

  • Intuitive editing (just drag, like moving furniture)
  • Code stays in sync (you don't lose parametric benefits)
  • Best of both worlds (visual ease + code precision)

Real-world analogy: Like using a smart thermostat - you adjust the temperature dial (visual), and it automatically updates the settings (code behind the scenes)!

Example:

You drag: Circle corner handle (radius 50 → 75)
    ↓
Shape resizes immediately (you see it grow)
    ↓
Code updates: "radius: 50" → "radius: 75"

Workflow 3: Switch Between Text and Blocks Seamlessly

What you do:

  • Type code OR drag blocks
  • Switch between modes anytime

What happens:

  • Type code → blocks update automatically
  • Move blocks → code updates automatically
  • Both always show the same thing

Why this is powerful:

  • Beginners can use blocks (easier, visual)
  • Power users can type code (faster, more control)
  • Everyone can switch anytime

Real-world analogy: Like having a book in both English and Spanish - same content, different formats. You can read whichever you prefer, and they always say the same thing!

Example:

You type code: "shape circle c1 { radius: 50 }"
    ↓
Blocks mode shows: [Circle] block with [radius: 50] field

You change block: [radius: 50] → [radius: 75]
    ↓
Code updates: "radius: 50" → "radius: 75"

Why This Creates a Powerful Workflow:

Traditional Tool Workflow (One Way Only):

Option A (Code):
Write code → See shape
❌ Can't drag to edit
❌ Must type new code for every change

Option B (Visual):
Drag shape → See change
❌ Code doesn't update
❌ Lose parametric benefits

Otto Workflow (Both Ways!):

Type code → See shape ✓
Drag shape → Code updates ✓
Use blocks → Code updates ✓
Use code → Blocks update ✓

✅ Best of both worlds!
✅ Use whichever is easier for the task
✅ Everything stays in sync

The Key Benefit:

You're never locked into one way. Need to be precise? Type code. Need to experiment? Drag shapes. Need to teach someone? Use blocks. All three ways work, and they all stay in sync!

But Here's the Challenge: You have THREE representations of the same thing:

  • Text code in the editor
  • Blockly blocks (visual blocks)
  • Shape objects in memory

When you change one, the others need to update. But if you're not careful, you get infinite loops:

  • Change code → update blocks → blocks trigger code change → update blocks → update code → INFINITE LOOP!

This chapter teaches you how to prevent these loops while keeping everything in sync.

The Three Representations - Explained Much More Simply with Examples

What are "Representations"?

A "representation" is just a different way of showing the same thing. Think of it like this:

  • The same person can be described as "John" (name), shown in a photo (picture), or listed by their ID number (data)
  • All three represent the same person, just in different formats

Every shape exists in THREE different forms:

Every shape in your system exists as THREE different representations at the same time. They all show the same shape, just in different formats!

Representation 1: Text Code (in the editor) - The Written Form

What it looks like:

shape circle c1 { radius: 50 }

What it is:

  • Plain text that you can read and type
  • Like a recipe written in words
  • Users see this when they're in "text mode"

How users interact with it:

  • Type directly in the code editor
  • Edit like editing a document
  • Use keyboard shortcuts

Real-world analogy: Like a written recipe:

"Add 2 cups of flour"

You can read it, type it, edit it - it's just text.

Example in Otto:

Text code: "shape circle c1 { radius: 50 }"
    ↓
This is a circle named "c1" with radius 50

Representation 2: Blockly Blocks (visual blocks) - The Visual Form

What it looks like:

  • Colorful blocks you can drag and drop
  • Blocks snap together
  • Fields (like radius) show as inputs you can fill

What it is:

  • Visual representation of the same information
  • Same circle, same radius, just shown visually instead of text
  • Users see this when they're in "blocks mode"

How users interact with it:

  • Drag blocks from toolbox
  • Connect blocks together
  • Fill in fields (numbers, text, etc.)
  • Rearrange blocks

Real-world analogy: Like a picture recipe:

  • Instead of text saying "add flour", you see a picture of flour
  • Same information, visual format
  • Easier for some people to understand

Example in Otto:

Blocks mode shows:
┌─────────────┐
│   Circle    │
│  radius: 50 │
└─────────────┘

This is the SAME circle (c1, radius 50), just shown as a block!

Representation 3: Shape Objects (in memory) - The Data Form

What it looks like:

{ 
  type: 'circle', 
  params: { radius: 50 }, 
  name: 'c1',
  transform: { position: [0, 0], rotation: 0, scale: [1, 1] }
}

What it is:

  • JavaScript object (data structure)
  • The actual data the computer uses
  • Lives in memory (users don't see this directly)
  • This is what the renderer reads to draw shapes

How users interact with it:

  • Users DON'T interact with this directly
  • This is created automatically from code or blocks
  • The renderer reads this to know what to draw

Real-world analogy: Like a computer's internal file format:

  • You might save a document as "myfile.doc" (text representation - what you see)
  • But the computer stores it as binary data (data representation - what computer uses)
  • Same document, different formats

Example in Otto:

Shape object (in memory):
{
  type: 'circle',        // It's a circle
  name: 'c1',            // Named "c1"
  params: { radius: 50 } // Radius is 50
}

This is the SAME circle, stored as data!
The renderer reads this and draws a circle on the canvas.

Why All Three Exist:

Text Code:

  • Easy to write for power users
  • Can copy/paste
  • Can version control (save in git)
  • Human-readable

Blocks:

  • Easy for beginners
  • Visual, intuitive
  • Harder to make mistakes (blocks guide you)
  • Great for learning

Shape Objects:

  • What the computer actually uses
  • Efficient for rendering
  • Can be manipulated programmatically
  • The "source of truth" for drawing

The Key Insight:

All three represent the SAME thing! They're just different formats:

Text Code:    "shape circle c1 { radius: 50 }"
     ↕ (converts to/from)
Blocks:       [Circle block with radius: 50]
     ↕ (converts to/from)
Shape Object: { type: 'circle', params: { radius: 50 }, name: 'c1' }

Why This Matters:

Because they're all the same thing, when you change ONE, the others must update to match! This is what "staying in sync" means - all three representations always show the same shape data.

The Challenge: All three need to stay in sync! When you change one, the others must update:

  • Change code → blocks update, shape object updates
  • Move a block → code updates, shape object updates
  • Drag a shape → code updates, blocks update

The Nightmare Scenario - Infinite Loop: Without careful coordination:

  1. User changes code → system updates blocks
  2. Blocks update → blocks trigger code change event
  3. Code changes → system updates blocks again
  4. Blocks update → code changes → blocks update → INFINITE LOOP! 💥

Your app freezes, uses 100% CPU, and becomes unusable.

The Solution: Sync Flags and Debouncing

We solve this with two techniques:

1. Sync Flags - "Who's Currently Updating?"

  • Boolean flags that track which component is currently updating
  • If blocks are updating code, set a flag
  • If code tries to update blocks while flag is set, skip it (don't create a loop!)
  • Like a "Do Not Disturb" sign - "I'm updating, don't update me back"

2. Debouncing - "Wait Until They Stop"

  • Instead of updating on every keystroke, wait until the user stops typing
  • User types "c" → wait...
  • User types "i" → wait...
  • User types "r" → wait...
  • User stops typing for 300ms → NOW update blocks
  • This prevents excessive updates and reduces loop chances

The Problem

You have three representations of the same thing:

  1. Text code - shape circle c1 { radius: 50 }
  2. Blockly blocks - Visual blocks in the editor
  3. Shape objects - JavaScript objects with { type: 'circle', params: { radius: 50 } }

When someone changes one, the others need to update. But here's the catch: if you're not careful, you get infinite loops. Change code → update blocks → blocks trigger code change → update blocks again → infinite loop.

Building Sync Flags From Scratch

What You're Building: Sync flags are like "Do Not Disturb" signs for your code. They're simple boolean (true/false) variables that track which component is currently updating. This prevents infinite loops.

Real-World Analogy: Think of sync flags like a busy signal on a phone:

  • When you call someone and they're already on the phone, you get a busy signal
  • You don't try to connect again - you wait
  • Sync flags work the same way: "I'm updating right now, don't update me back!"

Why Sync Flags: Without flags, you get infinite loops:

Blocks update editor → Editor fires change event → Sync to blocks → Blocks update editor → Loop!

With flags:

Blocks update editor (set flag = true) → Editor fires change event → Check flag (it's true!) → Skip syncing back → No loop! ✓

How to Build It Step by Step:

Step 1: Create the Sync Flags

Sync flags are like "Do Not Disturb" signs - when one is true, that component is updating, so we shouldn't update it back (that would create a loop). We need three separate flags because there are three different update paths that can happen independently.

What Each Flag Does:

Each flag tracks a specific update path and prevents loops in that path:

  1. syncingFromBlocks - Prevents: blocks → editor → blocks loop

    • Set to true when blocks are updating the editor (blocks changed, generating new code)
    • When this flag is true, the editor change handler skips syncing back to blocks
    • Prevents the cycle: blocks change → editor updates → editor change event → blocks update → blocks change → ...
  2. _writingEditorFromShape - Prevents: shapes → editor → code → shapes loop

    • Set to true when shapes are updating the editor (shape dragged, code being written)
    • When this flag is true, the editor change handler skips running code (which would update shapes)
    • Prevents the cycle: shape changes → editor code updates → code executes → shapes update → shape changes → ...
  3. _writingEditorFromConstraints - Prevents: constraints → editor → code → constraints loop

    • Set to true when constraints are updating the editor (constraint solved, code being written)
    • When this flag is true, the editor change handler skips running code (which would rebuild constraints)
    • Prevents the cycle: constraint solves → editor code updates → code executes → constraints rebuild → constraint solves → ...

Flag Naming Convention:

  • syncingFromBlocks - No underscore, active voice ("syncing from blocks"). This flag is used more frequently and may be accessed from multiple places.
  • _writingEditorFromShape - Underscore prefix indicates internal/private. This flag is primarily used internally within the sync system.
  • _writingEditorFromConstraints - Underscore prefix, same pattern. Also internal to the sync system.

The underscore prefix is a JavaScript convention meaning "this is internal, don't use it from outside this module." It signals to other developers that this variable is implementation detail and shouldn't be accessed directly.

The Golden Rule:

Before updating anything, ALWAYS check if you're already syncing. If any flag is true, bail out immediately (return early). This simple rule prevents ALL infinite loops.

Understanding the Flag Lifecycle:

Each flag follows a pattern:

  1. Set flag before update: syncingFromBlocks = true;
  2. Perform the update: Update editor, blocks, or shapes
  3. Clear flag after update: syncingFromBlocks = false;

The flag acts like a lock - while it's set, other components know not to respond to changes from that source.

Why Three Separate Flags:

We can't use a single flag because multiple update paths can happen independently:

  • Blocks can update editor while shapes are also updating editor (different sources)
  • We need to track which specific source is updating, not just "something is updating"
  • Different flags allow different components to update simultaneously without interfering (as long as they're not trying to update each other)

For example:

  • Blocks updating editor → syncingFromBlocks = true
  • Shapes updating editor → _writingEditorFromShape = true
  • Both can happen at the same time because they're independent operations
  • The editor change handler checks ALL flags, so it knows to skip syncing in both cases

Example Scenarios:

Blocks → Editor Sync:

  1. User moves a block → blocks generate code → update editor
  2. Editor change event fires (because we just updated it)
  3. Normally, this would trigger "sync editor → blocks"
  4. But we check: if (syncingFromBlocks) return; → flag is true, so we skip!
  5. No loop! ✓

Shapes → Editor Sync:

  1. User drags a shape → shape updates code in editor
  2. Editor change event fires
  3. Normally, this would trigger code execution
  4. But we check: if (_writingEditorFromShape) return; → flag is true, so we skip!
  5. No loop! ✓
// Sync flags prevent circular updates between code, blocks, and shapes
let syncingFromBlocks = false;              // Blocks → editor sync
let _writingEditorFromShape = false;        // Shapes → editor sync
let _writingEditorFromConstraints = false;  // Constraints → editor sync

Visual Example:

Component A wants to update Component B:
  → Check: Is Component A currently being updated? (check flag)
  → If YES: Stop! Don't update. (prevents loop)
  → If NO: Set flag, update, clear flag

The Golden Rule: Before updating anything, check if you're already syncing. If you are, bail out immediately. This simple rule prevents all infinite loops.

Why Three Flags? We need three separate flags because there are three different update paths:

  1. Blocks → Editor - Blocks generate code and update editor
  2. Shapes → Editor - Shape changes update code in editor
  3. Constraints → Editor - Constraint solving updates code in editor

Each path needs its own flag because they can happen independently. For example, a user might drag a shape while constraints are also solving. We need to track both separately.

Flag Naming Convention:

  • syncingFromBlocks - Active voice, describes what's happening
  • _writingEditorFromShape - Underscore prefix indicates internal/private
  • _writingEditorFromConstraints - Same pattern for consistency

The underscore prefix is a convention indicating these are internal implementation details that shouldn't be accessed directly from outside the module.

Building Text → Blocks Sync From Scratch

What You're Building: An event handler that listens to text editor changes and synchronizes them to Blockly blocks. This handler must carefully check sync flags to prevent infinite loops and use debouncing to avoid excessive updates.

Why This Handler: When users type code, the blocks need to update to reflect the changes. But we must prevent loops: if blocks are updating the editor, we shouldn't sync back to blocks. This handler implements that logic.

How to Build It Step by Step:

Step 1: Create the Editor Change Handler

Every time the editor content changes (keystroke, paste, delete), this function runs. It needs to:

  1. Check if this change came from a sync (if so, ignore it)
  2. Wait until user stops typing (debouncing)
  3. Then update blocks and run code

Understanding the Guard Clause (The Critical Check):

This line is THE most important line in bidirectional sync - it prevents infinite loops. A guard clause is a programming pattern where you check for invalid/undesirable conditions at the start of a function and return early if those conditions are met. It "guards" the rest of the function from executing in bad situations.

The Guard Clause:

if (syncingFromBlocks || _writingEditorFromShape || _writingEditorFromConstraints) return;

What It Checks - Detailed Breakdown:

The condition uses the logical OR operator (||) to check if ANY of the flags are true:

  1. syncingFromBlocks - Are blocks currently updating the editor?

    • If true, blocks just generated new code and updated the editor
    • We don't want to sync editor → blocks because blocks already have the new code
    • This prevents: blocks → editor → blocks → editor loop
  2. _writingEditorFromShape - Are shapes currently updating the editor?

    • If true, a shape change triggered code generation and editor update
    • We don't want to run code (which would update shapes) because the change already came from shapes
    • This prevents: shapes → editor → code → shapes → editor loop
  3. _writingEditorFromConstraints - Are constraints currently updating the editor?

    • If true, constraint solving triggered code generation and editor update
    • We don't want to run code (which would rebuild constraints) because the change already came from constraints
    • This prevents: constraints → editor → code → constraints → editor loop

How the OR Operator Works:

The || operator evaluates left to right and returns true if ANY operand is true:

  • false || false || false = false (none are true, condition fails, continue execution)
  • true || false || false = true (first is true, condition succeeds, exit immediately)
  • false || true || false = true (second is true, condition succeeds, exit immediately)
  • false || false || true = true (third is true, condition succeeds, exit immediately)

Detailed Evaluation Process:

When JavaScript evaluates syncingFromBlocks || _writingEditorFromShape || _writingEditorFromConstraints, it performs these steps:

Step 1.1: Evaluate First Operand

  • Check the value of syncingFromBlocks
  • If it's true, the entire expression is true (short-circuit evaluation - stops here)
  • If it's false, continue to next operand

Step 1.2: Evaluate Second Operand (if first was false)

  • Check the value of _writingEditorFromShape
  • If it's true, the entire expression is true (short-circuit - stops here)
  • If it's false, continue to next operand

Step 1.3: Evaluate Third Operand (if first two were false)

  • Check the value of _writingEditorFromConstraints
  • If it's true, the entire expression is true
  • If it's false, the entire expression is false

Short-Circuit Evaluation:

JavaScript uses "short-circuit" evaluation for logical operators. This means:

  • If the first operand is true, JavaScript doesn't even check the other operands
  • This is an optimization - if we know the result will be true, we don't need to check the rest

Why Short-Circuit Matters:

Short-circuit evaluation improves performance:

  • Faster execution (doesn't evaluate unnecessary operands)
  • Can prevent errors (if later operands would throw errors, short-circuit avoids them)
  • More efficient code execution

Example Scenarios:

Scenario 1: Blocks are syncing

syncingFromBlocks = true
_writingEditorFromShape = false
_writingEditorFromConstraints = false

Evaluation:
- Check syncingFromBlocks → true
- Short-circuit! Expression = true (don't check others)
- Guard clause triggers → return early → sync is prevented

Scenario 2: Shapes are updating editor

syncingFromBlocks = false
_writingEditorFromShape = true
_writingEditorFromConstraints = false

Evaluation:
- Check syncingFromBlocks → false (continue)
- Check _writingEditorFromShape → true
- Short-circuit! Expression = true (don't check _writingEditorFromConstraints)
- Guard clause triggers → return early → sync is prevented

Scenario 3: None are syncing (normal user typing)

syncingFromBlocks = false
_writingEditorFromShape = false
_writingEditorFromConstraints = false

Evaluation:
- Check syncingFromBlocks → false (continue)
- Check _writingEditorFromShape → false (continue)
- Check _writingEditorFromConstraints → false
- Expression = false
- Guard clause fails → continue execution → sync proceeds normally

Why This Pattern Prevents Loops:

The guard clause acts as a "stop sign" that prevents synchronization when it would cause a loop:

Without Guard Clause (LOOP):

1. User drags shape → Shape updates
2. Shape → Code sync: Code updates in editor
3. Editor change event fires
4. Editor → Blocks sync: Blocks update
5. Blocks → Editor sync: Editor updates again
6. Editor change event fires again
7. Go back to step 3 → INFINITE LOOP!

With Guard Clause (NO LOOP):

1. User drags shape → Shape updates
2. Shape → Code sync: Set _writingEditorFromShape = true, Update editor, Set flag = false
3. Editor change event fires
4. Guard clause checks: _writingEditorFromShape was true (just cleared, but check happens)
5. Actually, flag is cleared, but we still check: syncingFromBlocks? No. _writingEditorFromShape? No. Continue.
6. Wait - we need better logic here...

Actually, the guard clause checks if we're CURRENTLY syncing. When shape → code sync happens:
- Set flag = true (before updating editor)
- Update editor (triggers change event)
- Guard clause sees flag = true → returns early → prevents editor → blocks sync
- Clear flag = false (after update complete)
- Next user action can proceed normally

The Critical Timing:

The flag must be set BEFORE the editor update, and the guard clause must check it. This ensures:

  1. Flag is set to true
  2. Editor update happens (triggers change event)
  3. Change event handler runs → Guard clause checks flag → Flag is true → Returns early
  4. Sync is prevented → Loop avoided
  5. Flag is cleared to false
  6. Normal operation resumes
  7. true || false || false = true (first is true, condition passes, return early)
  8. false || true || false = true (second is true, condition passes, return early)
  9. true || true || true = true (all are true, condition passes, return early)

Once any flag is true, the condition is true, and we return early. We don't need to check the remaining flags (short-circuit evaluation).

What Happens If We Don't Check:

Without the guard clause, here's what happens:

User moves a block:
  1. Block changes → blocks generate new code
  2. Code written to editor (editor content changes)
  3. Editor change event fires (because content changed)
  4. Handler runs → syncs editor → blocks (rebuilds blocks from code)
  5. Blocks update (because code changed)
  6. Blocks generate new code again
  7. Code written to editor → editor change event fires
  8. Handler runs → syncs editor → blocks
  9. Blocks update → generate code → editor changes → INFINITE LOOP!

The system enters an endless cycle of updates, consuming 100% CPU, freezing the browser, and making the application unusable.

What Happens With The Check:

With the guard clause, here's the protected flow:

User moves a block:
  1. Block changes → blocks generate new code
  2. Set flag: syncingFromBlocks = true
  3. Code written to editor (editor content changes)
  4. Editor change event fires (because content changed)
  5. Handler runs → checks guard clause:
     - syncingFromBlocks is true!
     - Condition is true → return immediately
  6. No syncing to blocks happens
  7. Clear flag: syncingFromBlocks = false (after update completes)
  8. No loop! ✓ System remains responsive

The Early Return Pattern:

The return; statement immediately exits the function if any flag is true. This is called a "guard clause" - it guards the rest of the function from executing in bad situations (infinite loops). This pattern is also called "fail fast" - we check for problems first and exit early rather than proceeding and causing issues.

Why Guard Clauses Are Better Than Nested Ifs:

Instead of wrapping all the code in an if block:

// BAD: Nested structure, harder to read
if (!syncingFromBlocks && !_writingEditorFromShape && !_writingEditorFromConstraints) {
  // 50 lines of code here...
}

We use early return:

// GOOD: Early return, flat structure, easier to read
if (syncingFromBlocks || _writingEditorFromShape || _writingEditorFromConstraints) return;
// 50 lines of code here (not nested)...

Benefits:

  • Reduced nesting: Code is less indented, easier to read
  • Clear intent: The guard clause clearly shows "if these conditions, don't proceed"
  • Fail fast: Problems are caught immediately at the start
  • Easier to maintain: Adding more guards is straightforward
editor.on('change', () => {
  // Check all sync flags - if any are set, bail out immediately (prevent loops)
  if (syncingFromBlocks || _writingEditorFromShape || _writingEditorFromConstraints) return;

Step 2: Cancel Any Pending Execution (Debouncing)

Instead of running code on EVERY keystroke, we wait until the user STOPS typing. This is called "debouncing" - a programming technique that delays function execution until a period of inactivity.

Understanding Debouncing:

Debouncing is a pattern that ensures a function only executes after a certain amount of time has passed since it was last called. In our case, we want to wait until the user stops typing before executing expensive operations like running code and updating blocks.

Why We Need Debouncing:

Without debouncing, here's what happens:

When a user types "circle":

  • User types 'c' → editor change event fires → code runs → canvas redraws → blocks update
  • User types 'i' → editor change event fires → code runs → canvas redraws → blocks update
  • User types 'r' → editor change event fires → code runs → canvas redraws → blocks update
  • User types 'c' → editor change event fires → code runs → canvas redraws → blocks update
  • User types 'l' → editor change event fires → code runs → canvas redraws → blocks update
  • User types 'e' → editor change event fires → code runs → canvas redraws → blocks update

Result:

  • Code runs 6 times (once per letter)
  • Canvas redraws 6 times (expensive operation)
  • Blocks update 6 times (expensive DOM manipulation)
  • Interpreter re-executes 6 times (very expensive)
  • System becomes slow and unresponsive
  • User sees flickering and lag

With debouncing:

When a user types "circle":

  • User types 'c' → schedule execution for 300ms from now
  • User types 'i' → cancel previous timer, schedule new one for 300ms from now
  • User types 'r' → cancel previous timer, schedule new one for 300ms from now
  • User types 'c' → cancel previous timer, schedule new one for 300ms from now
  • User types 'l' → cancel previous timer, schedule new one for 300ms from now
  • User types 'e' → cancel previous timer, schedule new one for 300ms from now
  • User stops typing → 300ms passes with no new changes → timer fires → code runs ONCE

Result:

  • Code runs 1 time (after user finishes typing)
  • Canvas redraws 1 time
  • Blocks update 1 time
  • System remains responsive and smooth
  • User sees smooth, immediate visual feedback in editor, actual execution happens after they pause

How clearTimeout Works:

clearTimeout(timeout) cancels any previously scheduled timeout. The timeout variable stores the ID returned by setTimeout(). When you call clearTimeout(timeout), JavaScript cancels that specific timeout, preventing it from executing.

Why We Need clearTimeout:

Without clearTimeout, each keystroke would schedule a separate execution. All of them would eventually fire, causing multiple executions. With clearTimeout, we cancel the previous timer each time, ensuring only the most recent one will execute.

Example Timeline (User Types "shape"):

Let's trace what happens when a user types the word "shape" character by character:

t=0ms:    User types 's' 
          → Editor change event fires
          → Handler runs
          → Schedule code execution for t=300ms
          → Store timer ID in 'timeout' variable
          → timeout = setTimeout(..., 300)

t=50ms:   User types 'h'
          → Editor change event fires
          → Handler runs
          → clearTimeout(timeout) → Cancels the timer scheduled for t=300ms
          → Schedule NEW code execution for t=350ms (50 + 300)
          → Store new timer ID in 'timeout' variable

t=100ms:  User types 'a'
          → Editor change event fires
          → Handler runs
          → clearTimeout(timeout) → Cancels the timer scheduled for t=350ms
          → Schedule NEW code execution for t=400ms (100 + 300)
          → Store new timer ID in 'timeout' variable

t=150ms:  User types 'p'
          → Editor change event fires
          → Handler runs
          → clearTimeout(timeout) → Cancels the timer scheduled for t=400ms
          → Schedule NEW code execution for t=450ms (150 + 300)
          → Store new timer ID in 'timeout' variable

t=200ms:  User types 'e'
          → Editor change event fires
          → Handler runs
          → clearTimeout(timeout) → Cancels the timer scheduled for t=450ms
          → Schedule NEW code execution for t=500ms (200 + 300)
          → Store new timer ID in 'timeout' variable

t=500ms:  User stopped typing (no more change events)
          → Timer fires (300ms has passed since last keystroke)
          → Code execution happens ONCE
          → Canvas redraws
          → Blocks update

Without clearTimeout: Code would run 5 times (once for each letter) - way too many! The first timer (scheduled at t=0ms) would fire at t=300ms, the second (at t=50ms) would fire at t=350ms, etc. All executions would happen, causing performance issues.

With clearTimeout: Code runs once after the user stops typing - perfect! Each new keystroke cancels the previous timer, so only the most recent one executes.

Understanding setTimeout - Detailed Breakdown:

setTimeout(callback, delay) is a browser API that schedules a function to execute after a specified delay. Let's break down how it works:

Function Signature:

const timerId = setTimeout(callback, delay);

Parameters:

  • callback: The function to execute after the delay (can be a named function or anonymous function)
  • delay: Time to wait in milliseconds (e.g., 300 = 300 milliseconds = 0.3 seconds)

Return Value:

  • Returns a numeric timer ID that uniquely identifies this scheduled timeout
  • This ID is used with clearTimeout() to cancel the timeout if needed
  • The ID is always positive and unique for each timeout

How setTimeout Works Internally:

Step 1: Registration Phase When you call setTimeout(callback, 300):

  1. JavaScript engine registers the callback in an internal timer queue
  2. Records the current time plus the delay (e.g., current time + 300ms)
  3. Returns a unique timer ID for this timeout
  4. The callback is NOT executed immediately

Step 2: Waiting Phase The browser's event loop continues processing other tasks:

  • User interactions (clicks, typing, etc.)
  • Other JavaScript code execution
  • Rendering and painting
  • Network requests

Step 3: Execution Phase When the delay period elapses:

  1. Browser checks the timer queue
  2. Finds timeouts that have expired (current time >= scheduled time)
  3. Adds the callback to the JavaScript execution queue
  4. When JavaScript engine is free, it executes the callback
  5. The callback runs in the same context where setTimeout was called

Important Notes:

Timing is Not Guaranteed: The delay is a minimum time, not an exact time. The callback might execute slightly later than the specified delay if:

  • JavaScript is busy executing other code
  • The browser is handling other high-priority tasks
  • The tab is in the background (some browsers throttle background timers)

Example:

console.log('Start:', Date.now());

setTimeout(() => {
  console.log('Delayed:', Date.now());
}, 300);

// Output:
// Start: 1234567890000
// Delayed: 1234567890320  (320ms later, not exactly 300ms)

Timer IDs Are Reusable: Timer IDs are unique but can be reused after a timeout completes or is cleared. The same ID won't be used for two active timeouts simultaneously.

Zero Delay Doesn't Mean Immediate: Even setTimeout(callback, 0) doesn't execute immediately. It schedules the callback for the next event loop iteration, allowing other code to run first.

Example with Zero Delay:

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

console.log('3');

// Output:
// 1
// 3
// 2  (executes after current code finishes, even with 0 delay)

Why setTimeout is Perfect for Debouncing:

Debouncing needs:

  1. Delay execution: Wait for a period of inactivity
  2. Cancel capability: Cancel previous scheduled execution if a new one comes in
  3. Reliable timing: Execute after the delay period

setTimeout provides all of these:

  • Delays execution: Exactly what we need
  • Returns ID: Can cancel with clearTimeout(id)
  • Reliable enough: Timing is sufficient for user interaction delays (300ms)

Real-World Analogy:

Think of setTimeout like setting an alarm clock:

  • You set it for a specific time in the future (the delay)
  • The alarm goes off when that time arrives (callback executes)
  • You can cancel it before it goes off (clearTimeout)
  • It doesn't interrupt you while you're doing other things (non-blocking)

In our debouncing case:

  • User types a letter → Set "alarm" for 300ms from now (code execution)
  • User types another letter → Cancel previous "alarm", set new one for 300ms from now
  • User stops typing → 300ms passes → "Alarm" goes off → Code executes
  // Cancel any pending execution (debouncing)
  // If user is still typing, cancel the previous timer and schedule a new one
  // This ensures code only executes after user stops typing for the full delay period
  // clearTimeout() prevents multiple timers from accumulating and executing
  clearTimeout(timeout);
  // Schedule code execution after typing stops (300ms delay)
  timeout = setTimeout(() => {
    // This callback runs 300ms after the last keystroke (if user stopped typing)
    runCode();

What setTimeout Does: setTimeout(() => { ... }, 300) schedules code to run 300 milliseconds in the future. If the user types again before 300ms passes, we cancel this (with clearTimeout above) and schedule a new one.

Why 300ms?

  • Fast enough: Users see results quickly after stopping
  • Long enough: Catches most typing pauses (users don't type constantly)
  • Sweet spot: Balance between responsiveness and efficiency

The code inside setTimeout runs LATER, not immediately. It's like setting an alarm clock - you set it now, but it goes off later.

    // If in blocks mode, rebuild the block workspace (text → blocks sync)
    if (editorMode === 'blocks' && blocklyWorkspace) {
      // Convert text code to blocks - parses code and creates corresponding Blockly blocks
      rebuildWorkspaceFromAqui(editor.getValue(), blocklyWorkspace);

      // Force Blockly to re-render (sometimes doesn't auto-detect changes)
      refreshBlockly();
    }
  }, 300); // Debounce delay: 300ms after typing stops

Understanding the Blocks Sync:

After running the code, if we're in blocks mode, we need to update the blocks to match the new code.

The Check: if (editorMode === 'blocks' && blocklyWorkspace)

  • editorMode === 'blocks' - Are we currently in blocks mode?
  • blocklyWorkspace - Does the workspace exist?
  • Only update blocks if BOTH are true

Why Check Both:

  • If we're in text mode, there are no blocks to update (skip it)
  • If workspace doesn't exist, we can't update it (would crash!)

What rebuildWorkspaceFromAqui() Does: Converts text code to blocks:

  • Takes the current editor text (code)
  • Parses it
  • Creates corresponding Blockly blocks
  • Updates the workspace

This is the "text → blocks" sync - code changes update the visual blocks.

What refreshBlockly() Does: Forces Blockly to re-render. Sometimes Blockly doesn't automatically detect changes, so we explicitly tell it to refresh the display.

If you see an error at this step:

Error: TypeError: editor.on is not a function

  • What this means: Editor doesn't have an on method or is not a CodeMirror instance
  • Common causes:
    1. Editor not initialized: editor is null/undefined
    2. Wrong editor type: Not a CodeMirror instance
    3. Editor not loaded yet: Script runs before editor is created
  • Fix: Check editor exists: if (!editor || typeof editor.on !== 'function') { console.error('Editor not initialized'); return; }, verify editor is CodeMirror instance

Error: ReferenceError: timeout is not defined

  • What this means: timeout variable not declared
  • Common causes:
    1. Forgot to declare: let timeout; at module/class level
    2. Variable in wrong scope: declared inside function instead of outside
    3. Typo in variable name: timeout vs timer vs timeoutId
  • Fix: Declare at top level: let timeout = null; (before the editor.on code)

Error: Infinite loop (editor constantly updating)

  • What this means: Guard clause not working or flags not set correctly
  • Common causes:
    1. Forgot to check flags: missing the if (syncingFromBlocks || ...) return; check
    2. Flags never set: code updates editor but doesn't set flags first
    3. Flags not cleared: flags set to true but never reset to false
  • Fix: Always check flags first, set flag before updating, clear flag after updating

Error: Code runs on every keystroke (very slow)

  • What this means: Debouncing not working
  • Common causes:
    1. clearTimeout not called: clearTimeout(timeout) missing
    2. timeout variable reset: let timeout inside function (creates new variable each time)
    3. setTimeout delay is 0: setTimeout(..., 0) instead of 300
  • Fix: Call clearTimeout(timeout) before setTimeout, ensure timeout is declared outside function, use delay of 300ms+

Error: TypeError: editorMode is not defined

  • What this means: editorMode variable doesn't exist
  • Common causes:
    1. Variable not declared: missing let editorMode = 'text'; or similar
    2. Wrong variable name: mode instead of editorMode
    3. Variable in wrong scope: declared in different file/scope
  • Fix: Declare editorMode variable: let editorMode = 'text'; (or get from state management)

Error: TypeError: blocklyWorkspace is not defined

  • What this means: Blockly workspace variable doesn't exist
  • Common causes:
    1. Workspace not created: Blockly.inject() never called
    2. Variable not declared: missing let blocklyWorkspace;
    3. Wrong variable name: workspace instead of blocklyWorkspace
  • Fix: Check workspace exists before use: if (!blocklyWorkspace) return;, verify Blockly.inject() was called

Error: TypeError: rebuildWorkspaceFromAqui is not a function

  • What this means: Function doesn't exist or not imported
  • Common causes:
    1. Function not implemented yet (expected if building step by step)
    2. Function not imported: missing import statement
    3. Typo in function name: rebuildWorkspaceFromAqu vs rebuildWorkspaceFromAqui
  • Fix: Implement the function or comment out until implemented, check import/export statements

Error: Blocks don't update when code changes

  • What this means: Block rebuild not happening or not working
  • Common causes:
    1. editorMode !== 'blocks': check failing, so blocks never update
    2. rebuildWorkspaceFromAqui() not working: function has bug or wrong implementation
    3. refreshBlockly() not called: blocks updated but not re-rendered
  • Fix: Check editorMode value, verify rebuildWorkspaceFromAqui() works, ensure refreshBlockly() is called // 300ms is the sweet spot: // - Fast enough to feel responsive (users see updates quickly) // - Slow enough to avoid performance issues (doesn't execute on every keystroke) // - Long enough to catch rapid typing (cancels previous executions) // // Too short (50ms): Executes too often, causes jank // Too long (1000ms): Feels unresponsive, users wait too long // Just right (300ms): Feels snappy without being excessive }); ```

Why Debouncing Matters: Debouncing is essential for performance and user experience. Without it:

  • Code would execute on every keystroke (very slow)
  • Canvas would redraw constantly (janky)
  • System would feel unresponsive

With debouncing:

  • Code executes only after user stops typing
  • System feels responsive (300ms is fast enough)
  • Performance is maintained (fewer executions)

The 300ms Sweet Spot: 300ms is chosen based on human typing patterns:

  • Average typing speed: ~40 words per minute = ~200ms per character
  • 300ms gives a small buffer after typing stops
  • Fast enough that users don't notice the delay
  • Slow enough to catch rapid typing sequences

Why Check All Three Flags: Each flag prevents a different type of loop:

  1. syncingFromBlocks - Prevents blocks → editor → blocks loop
  2. _writingEditorFromShape - Prevents shapes → editor → code → shapes loop
  3. _writingEditorFromConstraints - Prevents constraints → editor → code → constraints loop

We check all three because multiple sync operations can happen simultaneously. For example, a user might drag a shape while constraints are solving. We need to prevent both loops.

How rebuildWorkspaceFromAqui works:

function rebuildWorkspaceFromAqui(code, workspace) {
  // Parse the code
  const ast = new Parser(new Lexer(code)).parse();

  // Disable Blockly events so we don't trigger sync back
  Blockly.Events.disable();
  try {
    workspace.clear(); // Clear existing blocks

    // Convert each AST statement to a block
    let cursorY = 10;
    ast.forEach(stmt => {
      const blk = stmtToBlock(stmt, workspace);
      if (blk) {
        blk.moveBy(10, cursorY);
        cursorY += blk.getHeightWidth().height + 25;
      }
    });
  } finally {
    Blockly.Events.enable();
  }

  workspace.render();
}

Why disable events during rebuild: Blockly fires events for every block creation, connection, and field change. If we didn't disable events, creating each block would trigger the change listener, which would generate code, update the editor, which would trigger the editor's change listener, creating an infinite loop. By disabling events, we rebuild the entire workspace silently. The try/finally ensures events are always re-enabled, even if an error occurs during rebuild. The cursorY tracks vertical position - each block is placed 25px below the previous one's bottom edge (getHeightWidth().height). We need to manually position blocks because Blockly doesn't auto-layout them.

Building Blocks → Text Sync From Scratch

What You're Building: A change listener for the Blockly workspace that detects when blocks are modified and synchronizes those changes to the text editor. This listener must filter events carefully and use sync flags to prevent infinite loops.

Why This Listener: When users move or modify blocks, the text code needs to update to reflect those changes. But we must prevent loops: if text is updating blocks, we shouldn't sync back to text. This listener implements that logic.

How to Build It Step by Step:

Step 1: Add Change Listener to Blockly Workspace

Every time ANYTHING changes in the Blockly workspace, this function runs. It needs to:

  1. Filter out unnecessary events (UI events, field changes during rebuild)
  2. Check sync flags (prevent loops)
  3. Generate code from blocks
  4. Update the text editor

Understanding Blockly Events: Blockly fires many types of events:

  • UI events - Just dragging blocks around (doesn't change code structure)
  • CHANGE events - Fields edited, blocks connected/disconnected (changes code)
  • CREATE/DELETE events - Blocks added/removed (changes code)

We only care about events that actually change the code structure, not just visual movements.

blocklyWorkspace.addChangeListener(event => {
  // This listener fires whenever anything changes in the Blockly workspace
  // We need to filter these events carefully to avoid unnecessary syncs

If you see an error at this step:

Error: TypeError: blocklyWorkspace.addChangeListener is not a function

  • What this means: Workspace is not a Blockly workspace or not initialized
  • Common causes:
    1. Workspace not created: Blockly.inject() never called or failed
    2. Wrong variable: blocklyWorkspace is null/undefined
    3. Wrong object type: Workspace is not a Blockly.Workspace instance
  • Fix: Check workspace exists: if (!blocklyWorkspace) throw new Error('Workspace not initialized');, verify Blockly.inject() was called successfully

Error: Listener fires but code doesn't update

  • What this means: Event filtering too aggressive or code generation failing
  • Common causes:
    1. All events filtered out: Filter conditions too strict, blocking all events
    2. Code generation fails: workspaceToCode() returns empty string or throws error
    3. Editor update not happening: Flag set but editor not updating
  • Fix: Check event types in console: console.log('Event type:', event.type);, verify code generation works, check editor update logic

Error: Infinite loop when blocks change (blocks → code → blocks)

  • What this means: Sync flag not set or checked incorrectly
  • Common causes:
    1. Flag not set before updating editor: syncingFromBlocks = true missing
    2. Flag not cleared after update: Flag stays true forever
    3. Editor change handler not checking flag: Missing guard clause in editor handler
  • Fix: Always set flag BEFORE updating: syncingFromBlocks = true; editor.setValue(code); syncingFromBlocks = false;, ensure editor handler checks flag

Why Add Change Listener: Blockly fires events for every change in the workspace. We need to listen to these events to detect when blocks change, so we can generate code and update the text editor.

Step 2: Filter Out Unnecessary Events

Not all events should trigger code updates. We filter out events that don't actually change the code structure.

Understanding Event Filtering:

1. UI Events (Blockly.Events.UI):

  • These fire when you drag blocks around
  • Just visual movement - doesn't change code structure
  • Example: Dragging a block to a new position (same code, just visual)
  • We ignore these - no need to update code!

2. Field Change Events:

  • Field changes fire separate events when you edit numbers/text in fields
  • We only want to sync when the structure actually changes (connections, block moves, etc.)
  • Example: Changing a number in a field - this DOES change code, but we handle it differently

3. Already Syncing Check:

  • If syncingFromBlocks is true, we're already in the process of syncing
  • This prevents re-entrancy - if we're already syncing, don't sync again
  • Example: Blocks are updating editor, editor fires change, we don't want to sync back immediately
  // Filter out unnecessary events
  if (event.type === Blockly.Events.UI ||
      (event.type === Blockly.Events.CHANGE && event.element === 'field') ||
      syncingFromBlocks) {
    return;  // Skip this event - doesn't need code update
  }

2. Field Change Events:

  • These fire when you edit fields (typing in text/number inputs)
  • Blockly fires separate events for field edits
  • We only want to sync when STRUCTURE changes (connections, block moves)
  • We ignore individual field changes - they'll trigger other events

3. Sync Flag Check (syncingFromBlocks):

  • If we're already syncing, don't sync again!
  • Prevents re-entrancy (calling the sync function from within itself)
  • Critical for preventing infinite loops

What Events We DO Sync:

  • Block creation/deletion
  • Block connections/disconnections
  • Block moves that change structure
  • These actually change the code!

If you see an error at this step:

Error: TypeError: Cannot read property 'UI' of undefined or Blockly.Events is undefined

  • What this means: Blockly.Events not available or Blockly not loaded
  • Common causes:
    1. Blockly scripts not loaded: Missing script tags in HTML
    2. Blockly loaded after this code runs: Load order wrong
    3. Wrong Blockly version: Old version doesn't have Events object
  • Fix: Check Blockly scripts are loaded, verify typeof Blockly !== 'undefined', check Blockly version

Error: All events filtered out (nothing ever syncs)

  • What this means: Filter conditions too strict, blocking all events
  • Common causes:
    1. Wrong event type check: Blockly.Events.UI wrong constant name
    2. Filter logic wrong: Conditions too broad, filtering everything
    3. syncingFromBlocks always true: Flag never reset to false
  • Fix: Check event types in console, verify filter conditions, ensure flags are cleared after sync

Error: Code updates on every tiny movement (too many syncs)

  • What this means: Filter not catching UI events or field changes
  • Common causes:
    1. UI event check missing: Not checking for Blockly.Events.UI
    2. Field change check wrong: event.element !== 'field' instead of === 'field'
    3. Filter conditions inverted: Using && instead of ||
  • Fix: Verify UI event check, check field change logic, review filter conditions

Step 3: Generate Code from Blocks

  try {
    // Step 3.1: Generate code from blocks
    // Blockly.JavaScript.workspaceToCode() converts the block workspace
    // into JavaScript code. This is the blocks → code conversion.
    const code = Blockly.JavaScript.workspaceToCode(blocklyWorkspace);

What workspaceToCode() Does: This function looks at all the blocks in the workspace, figures out how they're connected, and generates text code that represents those blocks.

How It Works:

  1. Starts at the top of the workspace
  2. Finds all top-level blocks (blocks not connected to anything above them)
  3. For each block, generates code based on the block's type
  4. Recursively processes connected blocks (blocks attached below)
  5. Returns the complete code as a string

Example:

Blocks in workspace:
  [circle block] name="c1" radius=50

Generated code:
  "shape circle c1 { radius: 50 }"

Why Generate Code: We need text code to:

  • Update the text editor (show code to user)
  • Execute the code (run interpreter)
  • Store/save the program (code is easier to store than blocks)

If you see an error at this step:

Error: TypeError: Blockly.JavaScript.workspaceToCode is not a function

  • What this means: JavaScript code generator not loaded or wrong namespace
  • Common causes:
    1. javascript_compressed.js not loaded: Missing script tag for code generator
    2. Wrong namespace: Using Blockly.JavaScript but generator not registered
    3. Load order wrong: Generator loaded after this code runs
  • Fix: Check javascript_compressed.js script tag exists, verify load order (after core and blocks), check Blockly version supports JavaScript generator

Error: TypeError: Cannot read property 'workspaceToCode' of undefined

  • What this means: Blockly.JavaScript is undefined
  • Common causes:
    1. Generator script not loaded: javascript_compressed.js missing
    2. Wrong generator: Loaded different generator (python, lua, etc.) instead of JavaScript
    3. Generator not initialized: Script loaded but generator not registered
  • Fix: Ensure javascript_compressed.js is loaded, verify typeof Blockly.JavaScript !== 'undefined', check you're using JavaScript generator (not Python/Lua)

Error: Generated code is empty string or wrong

  • What this means: Code generation failing or blocks not registered
  • Common causes:
    1. No blocks in workspace: Workspace is empty, so code is empty
    2. Custom blocks not registered: Blocks exist but generators not defined
    3. Generator code has bugs: Custom generator functions have errors
  • Fix: Check workspace has blocks: blocklyWorkspace.getAllBlocks().length > 0, verify custom block generators are defined, check generator code for errors

Error: Generated code doesn't match blocks (wrong output)

  • What this means: Code generator logic is wrong
  • Common causes:
    1. Generator function incorrect: Custom generator returns wrong code
    2. Block structure different: Blocks have different structure than generator expects
    3. Generator not called for custom blocks: Missing generator registration
  • Fix: Check generator functions return correct code, verify block structure matches generator expectations, ensure all custom blocks have generators

Step 4: Check if Code Actually Changed

    // Step 4.1: Only update if code actually changed
    // Compare the generated code with the editor's current value.
    // If they're the same, no update needed - this prevents unnecessary
    // re-execution and editor updates.
    if (editor.getValue() !== code) {

Why Check if Code Changed:

This is a performance optimization. If the generated code is the same as what's already in the editor, there's no need to:

  • Update the editor (it's already correct)
  • Trigger editor change events (would cause unnecessary syncs)
  • Re-execute code (waste of CPU)

How It Works:

  • editor.getValue() gets the current code in the editor
  • code is the newly generated code from blocks
  • If they're the same (===), skip the update
  • If they're different, proceed with update

Example:

// Editor has: "shape circle c1 { radius: 50 }"
// Blocks generate: "shape circle c1 { radius: 50 }"
// They're the same → skip update (no need to change editor)

// Editor has: "shape circle c1 { radius: 50 }"
// Blocks generate: "shape circle c1 { radius: 75 }"
// They're different → update editor

If you see an error at this step:

Error: TypeError: editor.getValue is not a function

  • What this means: Editor doesn't have getValue method or is not CodeMirror
  • Common causes:
    1. Editor not initialized: editor is null/undefined
    2. Wrong editor type: Not a CodeMirror instance (different editor library)
    3. Editor API different: Using different editor with different method name
  • Fix: Check editor exists: if (!editor) return;, verify editor is CodeMirror, check editor API for correct method name

Error: Code always updates even when it hasn't changed

  • What this means: Comparison not working or code formatting differs
  • Common causes:
    1. Whitespace differences: Generated code has different spacing/tabs than editor
    2. Comparison wrong: Using == instead of === (type coercion issues)
    3. Code formatted differently: Generator adds/removes spaces, editor has different format
  • Fix: Normalize whitespace before comparing, use strict equality ===, consider normalizing code format (trim, etc.) before comparison

Error: Code never updates even when blocks change

  • What this means: Comparison always returns true (same) or condition inverted
  • Common causes:
    1. Condition inverted: if (editor.getValue() === code) instead of !==
    2. Code generation always returns same: Generator has bug, always returns same code
    3. Editor value not updating: getValue() returns stale value
  • Fix: Check condition uses !== (not equal), verify code generation actually changes, check editor.getValue() returns current value

Step 5: Set Flag and Update Editor

      // Step 5.1: Set flag to prevent circular update
      // This flag tells the editor change handler to ignore the change event
      // that will fire when we update the editor. Without this, we'd get:
      // blocks change → update editor → editor fires change → sync to blocks → loop!
      syncingFromBlocks = true;

Understanding the Flag Pattern:

This is the critical pattern for preventing loops:

  1. Set flag BEFORE updating - syncingFromBlocks = true;

    • This tells other handlers "I'm updating, don't update me back"
  2. Update the editor - editor.setValue(code);

    • This triggers editor change event
    • But editor handler sees flag is true, so it skips syncing back!
  3. Clear flag AFTER updating - syncingFromBlocks = false;

    • Reset the flag so future updates can happen

Why This Order Matters: If you update first, then set flag:

  • Editor updates → change event fires → handler runs → flag not set yet → handler syncs back → loop!

If you set flag first, then update:

  • Flag set → editor updates → change event fires → handler checks flag (true!) → skips sync → no loop! ✓
      // Step 5.2: Update editor in an atomic operation
      // editor.operation() tells CodeMirror to batch the setValue() and treat
      // it as one atomic operation. Without it, setting the value might trigger
      // multiple change events.
      editor.operation(() => {
        editor.setValue(code);
        runCode(); // Execute the new code
      });
    }

Why Set Flag: The flag tells the editor change handler to ignore the change event that will fire when we update the editor. Without this, we'd get: blocks change → update editor → editor fires change → sync to blocks → loop!

Why editor.operation(): editor.operation() tells CodeMirror to batch the setValue() and treat it as one atomic operation. Without it, setting the value might trigger multiple change events, which could cause performance issues or unexpected behavior.

Step 6: Handle Errors and Clear Flag

  } catch (e) {
    // Step 6.1: Handle code generation errors gracefully
    // Code generation can fail if blocks are in an invalid state.
    // We log the error but don't crash - sync continues working.
    console.warn('Codegen error:', e);
  } finally {
    // Step 6.2: Always clear the flag
    // This is critical - even if code generation fails or an error occurs,
    // the flag must be cleared. Otherwise, sync breaks forever.
    syncingFromBlocks = false;
  }
});

Why try/finally: The finally block ensures the flag is always cleared, even if an error occurs. If the flag stays set, sync breaks forever because the editor change handler will always bail out. This is a critical safety mechanism.

The Complete Function:

blocklyWorkspace.addChangeListener(event => {
  // Ignore UI events (like dragging) and field changes during rebuild
  if (event.type === Blockly.Events.UI ||
      (event.type === Blockly.Events.CHANGE && event.element === 'field') ||
      syncingFromBlocks) {
    return;
  }

  try {
    // Generate code from blocks
    const code = Blockly.JavaScript.workspaceToCode(blocklyWorkspace);

    // Only update if code actually changed
    if (editor.getValue() !== code) {
      syncingFromBlocks = true; // Set flag to prevent circular update
      editor.operation(() => {
        editor.setValue(code);
        runCode(); // Execute the new code
      });
    }
  } catch (e) {
    console.warn('Codegen error:', e);
  } finally {
    syncingFromBlocks = false; // Always clear the flag
  }
});

Building This Step by Step:

  1. Add change listener to Blockly workspace
  2. Filter out UI events and field changes
  3. Check if already syncing from blocks, return early if so
  4. Generate code from blocks using workspaceToCode()
  5. Compare generated code with editor value
  6. If different, set syncingFromBlocks flag
  7. Update editor using editor.operation() for atomic update
  8. Execute code with runCode()
  9. Clear flag in finally block to ensure it's always cleared
  10. Handle errors gracefully without breaking sync

Building Shape → Code Sync From Scratch

When someone drags a shape handle or changes a slider, we need to update the code in the editor to match the new shape state. This sync must be carefully controlled to prevent infinite loops.

How to Build It Step by Step:

Step 1: Update Shape Parameter The main entry point is updateShapeParameter(), which coordinates all updates:

// In shapeManager.mjs
updateShapeParameter(shapeName, paramName, value, source = 'unknown') {
  // Step 1.1: Update the shape object immediately
  // This is the source of truth - update it first
  this.immediateShapeUpdate(shapeName, paramName, value);

  // Step 1.2: Update the visual immediately
  // Redraw the canvas so user sees the change right away
  this.immediateVisualUpdate();

  // Step 1.3: Update UI (sliders) immediately
  // Keep sliders in sync with shape state
  this.immediateUISync(shapeName, paramName, value, source);

  // Step 1.4: Schedule code update (only if not from code)
  // Don't update code if the change came from code itself - that would be redundant
  if (source !== 'code' && source !== 'editor') {
    this.scheduleCodeUpdate(shapeName, paramName, value);
  }
}

Why This Order:

  1. Shape object first (source of truth)
  2. Visual update second (immediate feedback)
  3. UI sync third (keep controls in sync)
  4. Code update last (delayed, expensive)

Why Check Source: If the change came from code or editor, the code already matches the shape. Updating it would be redundant and could cause loops. We only update code when the change came from canvas interaction (handle drag, slider change).

Step 2: Schedule Code Update with Debouncing

scheduleCodeUpdate(shapeName, paramName, value) {
  // Step 2.1: Clear any pending update
  // This implements debouncing - if a new update comes in before the timer fires,
  // we cancel the old timer and start a new one. This means code only updates
  // after the user stops making changes for the delay period.
  if (this.codeUpdateTimer) {
    clearTimeout(this.codeUpdateTimer);
  }

  // Step 2.2: Schedule new update
  // setTimeout delays the update by 200ms. During dragging, updates fire
  // 60+ times per second. We don't want to update code on every update -
  // that would be slow and cause the editor to constantly change.
  this.codeUpdateTimer = setTimeout(() => {
    this.executeCodeUpdate(shapeName, paramName, value);
  }, 200);
}

Why Debounce: You don't want to update code on every pixel of dragging. That's slow. Instead, wait 200ms after the last change. During dragging, updates fire 60+ times per second. We don't want to update code on every update - that would be slow and cause the editor to constantly change. 200ms is a good balance - fast enough that users see code update quickly after stopping, but long enough to avoid updates during active dragging.

Step 3: Execute Code Update

executeCodeUpdate(shapeName, paramName, value) {
  // Step 3.1: Disable auto-run to prevent code from re-executing
  // When we update code, the editor fires a change event. That would normally
  // trigger runCode(), which would re-execute everything. But we just updated
  // the code to match the shape - we don't want to re-run it. So we disable
  // auto-run temporarily.
  this.disableAutoRun();

  // Step 3.2: Update the code in the editor
  // The parameter manager knows how to find and update the specific parameter
  // in the code. It handles parsing, finding the right line, and updating
  // the value while preserving formatting.
  if (this.parameterManager.updateCodeInEditor) {
    this.parameterManager.updateCodeInEditor(shapeName, paramName, value);
  }

  // Step 3.3: Re-enable auto-run after delay
  // We delay re-enabling to ensure the code update is complete and the change
  // event has been processed. This prevents any race conditions.
  this.enableAutoRunDelayed();
}

Why Disable Auto-Run: When you update code, the editor fires a change event. That would normally trigger runCode(), which would re-execute everything. But we just updated the code to match the shape - we don't want to re-run it. So we disable auto-run temporarily. This prevents the circular execution: shape change → update code → editor fires change → run code → update shape → loop!

Why Delay Re-enable: We delay re-enabling auto-run to ensure the code update is complete and the change event has been processed. This prevents any race conditions where auto-run might trigger before the update is finished.

The Complete Functions:

// In shapeManager.mjs
updateShapeParameter(shapeName, paramName, value, source = 'unknown') {
  // 1. Update the shape object immediately
  this.immediateShapeUpdate(shapeName, paramName, value);

  // 2. Update the visual immediately
  this.immediateVisualUpdate();

  // 3. Update UI (sliders) immediately
  this.immediateUISync(shapeName, paramName, value, source);

  // 4. Schedule code update (only if not from code)
  if (source !== 'code' && source !== 'editor') {
    this.scheduleCodeUpdate(shapeName, paramName, value);
  }
}

scheduleCodeUpdate(shapeName, paramName, value) {
  if (this.codeUpdateTimer) {
    clearTimeout(this.codeUpdateTimer);
  }

  this.codeUpdateTimer = setTimeout(() => {
    this.executeCodeUpdate(shapeName, paramName, value);
  }, 200);
}

executeCodeUpdate(shapeName, paramName, value) {
  // Disable auto-run to prevent code from re-executing
  this.disableAutoRun();

  // Update the code in the editor
  if (this.parameterManager.updateCodeInEditor) {
    this.parameterManager.updateCodeInEditor(shapeName, paramName, value);
  }

  // Re-enable auto-run after delay
  this.enableAutoRunDelayed();
}

Building This Step by Step:

  1. Create updateShapeParameter() method that coordinates all updates
  2. Update shape object immediately (source of truth)
  3. Update visual immediately (canvas redraw)
  4. Update UI immediately (slider sync)
  5. Check if source is not 'code' or 'editor'
  6. If not, schedule code update with debouncing
  7. Create scheduleCodeUpdate() method with debouncing logic
  8. Clear any pending timer (debouncing)
  9. Schedule new update with 200ms delay
  10. Create executeCodeUpdate() method
  11. Disable auto-run to prevent code re-execution
  12. Update code in editor using parameter manager
  13. Re-enable auto-run after delay
  14. This ensures code stays in sync with shape state without causing loops

The Complete Flow

User types in text editor:

  1. Text editor fires change event
  2. Check flags → not syncing, proceed
  3. Debounce 300ms
  4. Parse code → run interpreter → update shapes
  5. If in blocks mode, rebuild blocks (with events disabled)

User moves a block:

  1. Blockly fires change event
  2. Check flags → not syncing, proceed
  3. Generate code from blocks
  4. Set syncingFromBlocks = true
  5. Update text editor (which would fire change event, but flag prevents sync)
  6. Run code → update shapes
  7. Clear flag

User drags shape handle:

  1. Handle system detects drag
  2. Update shape object immediately
  3. Redraw canvas immediately
  4. Update slider value immediately
  5. Schedule code update (200ms delay)
  6. Update code in editor
  7. Disable auto-run
  8. Editor fires change event, but auto-run is disabled, so code doesn't re-execute
  9. Re-enable auto-run after delay

Common Gotchas

Gotcha 1: Forgetting to clear flags Always use try/finally to clear flags. If an error happens, the flag stays set and sync breaks forever.

Gotcha 2: Not checking flags in all paths Every sync handler must check all relevant flags. If you miss one, you get loops.

Gotcha 3: Debounce timing Too short = janky, too long = feels unresponsive. 200-300ms is usually good.

Gotcha 4: Blockly events Always disable Blockly events when rebuilding. Otherwise every block creation triggers a sync.

Gotcha 5: CodeMirror operations Use editor.operation() to batch edits. Otherwise you get multiple change events.

results matching ""

    No results matching ""