Building the Shape Manager From Scratch - Complete Beginner's Guide

The Shape Manager is like the traffic controller of your application. It coordinates between all the different parts: the UI (sliders, buttons), the canvas (visual display), the code editor, and the interpreter. Without it, everything would get out of sync and you'd get infinite loops.

Simple Analogy: Imagine you're editing a document in multiple places at once:

  • You change text in the editor
  • The preview updates
  • The outline panel updates
  • The word count updates

Now imagine if changing the text in the editor caused the editor to change again (because the preview updated the editor, which triggered the preview...). You'd get an infinite loop!

The Shape Manager prevents this by tracking WHERE each change came from and only updating the parts that need updating.

Why the Shape Manager Exists

As your application grows, you'll have MULTIPLE ways to change the same thing:

  1. Users drag sliders in the UI - "I want the circle radius to be 50"
  2. Users drag handles on canvas - "I want to resize this circle by dragging"
  3. Code executes - The interpreter runs code that creates/updates shapes
  4. Blockly blocks - Visual blocks generate code that creates shapes

The Problem: Each of these changes needs to update MULTIPLE parts:

  • The shape object (so the renderer knows what to draw)
  • The visual canvas (so users see the change immediately)
  • The UI sliders (so they show the current value)
  • The code editor (so the code matches what's on screen)

The Critical Challenge: If you're not careful, you get circular updates:

  1. User drags slider → Updates shape → Updates code → Code runs → Updates shape → Updates slider
  2. This creates an infinite loop! Everything keeps updating forever.

The Solution: The Shape Manager tracks WHERE each change came from (the "source"):

  • If change came from "code" → Don't update code (it already matches!)
  • If change came from "slider" → Update everything EXCEPT code immediately, then update code later (after user stops dragging)
  • If change came from "canvas/handle" → Same as slider

This prevents infinite loops while keeping everything in sync.

Real-World Example: Think of a smart home system:

  • You adjust the thermostat manually (slider)
  • The app updates the temperature display (visual)
  • The app sends command to device (code)
  • But it doesn't send the command back to itself (that would be a loop!)

Shape Manager does the same thing - coordinates updates without creating loops.

The Core Challenge: Source Tracking - Explained Simply

What is Source Tracking? Every time something changes, we need to know WHERE that change came from. This "source" tells us what to update and what NOT to update.

Why Track the Source?

Imagine this scenario without source tracking:

  1. User drags slider → Shape updates → Code updates → Code runs → Shape updates → Slider updates → Shape updates → ... INFINITE LOOP!

With source tracking:

  1. User drags slider (source = "slider") → Shape updates → Visual updates → Code updates LATER (not immediately)
  2. Code runs (source = "code") → Shape updates → Visual updates → Code does NOT update (because it already matches)

The Rules Based on Source:

Source = "code" or "editor":

  • Update: Shape object, Visual canvas, UI sliders
  • DON'T Update: Code editor (because code already matches - we just ran it!)
  • Why: The code is the source of truth here, so we don't need to change it back

Source = "slider" or "handle" or "canvas":

  • Update Immediately: Shape object, Visual canvas, UI sliders
  • Update Later (delayed): Code editor (wait 200ms after user stops dragging)
  • Why: User is interacting directly, so show changes immediately. But don't update code constantly while dragging (that would be distracting and slow).

Source = "unknown":

  • Treat as external change
  • Update everything (be safe)
  • Schedule code update

Real-World Analogy: Think of a document editor with live preview:

  • You type in editor (source = "editor") → Preview updates, but editor doesn't change itself
  • You drag preview (source = "preview") → Editor updates to match, but preview doesn't trigger editor change event

The source tells the system "I know where this came from, so I know what to sync."

Visual Example:

User drags slider (source = "slider"):
┌─────────┐
│ Slider  │ ──source="slider"──> Shape Manager
└─────────┘
                │
                ├─> Update Shape Object ✓
                ├─> Update Visual (canvas) ✓
                ├─> Update UI sliders ✓
                └─> Schedule code update (wait 200ms) ✓

Code executes (source = "code"):
┌─────────┐
│  Code   │ ──source="code"──> Shape Manager
└─────────┘
                │
                ├─> Update Shape Object ✓
                ├─> Update Visual (canvas) ✓
                ├─> Update UI sliders ✓
                └─> Update Code Editor ✗ (already matches!)

This prevents loops while keeping everything in sync!

The Problem - Detailed Explanation with More Examples

The Situation - Explained Simply:

You have MULTIPLE ways users can change the same shape. This is like having multiple doors to the same room - people can enter from any door, but everyone needs to know the room state changed.

The Four Ways Users Can Change Shapes:

  1. Slider in UI - User drags a slider to change radius from 30 to 50

    • Real-world analogy: Like adjusting volume with a slider on your phone
    • What happens: User drags slider → value changes → shape should update
    • Example: Slider at 30, user drags to 50, circle radius should become 50
  2. Handle on Canvas - User drags a corner handle to resize the shape

    • Real-world analogy: Like grabbing and resizing a window by its corner
    • What happens: User drags handle → shape resizes → values should update
    • Example: Circle with radius 30, user drags corner handle, circle grows to radius 50
  3. Code Execution - Interpreter runs code like circle(r=50)

    • Real-world analogy: Like typing a command and seeing it execute
    • What happens: Code runs → creates/updates shapes → everything should reflect the code
    • Example: User types shape circle c1 { radius: 50 }, code runs, circle appears with radius 50
  4. Blockly Blocks - User moves a block, which generates code

    • Real-world analogy: Like dragging puzzle pieces that automatically assemble
    • What happens: User moves block → code generates → code runs → shapes update
    • Example: User changes "radius" block from 30 to 50, code updates, shape updates

Each Change Needs to Update Multiple Things - Why This Matters:

When ANY of these changes happen, you need to update multiple places. Think of it like a house with multiple clocks - when time changes, ALL clocks need to update, not just one!

The Four Places That Need Updates:

  1. The Shape Object - The actual data in memory (so renderer knows what to draw)

    • What it is: JavaScript object storing shape data like { type: 'circle', params: { radius: 50 } }
    • Why update it: The renderer reads this to know what to draw
    • Example: shape.params.radius = 50 - Store the new radius value
    • Real-world analogy: Like updating the blueprint - the actual plan that tells builders what to build
  2. The Visual Canvas - Redraw the shape so user sees the change

    • What it is: The HTML5 canvas where shapes are drawn (what user sees)
    • Why update it: User needs visual feedback - they need to SEE the change
    • Example: renderer.redraw() - Redraw all shapes on canvas
    • Real-world analogy: Like updating a TV screen - the screen shows what's happening
  3. The UI Sliders - Update slider values to show current state

    • What it is: Slider controls in the UI panel (like volume slider)
    • Why update it: If user drags handle, slider should move to match (and vice versa)
    • Example: slider.value = 50 - Move slider position to match new value
    • Real-world analogy: Like updating a speedometer - shows current speed
  4. The Code Editor - Update code to match what's on screen

    • What it is: The text editor where code is written (like a Word document)
    • Why update it: Code should match what's displayed (bidirectional sync)
    • Example: Change code from circle(r=30) to circle(r=50) - Code text updates
    • Real-world analogy: Like updating a recipe - the recipe should match what you actually cooked

Why All Four Need Updates:

Imagine if only one updated:

  • Only shape object updates: User drags slider, but nothing changes visually (confusing!)
  • Only canvas updates: User sees change, but shape data is wrong (breaks on next code run!)
  • Only slider updates: Slider moves, but shape doesn't change (useless!)
  • Only code updates: Code changes, but nothing else happens (broken!)

They ALL need to stay in sync! This is called "state synchronization" - keeping multiple copies of the same information in sync.

The Nightmare Scenario - Infinite Loop:

Without careful coordination, you get this:

User drags slider:
  → Updates shape object (radius = 50)
  → Updates canvas (redraw)
  → Updates code editor (circle(r=50))
  → Code editor change event fires
  → Interpreter runs new code
  → Updates shape object again
  → Updates slider (shows 50)
  → Slider change event fires (even though value didn't really change)
  → Updates shape object again
  → Updates code editor again
  → ... INFINITE LOOP! 💥

Everything keeps updating everything else, creating an endless cycle. Your app freezes, uses 100% CPU, and becomes unusable.

Real-World Example: Imagine a smart mirror:

  • You adjust the brightness (slider)
  • Mirror gets brighter (visual)
  • App saves setting (code/data)
  • But if the app updating the data triggers the slider to update, which triggers the app to update, which triggers the slider... you get stuck in a loop!

The Solution: Track WHERE each change came from, and only update what makes sense. This is what the Shape Manager does - it's the traffic controller that prevents these loops.

The Solution: Source Tracking

Every update has a source. If the source is "code", don't update code. If the source is "slider", update everything except code (until user stops dragging).

Understanding the Constructor - Step by Step:

Let's break down what each part of the constructor does and why it exists:

export class ShapeManager {
  constructor() {
    // ... properties explained below ...
  }
}

1. The Shapes Map - The Single Source of Truth:

This stores all the shapes in your application. Think of it like a phone book:

  • Key = Shape name (like "c1" for circle 1, "r1" for rectangle 1)
  • Value = The actual shape object (with type, params, position, etc.)

Why Use a Map Instead of an Object?

  • Maps are faster for frequent additions/deletions
  • Maps preserve the order you add things (objects don't guarantee order)
  • Maps are designed for key-value storage with string keys
this.shapes = new Map();

Real-World Analogy: Think of a filing cabinet:

  • Each drawer (Map entry) is labeled with a shape name ("c1", "r2", etc.)
  • Inside each drawer is the shape's data (what type, where it is, how big, etc.)
  • When you need to find or update a shape, you look it up by name (the key)

2. Component References - Wiring Everything Together:

These are references to other parts of your application. They start as null because those parts don't exist yet when ShapeManager is created.

Why Start as Null? Components are created in a specific order:

  1. ShapeManager is created first
  2. Renderer is created next
  3. Interpreter is created
  4. ParameterManager is created
  5. Editor is created

Since ShapeManager is created first, the other components don't exist yet. We'll "register" them later using special methods.

Why This Pattern? Instead of passing everything in the constructor (which would be messy with 4+ parameters), we use "registration" methods. This is cleaner and more flexible.

this.parameterManager = null;  // Manages UI sliders and parameter controls
this.renderer = null;           // Draws shapes to canvas
this.interpreter = null;       // Executes code and creates shapes
this.editor = null;            // Code editor (CodeMirror instance)

3. Visual Update Throttling - Performance Optimization:

When you drag a handle or slider, updates fire 60+ times per second. If you redrew the canvas every single time, it would be SLOW. Throttling limits redraws to 60fps maximum.

The Math:

  • 60 frames per second = 1 frame every ~16.67 milliseconds
  • So we only redraw if at least 16ms has passed since the last redraw
  • This is smooth enough for humans (we can't see faster than 60fps anyway) but much faster than redrawing 100+ times per second

Real-World Analogy: Think of a flipbook animation:

  • If you flip too fast, it's blurry and hard to see
  • If you flip at just the right speed (60fps), it looks smooth
  • Flipping faster than 60fps doesn't help - your eyes can't see the difference anyway
this.lastVisualUpdate = 0;     // Timestamp of last visual update
this.visualUpdateThrottle = 16; // 60fps max (1000ms / 60 = ~16ms per frame)

4. Code Update Delay - Debouncing:

When dragging a handle, we update the shape and visual immediately (so it feels responsive), but we DELAY updating the code until the user stops dragging.

Why Delay Code Updates?

  • If code updated on every pixel movement, the editor would constantly be changing
  • This would be distracting and confusing
  • Also, updating code triggers events that could cause performance issues

How It Works:

  1. User starts dragging → schedule code update for 200ms from now
  2. User continues dragging → cancel old timer, schedule new one (200ms from now)
  3. User stops dragging → timer fires after 200ms, code updates once

This means code only updates AFTER the user stops making changes for 200ms.

Real-World Analogy: Think of auto-save in a document:

  • You type a character → auto-save starts a 2-second timer
  • You type another character → timer resets, starts new 2-second timer
  • You stop typing → after 2 seconds, document saves
  • This prevents saving on every keystroke (slow!) but saves soon after you stop

Why 200ms?

  • Fast enough that users see code update quickly after stopping
  • Long enough to avoid updates during active dragging
  • A good balance between responsiveness and performance
this.codeUpdateTimer = null;   // setTimeout ID for scheduled code update
this.codeUpdateDelay = 200;     // Wait 200ms after last change before updating code

5. Code Running Flag - Preventing Re-Entrancy:

This flag tracks whether code is currently executing. If code is already running, we ignore new execution requests.

Why This is Needed: Without this flag:

  • Code starts running
  • While it's running, something triggers it to run again
  • Now you have code running twice at the same time
  • This causes race conditions, inconsistent state, and crashes

Real-World Analogy: Think of a washing machine:

  • You press start → washing machine starts
  • While it's running, you can't press start again (the button is disabled)
  • Only after it finishes can you start a new cycle
  • This prevents the machine from trying to do two cycles at once

What Happens With the Flag:

  • Code starts → set isCodeRunning = true
  • New execution request comes in → check flag, if true, ignore it
  • Code finishes → set isCodeRunning = false
  • Now new execution requests are allowed again

This prevents multiple simultaneous executions, which would cause chaos.

this.isCodeRunning = false;

Understanding the Constructor: The ShapeManager constructor sets up the foundation for the entire coordination system. Each property serves a specific purpose:

  1. shapes Map - This is the single source of truth for all shapes. When any component needs to know about shapes, it goes through the ShapeManager. Using a Map provides O(1) lookup time and preserves insertion order.

  2. Component References - These start as null because of initialization order. The ShapeManager is created early, but other components (renderer, interpreter, etc.) are created later. We use registration methods to wire them up, which is more flexible than passing everything in the constructor.

  3. Throttling System - The lastVisualUpdate and visualUpdateThrottle work together to limit redraw frequency. When an update is requested, we check if enough time has passed since the last update. If not, we schedule it for later using requestAnimationFrame.

  4. Code Update Delay - The codeUpdateTimer and codeUpdateDelay implement debouncing for code updates. When a change is requested, we clear any pending timer and start a new one. This means code only updates after the user stops making changes for 200ms.

  5. Code Running Flag - The isCodeRunning flag prevents re-entrancy. If code is already executing, we don't start another execution. This is critical for preventing race conditions and ensuring consistent state.

Component Registration - Explained Much More Simply

What is Component Registration?

Component registration is like giving someone your phone number so they can call you. The Shape Manager needs to know about other components (renderer, editor, etc.) so it can talk to them. Registration is how we give it those references.

Real-World Analogy:

Think of a restaurant:

  • The kitchen (Shape Manager) needs to know:
    • Where the waiters are (renderer - to serve food)
    • Where the cash register is (editor - to record orders)
    • Where the ingredient storage is (parameter manager - to get supplies)
  • But when the restaurant first opens, these things don't exist yet
  • So we "register" them one by one as they're set up

Why Not Pass Everything in the Constructor?

You might wonder: "Why not just pass everything when creating ShapeManager?"

// This would work, but it's messy:
const shapeManager = new ShapeManager(renderer, editor, parameterManager, interpreter);
// 4+ parameters is too many! Hard to remember order, hard to use

Instead, we use registration methods:

// Cleaner - register one at a time:
shapeManager.registerRenderer(renderer);
shapeManager.registerEditor(editor);
// Easy to understand, easy to use

How Registration Works:

Registration methods save references to other components so Shape Manager can communicate with them. Each registration method follows the same pattern: take a component as a parameter and save it to a property.

Understanding "this.parameterManager = parameterManager":

This line saves the parameter manager:

  • Left side: this.parameterManager - The Shape Manager's property (where we're saving it)
  • Right side: parameterManager - The actual parameter manager object (what we're saving)
  • The = sign: Copies the reference (like copying a phone number)

Think of it like this:

  • You have a phone number (the parameterManager object)
  • You write it down in your address book (this.parameterManager)
  • Now you can call them later (use it later)

Real-World Example:

Before registration:

// Shape Manager doesn't know about parameter manager:
shapeManager.parameterManager  // null (doesn't exist)

After registration:

// Register it:
shapeManager.registerParameterManager(myParameterManager);

// Now Shape Manager knows about it:
shapeManager.parameterManager  // myParameterManager (exists!)

All Registration Methods Work the Same Way:

registerParameterManager(parameterManager) {
  this.parameterManager = parameterManager;
}

registerRenderer(renderer) {
  this.renderer = renderer;
}

registerEditor(editor) {
  this.editor = editor;
}

registerInterpreter(interpreter) {
  this.interpreter = interpreter;
}

Why These Methods Exist:

Instead of directly setting properties:

// BAD - Direct property access (not safe):
shapeManager.parameterManager = something;  // Anyone can set it, no control

We use methods:

// GOOD - Method (safe, controlled):
shapeManager.registerParameterManager(something);  // Controlled access, can add validation later

When to Register Components:

Components are registered in a specific order (during initialization):

  1. Renderer first - Shape Manager needs it immediately (to redraw)
  2. Editor second - Needed for code updates
  3. Parameter Manager third - Needed for UI updates
  4. Interpreter last - Needed when code executes

Why This Order:

  • Renderer is most critical (visual feedback)
  • Editor is needed early (code updates happen often)
  • Parameter Manager is optional (UI might not be visible)
  • Interpreter is only needed when code runs (lazy)

Real-World Example - Registration Flow:

// Step 1: Create Shape Manager (no components registered yet)
const shapeManager = new ShapeManager();
// shapeManager.renderer === null
// shapeManager.editor === null

// Step 2: Create Renderer
const renderer = new Renderer(canvas);

// Step 3: Register Renderer with Shape Manager
shapeManager.registerRenderer(renderer);
// shapeManager.renderer === renderer (now exists!)

// Step 4: Create Editor
const editor = CodeMirror.fromTextArea(textarea);

// Step 5: Register Editor with Shape Manager
shapeManager.registerEditor(editor);
// shapeManager.editor === editor (now exists!)

// Now Shape Manager can use both renderer and editor!

registerInterpreter(interpreter) { this.interpreter = interpreter; this.shapes = interpreter?.env?.shapes || new Map(); }

registerEditor(editor) { this.editor = editor; }


**Why register?** The shape manager is created early, but other components are created later. Registration lets you wire them up in the right order.

**If you see an error at this step:**

**Error: `TypeError: Cannot read property 'shapes' of undefined`**
- **What this means:** `interpreter.env` is undefined
- **Common causes:**
  1. Interpreter not initialized: `interpreter` exists but `env` doesn't
  2. Interpreter structure different: no `env` property
  3. Interpreter is null/undefined: passed null instead of interpreter instance
- **Fix:** Use optional chaining: `interpreter?.env?.shapes || new Map()`, or check: `if (interpreter && interpreter.env) { this.shapes = interpreter.env.shapes; }`

**Error: `TypeError: this.shapes.get is not a function` later in code**
- **What this means:** shapes is not a Map after registration
- **Common causes:**
  1. interpreter.env.shapes is not a Map: it's an object `{}` instead
  2. Fallback didn't run: `interpreter?.env?.shapes` exists but isn't a Map
  3. shapes got overwritten with wrong type
- **Fix:** Ensure it's always a Map: `this.shapes = interpreter?.env?.shapes instanceof Map ? interpreter.env.shapes : new Map();`

**Error: Components are null when trying to use them**
- **What this means:** Registration methods not called before using components
- **Common causes:**
  1. Forgot to call registration: `shapeManager.registerRenderer(renderer)` missing
  2. Wrong order: using component before registration
  3. Component creation failed: passed null/undefined to registration
- **Fix:** Always register components in initialization: check `if (!this.renderer) throw new Error('Renderer not registered');` before use

### The Core Update Method

This is the heart of the system:

```javascript
updateShapeParameter(shapeName, paramName, value, source = 'unknown') {
  // This is the central method that coordinates all updates when a shape parameter changes.
  // It's called from many places: sliders, handles, code execution, etc.
  // Step 1: IMMEDIATE shape object update (source of truth)
  const updated = this.immediateShapeUpdate(shapeName, paramName, value);
  if (!updated) return false;  // Early return if shape doesn't exist

  // Step 2: IMMEDIATE visual update (throttled internally to 60fps)
  this.immediateVisualUpdate();

  // Step 3: IMMEDIATE UI sync (sliders, etc.) - source determines what to sync
  this.immediateUISync(shapeName, paramName, value, source);

  // Step 4: SCHEDULED code update (only if change didn't come from code/editor)
  if (source !== 'code' && source !== 'editor') {
    this.scheduleCodeUpdate(shapeName, paramName, value);
  }

  return true;
}

If you see an error at this step:

Error: TypeError: this.immediateShapeUpdate is not a function

  • What this means: Method doesn't exist on ShapeManager
  • Common causes:
    1. Method not implemented: forgot to add it
    2. Typo in method name: immediateShapeUpdate vs immediateShapeUpdat or similar
    3. Method defined outside class (not accessible)
  • Fix: Check method exists in class, verify spelling matches exactly

Error: Infinite update loop (shape keeps updating)

  • What this means: Circular updates between components
  • Common causes:
    1. Source parameter wrong: update marked as 'code' but actually from slider, causing code update which triggers more updates
    2. immediateUISync triggers slider change event which calls updateShapeParameter again
    3. Code execution triggers updates which trigger code execution
  • Fix: Verify source parameter is correct: 'code' for code execution, 'slider' for slider changes, 'handle' for handle drags. Check immediateUISync doesn't trigger events

Error: Shape updates but canvas doesn't redraw

  • What this means: Visual update not working
  • Common causes:
    1. immediateVisualUpdate() not implemented or fails silently
    2. renderer not registered: this.renderer = null
    3. renderer.redraw() has error (check console)
  • Fix: Check renderer registered, verify immediateVisualUpdate() calls renderer.redraw(), check for errors in redraw method

Error: TypeError: Cannot read property 'paramName' of undefined in immediateUISync

  • What this means: Shape doesn't exist when UI sync tries to access it
  • Common causes:
    1. immediateShapeUpdate() returned false but code continued
    2. Shape deleted between update and UI sync (race condition)
    3. immediateUISync() tries to read shape that doesn't exist
  • Fix: Check immediateShapeUpdate() return value, ensure early return if false, verify shape exists in UI sync

Error: Code updates even when source is 'code' (infinite loop)

  • What this means: Source check not working
  • Common causes:
    1. Source comparison wrong: source !== 'code' fails if source is 'Code' (case sensitive)
    2. Source not passed: defaults to 'unknown' which passes check
    3. Multiple sources: code execution also triggers from other source
  • Fix: Check source value: console.log('Source:', source);, ensure source passed correctly, use strict comparison: source !== 'code' && source !== 'editor' ```

Why This Update Order Matters:

The order of operations is carefully designed to provide the best user experience while preventing performance issues and infinite loops. Each step serves a specific purpose and must happen in this exact sequence:

1. Shape Object First - The Foundation:

The shape object is the source of truth. Everything else derives from it. If the shape doesn't exist, nothing else matters, so we check and update it first. Here's why this order is critical:

Why Shape Object Must Be First:

  • It's the authoritative state: The shape object is the definitive record of the shape's state. All other systems read from it to determine what to display or how to behave.
  • Other steps depend on it: Before we can update visuals, UI, or code, we need to know what the new state is. The shape object holds that state.
  • Early validation: By checking if the shape exists first, we can fail fast if there's a problem. This prevents wasted computation trying to update non-existent shapes.
  • Atomic update: Updating the shape object is a single, atomic operation. Once it's updated, all subsequent steps can proceed knowing they're working with valid, current data.

What Happens If We Don't Update Shape First:

  • Renderer might try to draw using old/incorrect shape data
  • UI sliders might show wrong values
  • Code generation might generate code for the wrong state
  • Systems would be out of sync with each other

2. Visual Update Second - Immediate User Feedback:

Users need immediate visual feedback. When they drag a handle, they expect to see the shape move immediately. The visual update is throttled internally to maintain performance, but from the user's perspective, it's immediate.

Why Visual Update Comes Second:

  • User perception: Users see changes on screen first. If visuals lag behind, the interface feels unresponsive and broken.
  • Visual feedback is critical: Visual feedback confirms that user actions are working. Without it, users don't know if their input is being processed.
  • Throttled for performance: Even though we call it "immediate", it's actually throttled to 60fps internally. This gives smooth animation while preventing excessive redraws.

What immediateVisualUpdate() Does:

  • Checks if enough time has passed since last update (throttling)
  • If throttled, skips this update (prevents too many redraws)
  • If not throttled, calls renderer.redraw() to draw all shapes on canvas
  • Updates timestamp to track when last visual update occurred

Why Throttling is Important:

  • Without throttling: Dragging a handle might trigger 100+ redraws per second (unnecessary, causes lag)
  • With throttling: Limited to 60 redraws per second maximum (smooth animation, good performance)
  • Result: Smooth, responsive interface without performance issues

3. UI Sync Third - Keep Controls in Sync:

UI controls (sliders) need to reflect the current state. However, we're smart about this - if the change came from a slider, we don't update the slider (it already has the right value). This prevents unnecessary DOM updates.

Why UI Sync Comes Third:

  • UI is derived from shape state: Sliders display what's in the shape object. We update the shape first, then sync the UI to match.
  • Prevent redundant updates: If the change came from a slider (source='slider'), the slider already has the correct value. Updating it again is wasteful and could cause flickering.
  • DOM updates are expensive: Changing slider values requires DOM manipulation, which is slower than updating JavaScript objects. We only do it when necessary.

What immediateUISync() Does:

  • Checks the source of the change
  • If source is 'canvas' or 'handle', updates UI sliders to match shape state
  • If source is 'slider', skips UI update (slider already has correct value)
  • Finds relevant slider elements using data attributes
  • Updates slider values and corresponding number inputs

Why Source Checking Matters:

  • Prevents unnecessary DOM manipulation
  • Avoids circular updates (slider change → update shape → update slider → update shape...)
  • Improves performance by skipping redundant operations

4. Code Update Last (and Delayed) - Avoid Disruption:

Code updates are expensive and can cause re-execution. We delay them until the user stops making changes. This prevents the editor from constantly changing during dragging, which would be distracting and could cause performance issues.

Why Code Update Comes Last:

  • Code updates trigger re-execution: When code changes, the interpreter re-runs it, which is computationally expensive
  • Code updates are visible to users: Constantly changing code in the editor is distracting and makes it hard to read/edit
  • Code doesn't need to be immediate: Users don't need to see code changes while actively dragging - they can see the visual result instead

What scheduleCodeUpdate() Does:

  • Uses debouncing: Cancels any pending code update timer
  • Schedules a new code update for 200ms in the future
  • If user stops changing (no new updates for 200ms), timer fires and code updates
  • If user continues changing, timer keeps getting reset (code doesn't update until they stop)

Why Debouncing is Critical:

  • During dragging: Updates fire 60+ times per second. Without debouncing, code would update 60+ times, causing:
    • Editor text constantly changing (distracting)
    • Code re-executing constantly (slow)
    • High CPU usage (performance problems)
  • With debouncing: Code only updates once, 200ms after user stops dragging. This gives:
    • Smooth dragging experience (no editor changes during drag)
    • Single code update when done (efficient)
    • Good performance (no constant re-execution)

The Complete Flow Example:

Let's trace what happens when a user drags a slider:

  1. User moves sliderupdateShapeParameter('c1', 'radius', 75, 'slider') called
  2. Step 1: immediateShapeUpdate() → Updates shapes.get('c1').params.radius = 75 (shape object updated)
  3. Step 2: immediateVisualUpdate() → Calls renderer.redraw() → Canvas shows larger circle (user sees change immediately)
  4. Step 3: immediateUISync() → Checks source='slider' → Skips UI update (slider already shows 75, no need to update it)
  5. Step 4: Checks source !== 'code' && source !== 'editor' → true, so calls scheduleCodeUpdate() → Sets timer for 200ms from now
  6. User continues dragging → More calls to updateShapeParameter() → Step 4 keeps resetting the timer (code doesn't update yet)
  7. User stops dragging → 200ms passes with no new updates → Timer fires → Code updates → Editor shows new code

Result: Smooth dragging, immediate visual feedback, code updates only when user is done!

The Source Parameter: The source parameter is critical for preventing infinite loops:

  • 'code' or 'editor' - Change came from code execution. Don't update code (it already matches).
  • 'slider' - Change came from a slider. Update shape, visual, but schedule code update.
  • 'canvas' or 'handle' - Change came from canvas interaction. Update everything, schedule code update.
  • 'unknown' - Default fallback. Treat as external change, schedule code update.

Why Immediate Updates First, Then Scheduled: When dragging a handle, updates fire 60+ times per second. If we updated code on every pixel movement:

  • The editor would constantly change, which is distracting
  • Code would constantly re-execute, causing performance issues
  • The system would feel janky and unresponsive

Instead, we:

  • Update shape and visual immediately (user sees smooth movement)
  • Schedule code update for 200ms after dragging stops
  • This gives immediate feedback while avoiding performance issues

The early return if the shape isn't found prevents errors and wasted work. If someone tries to update a non-existent shape, we fail fast rather than continuing with invalid operations.

Building Immediate Shape Update From Scratch

This method directly modifies the shape object in memory. It's the first step in the update chain - the shape object is the source of truth. This method is called whenever a shape parameter needs to be updated immediately, before any other systems (renderer, UI, code) are updated.

Why This Method Exists:

The shape object stored in the shapes Map is the definitive representation of each shape's state. When something changes (user drags a handle, slider changes, code executes), the shape object must be updated first because:

  1. It's the source of truth: All other systems (renderer, UI, code) derive their state from the shape object
  2. It's in memory: Direct object modification is the fastest way to update state
  3. It's shared: The same shape object is referenced by multiple systems, so updating it updates everything that references it

Understanding Direct Object Modification:

When we modify shape.params.radius = value, we're directly changing the object stored in the Map. This is different from creating a new object - we're modifying the existing one. This is efficient because:

  • No memory allocation needed (not creating a new object)
  • Other references automatically see the change (they point to the same object)
  • Fast - just setting a property value

However, this means we must be careful - if we accidentally modify the wrong shape or modify it incorrectly, all systems using that shape will see the wrong state.

How to Build It Step by Step:

Step 1: Validate Shape Exists

If the shape doesn't exist, we can't update it. Return false early to prevent errors and wasted computation. The shape object must exist before we can modify it.

immediateShapeUpdate(shapeName, paramName, value) {
  // Check if shape exists in the shapes Map
  if (!this.shapes.has(shapeName)) return false;

  // Get the shape object from the Map
  const shape = this.shapes.get(shapeName);

Step 2: Handle Position Parameters

Position parameters are special - they update where the shape is located (x, y coordinates). Position comes in two parts:

  • position_x = horizontal position (left/right)
  • position_y = vertical position (up/down)

But we store them as an array: position: [x, y]

  • Index 0 = x coordinate
  • Index 1 = y coordinate

So when we see position_x, we know to update index 0. When we see position_y, we update index 1.

Understanding Defensive Checks:

Some shapes might be created without transforms. For example, a shape created from code might not have a transform object initially. If we try to access shape.transform.position when shape.transform doesn't exist, we get an error! This is called "defensive programming" - we check before we use, and create if missing.

Why Parse Float:

Values from UI elements (sliders, text inputs) come as STRINGS, not numbers. If you try to do math with strings: "50" + 10 = "5010" (wrong!). parseFloat(value) converts a string to a number: parseFloat("50") = 50 (correct!).

  // Handle position parameters (position_x, position_y)
  if (paramName.startsWith('position_')) {
    // Determine which coordinate (x=0, y=1)
    const index = paramName === 'position_x' ? 0 : 1;

    // Create transform object if it doesn't exist (defensive programming)
if (!shape.transform) {
  shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
}

    // Create position array if it doesn't exist
    if (!shape.transform.position) {
      shape.transform.position = [0, 0];
    }

    // Update the position coordinate (parseFloat converts string to number)
    shape.transform.position[index] = parseFloat(value);

Example:

// User drags slider, value comes as string
const value = "50";  // string from slider

// Convert to number
const numValue = parseFloat(value);  // 50 (number)

// Now we can use it in calculations
shape.transform.position[0] = numValue;  // Works correctly!

Step 3: Handle Rotation Parameter

Rotation controls how much the shape is turned (like spinning a picture).

  } else if (paramName === 'rotation') {
    // Step 3.1: Create transform object if it doesn't exist
    if (!shape.transform) {
      shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
    }

    // Step 3.2: Update rotation (keep in 0-360 range)
    shape.transform.rotation = parseFloat(value) % 360;

Understanding Rotation:

  • Rotation is measured in degrees (0-360)
  • = no rotation (normal)
  • 90° = rotated 90 degrees clockwise
  • 180° = upside down
  • 360° = full circle (back to 0°)

Why Modulo 360 (% 360):

The Problem: If users keep rotating, the rotation value could grow unbounded:

  • Rotate once: rotation = 360°
  • Rotate again: rotation = 720°
  • Rotate again: rotation = 1080°
  • And so on... numbers get huge!

The Solution: Modulo Operator (%) The modulo operator gives you the remainder after division:

  • 370 % 360 = 10 (370 ÷ 360 = 1 remainder 10)
  • 720 % 360 = 0 (720 ÷ 360 = 2 remainder 0)
  • 1080 % 360 = 0 (1080 ÷ 360 = 3 remainder 0)

Why This Works: A rotation of 370° looks exactly the same as 10° (they're the same position after a full circle). So we normalize all rotations to 0-360 range.

Visual Example:

Rotation Values (what we store):
  0°   = pointing right →
  90°  = pointing down ↓
  180° = pointing left ←
  270° = pointing up ↑
  360° = pointing right → (same as 0°)
  370° = pointing right → (same as 10°)

After % 360:
  370° % 360 = 10°  ✓
  720° % 360 = 0°   ✓
  450° % 360 = 90°  ✓

Why This Matters:

  • Keeps rotation values manageable (always 0-360)
  • Prevents numbers from growing infinitely
  • Makes calculations easier and more predictable
  • Ensures rotation behaves consistently

Real-World Analogy: Think of a clock:

  • After 12:00, it goes back to 1:00 (not 13:00)
  • After 360° rotation, we go back to 0° (not keep counting)
  • The modulo operator does this "wrap around" for us

Step 4: Handle Scale Parameters

Scale controls how big or small the shape is (like zooming in/out).

  } else if (paramName.startsWith('scale_')) {
    // Step 4.1: Determine which axis (x=0, y=1)
    const index = paramName === 'scale_x' ? 0 : 1;

    // Step 4.2: Create transform object if it doesn't exist
    if (!shape.transform) {
      shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
    }

    // Step 4.3: Create scale array if it doesn't exist
    if (!shape.transform.scale) {
      shape.transform.scale = [1, 1];
    }

    // Step 4.4: Update scale (prevent negative or zero)
    shape.transform.scale[index] = Math.max(0.1, parseFloat(value));

Understanding Scale:

Scale works like a multiplier:

  • scale: [1, 1] = normal size (100%)
  • scale: [2, 2] = double size (200%)
  • scale: [0.5, 0.5] = half size (50%)
  • scale: [2, 1] = double width, normal height (stretched horizontally)

Scale Has Two Parts (X and Y):

  • scale_x = horizontal scale (width)
  • scale_y = vertical scale (height)
  • Stored as array: scale: [x, y]

Understanding Math.max(0.1, ...):

The Problem: What if someone tries to set scale to 0 or negative?

  • scale = 0 → shape disappears (multiply by 0 = nothing)
  • scale = -1 → shape flips/mirrors (negative scale inverts)
  • scale = -2 → shape flips and doubles (confusing!)

The Solution: Math.max() Math.max(a, b) returns the larger of two values:

  • Math.max(0.1, 5) = 5 (5 is larger)
  • Math.max(0.1, 0) = 0.1 (0.1 is larger than 0)
  • Math.max(0.1, -5) = 0.1 (0.1 is larger than -5)

So Math.max(0.1, parseFloat(value)) ensures scale is always at least 0.1.

Why 0.1 (Not 0)?

  • 0.1 = 10% size (tiny but visible)
  • This prevents the shape from completely disappearing
  • Users can still see and interact with it (even if very small)
  • Better than 0 which would make it invisible

Visual Example:

Scale Values:
  0.1  = 10% size (tiny dot)
  0.5  = 50% size (half size)
  1.0  = 100% size (normal)
  2.0  = 200% size (double)

With Math.max(0.1, value):
  User tries scale = 0    → becomes 0.1 ✓
  User tries scale = -2   → becomes 0.1 ✓
  User tries scale = 0.5  → stays 0.5 ✓
  User tries scale = 2    → stays 2 ✓

Real-World Analogy: Think of a photo editor zoom:

  • You can zoom out to see the whole picture (small scale)
  • But you can't zoom to 0% (photo disappears!)
  • And you usually can't zoom to negative (that would be weird)
  • There's a minimum zoom level to keep the image visible

That's exactly what Math.max(0.1, ...) does - enforces a minimum visible size.

Step 5: Handle Regular Shape Parameters

  } else {
    // Step 5.1: Regular shape parameters (radius, width, height, etc.)
    // Create params object if it doesn't exist
    if (!shape.params) {
      shape.params = {};
    }

    // Step 5.2: Update the parameter
    shape.params[paramName] = value;
  }

  // Step 5.3: Return success
  return true;
}

Why Separate Transform from Params - Important Distinction:

This is a critical concept to understand:

Transform Properties (position, rotation, scale):

  • These DON'T change the shape's actual geometry (the points that make up the shape)
  • They change WHERE and HOW the shape is drawn
  • Applied by the renderer using canvas transforms (like moving/rotating the canvas before drawing)
  • Think of it like moving/rotating a photo frame - the photo inside doesn't change, just where/how it's displayed

Shape Params (radius, width, height, etc.):

  • These DO change the shape's actual geometry
  • They change the points that make up the shape
  • The shape itself is different (bigger circle, wider rectangle, etc.)
  • Think of it like changing the photo itself - making it bigger or cropping it

Visual Example:

Changing Transform (position):

Original shape at (0, 0):
  Shape geometry: circle with radius 50
  Transform: position [0, 0]

After changing position to [100, 50]:
  Shape geometry: STILL circle with radius 50 (unchanged!)
  Transform: position [100, 50] (changed!)
  Result: Same circle, just drawn at a different location

Changing Params (radius):

Original shape:
  Shape geometry: circle with radius 50
  Transform: position [0, 0]

After changing radius to 75:
  Shape geometry: circle with radius 75 (changed!)
  Transform: position [0, 0] (unchanged)
  Result: Different circle (bigger), at the same location

Why This Separation Matters:

Performance:

  • Changing transform is fast (just move/rotate canvas, draw same points)
  • Changing geometry requires recalculating all points (slower)

Flexibility:

  • You can move/rotate a shape without changing its size
  • You can resize a shape without moving it
  • You can combine both - resize AND move at the same time

Clean Code:

  • Transform = "how to draw it"
  • Params = "what to draw"
  • Clear separation of concerns

Real-World Analogy: Think of a drawing on paper:

  • Transform = Where you place the paper, how you rotate it (the frame/display)
  • Params = What's actually drawn on the paper (the content itself)

You can move the paper around (transform) without changing what's drawn (params), or you can draw something different on the paper (params) while keeping it in the same place (transform).

The Complete Function:

immediateShapeUpdate(shapeName, paramName, value) {
  if (!this.shapes.has(shapeName)) return false;

  const shape = this.shapes.get(shapeName);

  // Handle different parameter types
  if (paramName.startsWith('position_')) {
    const index = paramName === 'position_x' ? 0 : 1;
    if (!shape.transform) shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
    if (!shape.transform.position) shape.transform.position = [0, 0];
    shape.transform.position[index] = parseFloat(value);

  } else if (paramName === 'rotation') {
    if (!shape.transform) shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
    shape.transform.rotation = parseFloat(value) % 360;

  } else if (paramName.startsWith('scale_')) {
    const index = paramName === 'scale_x' ? 0 : 1;
    if (!shape.transform) shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
    if (!shape.transform.scale) shape.transform.scale = [1, 1];
    shape.transform.scale[index] = Math.max(0.1, parseFloat(value));

  } else {
    // Regular shape parameters
    if (!shape.params) shape.params = {};
    shape.params[paramName] = value;
  }

  return true;
}

Building This Step by Step:

  1. Create immediateShapeUpdate() method with shape name, parameter name, and value
  2. Validate shape exists in shapes Map, return false if not
  3. Get shape object from Map
  4. Check if parameter is position (starts with 'position_')
  5. If position, determine index (x=0, y=1) and update position array
  6. Check if parameter is rotation
  7. If rotation, update rotation with modulo 360
  8. Check if parameter is scale (starts with 'scale_')
  9. If scale, determine index and update scale array with minimum 0.1
  10. Otherwise, update regular shape parameter in params object
  11. Return true to indicate success

Building Immediate Visual Update From Scratch

This method redraws the canvas to show the updated shape. It's throttled to maintain performance during rapid updates (like dragging). Visual updates are critical for user feedback - when a user drags a handle or moves a slider, they expect to see the shape change immediately on the canvas.

Understanding Visual Updates:

When a shape parameter changes, the shape object in memory is updated first (via immediateShapeUpdate). However, the canvas doesn't automatically reflect these changes - it still shows the old visual representation. The immediateVisualUpdate method bridges this gap by telling the renderer to redraw the canvas, which reads the updated shape data and displays it visually.

Why This Method is Called "Immediate":

The method is called "immediate" because it's triggered synchronously right after the shape object is updated. However, the actual canvas redraw is throttled internally to prevent performance issues. From the caller's perspective, it's immediate (no delay before the update is scheduled), but from a rendering perspective, multiple rapid calls may be batched together.

How to Build It Step by Step:

Step 1: Validate Renderer Exists

immediateVisualUpdate() {
  // Step 1.1: Check if renderer is registered
  if (!this.renderer) return;

Why Validate:

If the renderer isn't registered, we can't redraw. Return early to prevent errors. The renderer component must be registered with the Shape Manager before visual updates can occur. This check prevents TypeError exceptions that would occur if we tried to call this.renderer.redraw() when this.renderer is null or undefined.

When Renderer Might Not Be Registered:

  • Application is still initializing (components being created in sequence)
  • Renderer failed to initialize (error during creation)
  • Renderer was intentionally removed (shutting down)
  • Registration method wasn't called (programming error)

Why Early Return:

Early return is a defensive programming pattern. Instead of trying to proceed and crashing, we check for prerequisites and exit immediately if they're not met. This makes the code more robust and prevents cascading errors.

Step 2: Check Throttle Time

Throttling is a performance optimization technique that limits how frequently a function can execute. In this case, we limit visual updates to a maximum of 60 times per second (60 FPS), which is the standard refresh rate for smooth animation.

  // Step 2.1: Get current timestamp
  const now = Date.now();

  // Step 2.2: Check if enough time has passed since last update
  if (now - this.lastVisualUpdate < this.visualUpdateThrottle) {

Understanding Timestamps:

Date.now() returns the current time in milliseconds since January 1, 1970 (Unix epoch). We store this timestamp each time we perform a visual update in this.lastVisualUpdate. By comparing the current time to the last update time, we can determine if enough time has passed.

Why Throttle:

If you redraw on every slider movement or handle drag, it's slow. During rapid interactions:

  • User drags handle → updateShapeParameter called 60+ times per second
  • Each call triggers immediateVisualUpdate
  • Without throttling, canvas would redraw 60+ times per second
  • Each redraw is expensive (clears canvas, draws all shapes, applies styles, etc.)
  • Result: Laggy, unresponsive interface

Throttling limits redraws to 60fps (every ~16ms), which is:

  • Smooth enough for human perception (humans can't distinguish above 60fps)
  • Performant (not overloading the browser with excessive redraws)
  • Balanced (good user experience without sacrificing performance)

The Math Behind Throttling:

  • 60 FPS = 60 frames per second
  • 1 second = 1000 milliseconds
  • Time per frame = 1000ms ÷ 60 = ~16.67ms
  • So visualUpdateThrottle = 16 means "wait at least 16ms between updates"

Throttling vs Debouncing:

These are different techniques:

  • Throttling (what we use here): "Execute at most once per X milliseconds" - ensures updates happen regularly but not too frequently
  • Debouncing: "Execute only after X milliseconds of inactivity" - waits until user stops, then executes once

For visual updates, throttling is better because users need continuous feedback while interacting. For code updates, debouncing is better because we want to wait until the user finishes editing.

What Happens When Throttled:

When the check now - this.lastVisualUpdate < this.visualUpdateThrottle is true, it means not enough time has passed since the last update. In this case, we skip the immediate redraw. However, the update request isn't lost - if requestAnimationFrame is used, it will be scheduled for the next frame, ensuring updates still happen smoothly.

Step 3: Schedule Delayed Update

When throttling prevents an immediate update, we still want the update to happen eventually. requestAnimationFrame is the browser's way of scheduling work that should happen before the next repaint.

    // Step 3.1: Not enough time has passed - schedule for next frame
    requestAnimationFrame(() => {
      if (this.renderer.redraw) {
        this.renderer.redraw();
      }
    });

Understanding the Throttled Path:

When the throttle check now - this.lastVisualUpdate < this.visualUpdateThrottle evaluates to true, it means we're trying to update too frequently. Instead of completely skipping the update (which would mean the user never sees their change), we schedule it for the next available animation frame. This ensures the update still happens, just slightly delayed to respect the throttling limits.

Why requestAnimationFrame:

requestAnimationFrame() schedules the redraw for the next animation frame. This ensures:

  1. Optimal timing: The browser calls your callback when it's ready to paint (typically at 60fps)
  2. Batch updates: Multiple rapid calls to requestAnimationFrame are batched into a single frame
  3. Performance: The browser can optimize when to call your code
  4. Smooth animation: Updates are synchronized with the display refresh rate

How requestAnimationFrame Works - Detailed Step-by-Step:

When you call requestAnimationFrame(callback), the browser performs these steps:

Step 3.1.1: Registration Phase

  • Your callback function is added to the browser's internal animation queue
  • The browser doesn't execute it immediately
  • Multiple calls to requestAnimationFrame can queue up multiple callbacks
  • The browser keeps track of when the next frame should be painted

Step 3.1.2: Waiting Phase

  • The browser waits until it's ready to paint a new frame
  • This happens at the display's refresh rate (usually 60 times per second = every ~16.67ms)
  • The browser may be doing other work (processing events, layout calculations, etc.)

Step 3.1.3: Execution Phase

  • When the browser is ready to paint, it calls all queued callbacks
  • Callbacks execute in the order they were registered (first-in, first-out)
  • This happens BEFORE the actual screen painting occurs
  • After callbacks run, the browser paints the frame to the screen

Step 3.1.4: Completion Phase

  • After painting, the browser waits for the next frame cycle
  • If any callback calls requestAnimationFrame again, it gets queued for the next frame
  • This creates a continuous animation loop

Visual Timeline Example:

Time 0ms:   updateShapeParameter() called → immediateVisualUpdate() called
Time 0ms:   Throttle check: only 5ms since last update (need 16ms) → THROTTLED
Time 0ms:   requestAnimationFrame() called → Callback queued
Time 0-16ms: Browser processing other tasks...
Time 16ms:  Browser ready to paint → Calls queued callback
Time 16ms:  Callback executes → renderer.redraw() called
Time 16ms:  Canvas updated → Frame painted to screen

The Callback Function - Detailed Breakdown:

The arrow function () => { ... } is the callback that will be executed on the next frame. Let's break down what happens inside:

Step 3.2.1: Method Existence Check

if (this.renderer.redraw) {
  • This checks if the redraw method exists on the renderer object
  • Uses JavaScript's truthy/falsy evaluation
  • If redraw doesn't exist, the check is false and the code inside doesn't execute

Step 3.2.2: Why This Check is Necessary

Even though we checked this.renderer exists at the start of the function, we still check this.renderer.redraw exists. This is defensive programming because:

  • Renderer structure might vary: Different renderer implementations might have different methods
  • Renderer might be partially initialized: The renderer object exists but isn't fully set up yet
  • Future changes: Code might be refactored and the method name might change
  • Error prevention: Without this check, we'd get TypeError: this.renderer.redraw is not a function if the method doesn't exist

Step 3.2.3: Execute Redraw

this.renderer.redraw();
  • Calls the renderer's redraw() method
  • This method is responsible for:
    1. Clearing the canvas
    2. Drawing the background (grid, rulers, etc.)
    3. Iterating through all shapes
    4. Drawing each shape on the canvas
    5. Drawing selection highlights and handles if needed
  • The entire canvas is redrawn, showing the updated shape state

Why Use requestAnimationFrame Instead of setTimeout:

Both requestAnimationFrame and setTimeout can delay execution, but requestAnimationFrame is superior for visual updates:

requestAnimationFrame Advantages:

  • Synchronized with display refresh rate (smooth animation)
  • Browser optimizes when to call (pauses when tab is hidden)
  • Automatically batches multiple calls (efficient)
  • Designed specifically for animations and visual updates

setTimeout Disadvantages:

  • Fixed delay regardless of display refresh rate (might miss frames)
  • Doesn't pause when tab is hidden (wastes resources)
  • Not optimized for visual updates
  • Can cause janky animation if timing doesn't match refresh rate

Example Comparison:

// Using setTimeout (NOT ideal)
setTimeout(() => {
  renderer.redraw();
}, 16);  // Try to match 60fps, but not guaranteed

// Using requestAnimationFrame (IDEAL)
requestAnimationFrame(() => {
  renderer.redraw();
});  // Browser handles timing perfectly

Performance Impact:

When throttling prevents immediate updates, using requestAnimationFrame ensures:

  • Updates still happen (user sees their changes)
  • Updates are batched efficiently (browser handles timing)
  • Performance is optimized (browser controls execution)
  • Smooth animation (synchronized with display)

Step 4: Immediate Update

  } else {
    // Step 4.1: Enough time has passed - update immediately
    if (this.renderer.redraw) {
      this.renderer.redraw();
    }

    // Step 4.2: Update last update timestamp
    this.lastVisualUpdate = now;
  }
}

Why Immediate Update: If enough time has passed since the last update, we can redraw immediately. This provides the fastest possible visual feedback when updates are infrequent. We update lastVisualUpdate to track when this update occurred.

The Complete Function:

immediateVisualUpdate() {
  if (!this.renderer) return;

  const now = Date.now();

  // Throttle to 60fps
  if (now - this.lastVisualUpdate < this.visualUpdateThrottle) {
    requestAnimationFrame(() => {
      if (this.renderer.redraw) {
        this.renderer.redraw();
      }
    });
  } else {
    // Immediate update
    if (this.renderer.redraw) {
      this.renderer.redraw();
    }
    this.lastVisualUpdate = now;
  }
}

Building This Step by Step:

  1. Create immediateVisualUpdate() method
  2. Validate renderer exists, return early if not
  3. Get current timestamp with Date.now()
  4. Check if time since last update is less than throttle interval
  5. If throttled, schedule redraw with requestAnimationFrame()
  6. If not throttled, redraw immediately
  7. Update lastVisualUpdate timestamp after immediate update
  8. This ensures smooth 60fps updates without overwhelming the browser

Building Immediate UI Sync From Scratch

This method updates UI controls (sliders, inputs) to match the shape's current state. It only updates when the change came from canvas interaction, not from sliders themselves.

How to Build It Step by Step:

Step 1: Check Source and Update Conditionally

immediateUISync(shapeName, paramName, value, source) {
  // Step 1.1: Only update sliders if change came from canvas
  // If change came from slider, slider already has the right value
  if (source === 'canvas' && this.parameterManager) {
    this.updateSliderValueImmediate(shapeName, paramName, value);
  }
}

Why Only from Canvas:

If the change came from a slider, the slider already has the right value. No need to update it - that would be redundant and could cause flickering or performance issues. We only update UI when the change came from canvas interaction (like dragging a handle).

Understanding the Source Parameter Logic:

The source parameter tells us where the change originated from. This is critical for preventing unnecessary work and circular updates:

  • source === 'canvas' or 'handle': Change came from canvas interaction (dragging a handle, moving a shape). The slider doesn't know about this change yet, so we need to update it to reflect the new value. This keeps the UI in sync with the shape state.

  • source === 'slider': Change came from the slider itself (user moved the slider). The slider already has the correct value because the user just set it. Updating it again would be redundant and could cause:

    • Visual flickering (slider value changing unnecessarily)
    • Performance issues (unnecessary DOM manipulation)
    • Potential event loops (if updating triggers another change event)
  • source === 'code' or 'editor': Change came from code execution. Typically, we don't update UI sliders in this case because code execution might have created or modified shapes in ways that don't directly map to slider values. However, this depends on the specific implementation.

The Optional Chaining Operator (?.):

The code this.parameterManager?.paramsList uses optional chaining. This is a modern JavaScript feature that safely accesses nested properties:

  • If this.parameterManager is null or undefined, the expression returns undefined (doesn't throw an error)
  • If this.parameterManager exists, it accesses paramsList
  • This prevents TypeError: Cannot read property 'paramsList' of null errors

This is safer than writing:

if (this.parameterManager && this.parameterManager.paramsList) {
  // ...
}

Step 2: Find Matching Sliders

updateSliderValueImmediate(shapeName, paramName, value) {
  // Step 2.1: Validate parameter manager exists
  if (!this.parameterManager?.paramsList) return;

  // Step 2.2: Find all matching sliders using data attributes
  // Data attributes allow us to identify which slider controls which parameter
  const elements = this.parameterManager.paramsList.querySelectorAll(
    `[data-shape-name="${shapeName}"][data-param-name="${paramName}"]`
  );

Understanding querySelectorAll:

querySelectorAll() is a DOM method that finds all elements matching a CSS selector. It returns a NodeList (array-like object) containing all matching elements. Unlike querySelector() which returns only the first match, querySelectorAll() returns all matches.

Why Find Multiple Elements:

There might be multiple sliders for the same parameter in different parts of the UI:

  • Main parameter panel
  • Context menus
  • Property inspectors
  • Sidebars

We want to update all of them to keep the UI consistent.

Understanding the CSS Selector:

The selector [data-shape-name="${shapeName}"][data-param-name="${paramName}"] uses attribute selectors:

  • [data-shape-name="${shapeName}"] - Matches elements with data-shape-name attribute equal to the shape name
  • [data-param-name="${paramName}"] - Matches elements with data-param-name attribute equal to the parameter name
  • Combined with no space between them - Both conditions must be true (AND logic)

Template Literals in Selectors:

The ${shapeName} and ${paramName} are template literals. They're interpolated before the selector string is created:

  • If shapeName = 'c1' and paramName = 'radius'
  • The selector becomes: [data-shape-name="c1"][data-param-name="radius"]

Why Data Attributes:

Data attributes (data-shape-name, data-param-name) allow us to identify which slider controls which parameter. This is more reliable than class names or IDs because:

  • Semantic: The attributes clearly indicate what they represent (shape name, parameter name)
  • Flexible: Can have multiple sliders with the same data attributes (unlike IDs which must be unique)
  • Accessible: Easy to query using querySelector or querySelectorAll
  • Standard: HTML5 standard way to store custom data on elements
  • Maintainable: If you change the slider's class name or structure, the data attributes remain the same

Alternative Approaches (and why they're worse):

  • Using IDs: id="slider-c1-radius" - IDs must be unique, so you can't have multiple sliders for the same parameter
  • Using class names: class="slider-c1-radius" - Less semantic, harder to query precisely
  • Using parent element traversal: Looking for sliders by their position in the DOM - Fragile, breaks if HTML structure changes
  • Using data attributes: Clean, semantic, flexible - Best approach!

Step 3: Update Each Slider

  // Step 3.1: Loop through all matching elements
  elements.forEach(element => {
    // Step 3.2: Check if element is a range slider
    if (element.type === 'range') {
      // Step 3.3: Update slider value
      element.value = value;

      // Step 3.4: Update data attributes for tracking
      element.dataset.currentValue = value;
      element.dataset.originalValue = value;

Why Update Data Attributes: The currentValue and originalValue data attributes help track the slider's state. This is useful for undo/redo, validation, and other features that need to know the slider's history.

Step 4: Update Corresponding Number Input

      // Step 4.1: Find corresponding number input (if it exists)
      const input = element.parentElement.querySelector('.parameter-value');
      if (input) {
        // Step 4.2: Update input value
        input.value = value;

        // Step 4.3: Update input data attributes
        input.dataset.currentValue = value;
        input.dataset.originalValue = value;
      }
    }
  });
}

Why Update Number Input: Many UI designs have both a slider and a number input for the same parameter. The slider is for dragging, the input is for precise typing. We update both to keep them in sync.

The Complete Functions:

immediateUISync(shapeName, paramName, value, source) {
  // Only update sliders if change came from canvas
  if (source === 'canvas' && this.parameterManager) {
    this.updateSliderValueImmediate(shapeName, paramName, value);
  }
}

updateSliderValueImmediate(shapeName, paramName, value) {
  if (!this.parameterManager?.paramsList) return;

  // Find all matching sliders
  const elements = this.parameterManager.paramsList.querySelectorAll(
    `[data-shape-name="${shapeName}"][data-param-name="${paramName}"]`
  );

  elements.forEach(element => {
    if (element.type === 'range') {
      element.value = value;
      element.dataset.currentValue = value;
      element.dataset.originalValue = value;

      // Update corresponding number input
      const input = element.parentElement.querySelector('.parameter-value');
      if (input) {
        input.value = value;
        input.dataset.currentValue = value;
        input.dataset.originalValue = value;
      }
    }
  });
}

Building This Step by Step:

  1. Create immediateUISync() method with shape name, parameter name, value, and source
  2. Check if source is 'canvas' and parameter manager exists
  3. If yes, call updateSliderValueImmediate()
  4. Create updateSliderValueImmediate() method
  5. Validate parameter manager and params list exist
  6. Find all matching sliders using querySelectorAll with data attributes
  7. Loop through each element
  8. Check if element is a range slider
  9. Update slider value and data attributes
  10. Find corresponding number input in parent element
  11. Update input value and data attributes if input exists
  12. This keeps UI controls in sync with shape state

Building Scheduled Code Update From Scratch

This method updates the code in the editor to match the shape's current state. It uses debouncing to delay updates until the user stops making changes.

How to Build It Step by Step:

Step 1: Schedule the Update with Debouncing

scheduleCodeUpdate(shapeName, paramName, value) {
  // Step 1.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 1.2: Schedule new update
  // setTimeout delays the update by codeUpdateDelay milliseconds (typically 200ms)
  this.codeUpdateTimer = setTimeout(() => {
    this.executeCodeUpdate(shapeName, paramName, value);
  }, this.codeUpdateDelay);
}

Why Debouncing:

When dragging a handle or slider, updates fire 60+ times per second. If we updated the code on every update, the editor would constantly change, which would be distracting and could cause performance issues. Debouncing delays the code update until the user stops making changes for 200ms. This provides a good balance - fast enough that users see code update quickly after stopping, but long enough to avoid updates during active dragging.

Understanding Debouncing in Detail:

Debouncing is a programming technique that ensures a function only executes after a period of inactivity. In our case, we want to update the code only after the user stops making changes for a certain amount of time (200ms).

How Debouncing Works:

  1. Initial Call: User starts dragging → scheduleCodeUpdate() called → Timer starts (200ms countdown)
  2. Rapid Updates: User continues dragging → scheduleCodeUpdate() called repeatedly → Each call cancels the previous timer and starts a new one
  3. User Stops: User stops dragging → No more calls to scheduleCodeUpdate()
  4. Timer Fires: 200ms passes with no new calls → Timer fires → Code update executes

Why 200ms is a Good Delay:

  • Too Short (e.g., 50ms): Code would update too frequently, still causing distraction during rapid interactions
  • Too Long (e.g., 1000ms): Users would wait too long to see code update, making the system feel unresponsive
  • 200ms: Good balance - users stop dragging, and within 200ms (imperceptible delay), code updates. This feels instant while avoiding unnecessary updates.

The Debouncing Pattern:

if (this.codeUpdateTimer) {
  clearTimeout(this.codeUpdateTimer);  // Cancel previous timer
}
this.codeUpdateTimer = setTimeout(() => {
  // Execute after delay
}, delay);

This pattern ensures:

  • If function is called multiple times rapidly, only the last call's timer matters
  • Function executes only after the delay period with no new calls
  • Each new call resets the timer, extending the wait period

Real-World Analogy:

Think of an elevator:

  • Person presses button → Door close timer starts (5 seconds)
  • Another person presses button → Timer resets (5 more seconds)
  • More people press button → Timer keeps resetting
  • Finally, no one presses button → 5 seconds pass → Door closes

This is exactly how debouncing works - the action (door closing / code updating) only happens after a period of inactivity.

Performance Benefits:

Without debouncing:

  • 60 updates per second × 200ms delay = 12 code updates per second
  • Each code update: Parse code, find parameter, update value, trigger editor change event, potential re-execution
  • Result: High CPU usage, constant editor changes, poor performance

With debouncing:

  • User drags for 2 seconds → Hundreds of scheduleCodeUpdate() calls
  • Only 1 code update actually happens (after user stops)
  • Result: Smooth interaction, single code update, good performance

Step 2: Execute the Code Update

executeCodeUpdate(shapeName, paramName, value) {
  // Step 2.1: Validate editor and parameter manager exist
  if (!this.editor || !this.parameterManager) return;

  // Step 2.2: Disable auto-run to prevent code re-execution
  this.disableAutoRun();

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. Disabling auto-run prevents this circular execution.

Step 3: Update the Code

  // Step 3.1: Update the code in the editor
  // The parameter manager has a method that knows how to update code for a specific parameter
  if (this.parameterManager.updateCodeInEditor) {
    this.parameterManager.updateCodeInEditor(shapeName, paramName, value);
  }

Why Delegate to Parameter Manager: The parameter manager knows how to parse and update code for specific parameters. It handles finding the right line, updating the value, and preserving code formatting. This separation of concerns keeps the shape manager focused on coordination, not code manipulation.

Step 4: Re-enable Auto-Run

  // Step 4.1: 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 Delayed 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:

scheduleCodeUpdate(shapeName, paramName, value) {
  // Clear any pending update
  if (this.codeUpdateTimer) {
    clearTimeout(this.codeUpdateTimer);
  }

  // Schedule new update
  this.codeUpdateTimer = setTimeout(() => {
    this.executeCodeUpdate(shapeName, paramName, value);
  }, this.codeUpdateDelay);
}

executeCodeUpdate(shapeName, paramName, value) {
  if (!this.editor || !this.parameterManager) return;

  // Disable auto-run to prevent code re-execution
  this.disableAutoRun();

  // Update the code
  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 scheduleCodeUpdate() method with shape name, parameter name, and value
  2. Check if there's a pending timer, clear it if so (debouncing)
  3. Schedule new update with setTimeout() using codeUpdateDelay
  4. Create executeCodeUpdate() method
  5. Validate editor and parameter manager exist
  6. Disable auto-run to prevent code re-execution
  7. Call parameter manager's updateCodeInEditor() method
  8. Re-enable auto-run after delay
  9. This ensures code stays in sync with shape state without causing loops

Building Canvas Interaction Handlers From Scratch

These methods are called when users interact with shapes on the canvas (dragging handles, moving shapes, rotating). They delegate to the core update method with the 'canvas' source.

How to Build It Step by Step:

Step 1: Create Generic Shape Change Handler

onCanvasShapeChange(shapeName, paramName, value) {
  // Step 1.1: Delegate to core update method
  // This handles any shape parameter change from canvas interaction
  // Source is 'canvas' so the system knows to update sliders
  return this.updateShapeParameter(shapeName, paramName, value, 'canvas');
}

Why Delegate: This method is a thin wrapper around updateShapeParameter(). It sets the source to 'canvas' so the system knows the change came from canvas interaction and should update sliders accordingly.

Step 2: Create Position Change Handler

onCanvasPositionChange(shapeName, position) {
  // Step 2.1: Update X coordinate
  // Position is an array [x, y], so we update each coordinate separately
  const xUpdated = this.updateShapeParameter(shapeName, 'position_x', position[0], 'canvas');

  // Step 2.2: Update Y coordinate
  const yUpdated = this.updateShapeParameter(shapeName, 'position_y', position[1], 'canvas');

  // Step 2.3: Return true only if both updates succeeded
  return xUpdated && yUpdated;
}

Why Separate Updates: Position has two components (x and y). We update each separately because updateShapeParameter() handles one parameter at a time. We return true only if both updates succeeded.

Step 3: Create Rotation Change Handler

onCanvasRotationChange(shapeName, rotation) {
  // Step 3.1: Update rotation parameter
  // Rotation is a single value, so this is straightforward
  return this.updateShapeParameter(shapeName, 'rotation', rotation, 'canvas');
}

Why Simple: Rotation is a single value, so this handler is straightforward - just delegate to the core update method.

The Complete Functions:

onCanvasShapeChange(shapeName, paramName, value) {
  return this.updateShapeParameter(shapeName, paramName, value, 'canvas');
}

onCanvasPositionChange(shapeName, position) {
  const xUpdated = this.updateShapeParameter(shapeName, 'position_x', position[0], 'canvas');
  const yUpdated = this.updateShapeParameter(shapeName, 'position_y', position[1], 'canvas');
  return xUpdated && yUpdated;
}

onCanvasRotationChange(shapeName, rotation) {
  return this.updateShapeParameter(shapeName, 'rotation', rotation, 'canvas');
}

Why Source is 'canvas': The 'canvas' source tells the system the change came from canvas interaction, so it should update sliders. This is important because when users drag shapes on the canvas, the sliders need to reflect the new values. If the change came from a slider, we wouldn't update sliders (they already have the right value).

Building This Step by Step:

  1. Create onCanvasShapeChange() method for generic parameter changes
  2. Delegate to updateShapeParameter() with 'canvas' source
  3. Create onCanvasPositionChange() method for position changes
  4. Update X coordinate separately
  5. Update Y coordinate separately
  6. Return true only if both updates succeeded
  7. Create onCanvasRotationChange() method for rotation changes
  8. Delegate to updateShapeParameter() with 'canvas' source
  9. These handlers provide a clean interface for canvas interactions

Building Slider Interaction Handlers From Scratch

This method is called when users drag sliders in the UI. It delegates to the core update method with the 'slider' source.

How to Build It Step by Step:

Step 1: Create Slider Change Handler

onSliderChange(shapeName, paramName, value, isIntermediate = false) {
  // Step 1.1: Delegate to core update method
  // Source is 'slider' so the system knows NOT to update sliders
  // (they already have the right value)
  return this.updateShapeParameter(shapeName, paramName, value, 'slider');
}

Why Source is 'slider': The 'slider' source tells the system the change came from a slider, so it shouldn't update sliders (they already have the right value). This prevents redundant updates and potential flickering.

Why isIntermediate Parameter: The isIntermediate parameter indicates if the slider is still being dragged. This could be used for optimization (e.g., skip code updates during dragging), but in this implementation, it's passed through for potential future use.

The Complete Function:

onSliderChange(shapeName, paramName, value, isIntermediate = false) {
  return this.updateShapeParameter(shapeName, paramName, value, 'slider');
}

Building This Step by Step:

  1. Create onSliderChange() method with shape name, parameter name, value, and intermediate flag
  2. Delegate to updateShapeParameter() with 'slider' source
  3. Return the result from the update method
  4. This handler provides a clean interface for slider interactions

Building Code Execution Integration From Scratch

When code runs, shapes are created or updated. This method integrates with the code execution system to keep everything in sync.

How to Build It Step by Step:

Step 1: Mark Code as Running

updateFromCode(interpreter) {
  // Step 1.1: Mark code as running
  // This flag prevents shape updates from triggering code updates
  // during code execution, which would cause infinite loops
  this.markCodeRunning(true);

Why Mark Code Running: During code execution, you don't want shape updates to trigger code updates. That would cause loops: code runs → updates shapes → shape update triggers code update → code runs again → loop! The flag prevents this.

Step 2: Update Shapes Reference

  try {
    // Step 2.1: Update our shapes reference
    // The interpreter's environment contains the shapes Map
    // We update our reference to point to the same Map
    // This ensures we're working with the latest shapes
    if (interpreter?.env?.shapes) {
      this.shapes = interpreter.env.shapes;
    }

Why Update Reference: The interpreter creates and manages shapes in its environment. We update our reference to point to the same Map so we're always working with the latest shapes. This is more efficient than copying.

Step 3: Update Visual Immediately

    // Step 3.1: Update visual immediately
    // Redraw the canvas to show the new/updated shapes
    this.immediateVisualUpdate();

Why Immediate Visual Update: Users expect to see changes immediately when code runs. We update the visual right away so the canvas reflects the new state.

Step 4: Update Parameter Manager

    // Step 3.2: Update parameter manager if menu is visible
    // The parameter manager needs to know about new shapes
    // We delay this slightly (50ms) to ensure shapes are fully initialized
    if (this.parameterManager?.menuVisible) {
      setTimeout(() => {
        this.parameterManager.updateWithLatestInterpreter();
      }, 50);
    }

Why Delay Parameter Manager Update: We delay the parameter manager update slightly (50ms) to ensure shapes are fully initialized. This prevents race conditions where the parameter manager might try to access shapes before they're ready.

Step 5: Clear Running Flag

  } finally {
    // Step 5.1: Always clear the running flag
    // This is critical - even if an error occurs, we must clear the flag
    // Otherwise, sync breaks forever because the flag stays set
    setTimeout(() => {
      this.markCodeRunning(false);
    }, 100);
  }
}

Why Finally Block: The finally block ensures the flag is always cleared, even if an error occurs. If the flag stays set, sync breaks forever because shape updates will always be blocked. We delay clearing (100ms) to ensure all updates are complete.

The Complete Function:

updateFromCode(interpreter) {
  this.markCodeRunning(true);

  try {
    // Update our shapes reference
    if (interpreter?.env?.shapes) {
      this.shapes = interpreter.env.shapes;
    }

    // Update visual immediately
    this.immediateVisualUpdate();

    // Update parameter manager if menu is visible
    if (this.parameterManager?.menuVisible) {
      setTimeout(() => {
        this.parameterManager.updateWithLatestInterpreter();
      }, 50);
    }

  } finally {
    // Always clear the running flag
    setTimeout(() => {
      this.markCodeRunning(false);
    }, 100);
  }
}

Building This Step by Step:

  1. Create updateFromCode() method with interpreter parameter
  2. Mark code as running to prevent loops
  3. Wrap in try/finally to ensure flag is always cleared
  4. Update shapes reference from interpreter environment
  5. Update visual immediately for user feedback
  6. Update parameter manager if menu is visible (with delay)
  7. Clear running flag in finally block (with delay)
  8. This ensures code execution integrates smoothly with the shape manager

How to Build the Shape Manager - Complete Step-by-Step Guide

This section provides a complete guide for building the Shape Manager from scratch.

Prerequisites

Before building the Shape Manager, you need:

  • A working renderer that can redraw shapes
  • A text editor (CodeMirror) that can be updated programmatically
  • A parameter manager UI (optional, but recommended)
  • An interpreter that creates shape objects

Step 1: Create the Basic Shape Manager Class

File: src/shapeManager.mjs

What You're Building: The ShapeManager class is the central coordination layer that synchronizes between the renderer (visual), editor (code), parameter manager (UI sliders), and interpreter (code execution). It manages shape updates, coordinates component communication, and handles performance optimizations like throttling and debouncing.

Why This Class: Without a central coordinator, components would need to know about each other directly, creating tight coupling and making the system hard to maintain. The Shape Manager acts as a mediator, allowing components to communicate without direct dependencies.

How to Build It Step by Step:

Step 1.1: Create the ShapeManager Class and Constructor Start with the class definition and initialize all properties:

export class ShapeManager {
  constructor() {
    // Step 1.1.1: Initialize shapes storage
    // Shapes are stored in a Map (key = shape name, value = shape object)
    // Map preserves insertion order and allows efficient lookup
    this.shapes = new Map();

    // Step 1.1.2: Initialize component references
    // These will be registered later via register methods
    this.renderer = null;          // Handles visual rendering
    this.editor = null;             // Code editor (CodeMirror)
    this.parameterManager = null;   // UI sliders for parameters
    this.interpreter = null;        // Executes code and creates shapes

    // Step 1.1.3: Performance throttling for visual updates
    // Throttling limits how often visual updates happen
    // Prevents excessive redraws that cause performance issues
    this.lastVisualUpdate = 0;      // Timestamp of last visual update
    this.visualUpdateThrottle = 16; // 16ms = ~60fps (throttle to 60 updates per second)

    // Step 1.1.4: Code update debouncing
    // Debouncing delays code updates until user stops making changes
    // Prevents code from updating on every keystroke
    this.codeUpdateTimer = null;    // Timer for debounced code updates
    this.codeUpdateDelay = 200;     // 200ms delay (wait 200ms after last change)

    // Step 1.1.5: Auto-run control
    // Prevents infinite loops when code updates trigger more code updates
    this.autoRunDisabled = false;   // Flag to temporarily disable auto-run
    this.autoRunDisableTimer = null; // Timer to re-enable auto-run
  }
}

Why These Properties:

  • shapes: Central storage for all shapes. All components reference this.
  • Component references: Allow Shape Manager to coordinate between components.
  • Throttling: Limits visual updates to 60fps, preventing performance issues.
  • Debouncing: Delays code updates until user stops typing, reducing unnecessary updates.
  • Auto-run control: Prevents circular updates (code → shape → code → shape...).

Export as singleton:

export const shapeManager = new ShapeManager();

Why Singleton: The Shape Manager should be a single instance shared across the entire application. This ensures all components coordinate through the same manager.

Step 2: Implement Component Registration

What You're Building: Methods that allow components (renderer, editor, parameter manager, interpreter) to register themselves with the Shape Manager. This creates the connections needed for coordination.

Why These Methods: Components need to register with the Shape Manager so it can coordinate between them. Registration happens at startup, after all components are created. This is a dependency injection pattern - components are injected into the manager.

How to Build It Step by Step:

Step 2.1: Implement registerRenderer() Method Register the renderer component:

registerRenderer(renderer) {
  // Step 2.1.1: Store renderer reference
  // Renderer handles visual updates (redrawing shapes on canvas)
  this.renderer = renderer;
}

Why Store Renderer: The Shape Manager needs the renderer to trigger visual updates when shapes change. The renderer is responsible for drawing shapes on the canvas.

Step 2.2: Implement registerEditor() Method Register the code editor:

registerEditor(editor) {
  // Step 2.2.1: Store editor reference
  // Editor is the code editor (CodeMirror instance)
  // Used to update code when shapes change (bidirectional sync)
  this.editor = editor;
}

Why Store Editor: The Shape Manager needs the editor to update code when shapes are modified on the canvas. This enables bidirectional sync (shape changes → code updates).

Step 2.3: Implement registerParameterManager() Method Register the parameter manager:

registerParameterManager(parameterManager) {
  // Step 2.3.1: Store parameter manager reference
  // Parameter manager handles UI sliders for shape parameters
  // Used to update sliders when shapes change
  this.parameterManager = parameterManager;
}

Why Store Parameter Manager: The Shape Manager needs the parameter manager to update UI sliders when shapes change. This keeps the UI in sync with shape state.

Step 2.4: Implement registerInterpreter() Method Register the interpreter and sync shapes:

registerInterpreter(interpreter) {
  // Step 2.4.1: Store interpreter reference
  // Interpreter executes code and creates shape objects
  this.interpreter = interpreter;

  // Step 2.4.2: Sync shapes from interpreter
  // When interpreter is registered, sync its shapes to Shape Manager
  // This initializes the shapes Map with shapes from code execution
  if (interpreter && interpreter.env && interpreter.env.shapes) {
    this.shapes = interpreter.env.shapes;
  }
}

Why Sync Shapes: When the interpreter is registered, it may already have shapes from previous code execution. We sync those shapes to the Shape Manager so it has the current state.

The Complete Methods:

registerRenderer(renderer) {
  this.renderer = renderer;
}

registerEditor(editor) {
  this.editor = editor;
}

registerParameterManager(parameterManager) {
  this.parameterManager = parameterManager;
}

registerInterpreter(interpreter) {
  this.interpreter = interpreter;
  // Sync shapes from interpreter
  if (interpreter && interpreter.env && interpreter.env.shapes) {
    this.shapes = interpreter.env.shapes;
  }
}

Building This Step by Step:

  1. Create registerRenderer() method with renderer parameter
  2. Store renderer as instance property
  3. Create registerEditor() method with editor parameter
  4. Store editor as instance property
  5. Create registerParameterManager() method with parameterManager parameter
  6. Store parameterManager as instance property
  7. Create registerInterpreter() method with interpreter parameter
  8. Store interpreter as instance property
  9. Check if interpreter has shapes in its environment
  10. If yes, sync shapes to Shape Manager's shapes Map
  11. These methods establish component connections for coordination

Test: Register components:

shapeManager.registerRenderer(renderer);
shapeManager.registerEditor(editor);
// Verify components are stored
console.log(shapeManager.renderer); // Should be renderer instance

Debug Visualizer Integration

Issue: The debug visualizer needs to draw overlays (bounds, performance info, etc.) during canvas redraws, but it wasn't being passed through the rendering pipeline. When shape manager triggered redraws, the debug visualizer wasn't available, so debug overlays wouldn't appear even when debug mode was enabled.

What Was Added: Debug visualizer tracking in shape manager and passing it through all redraw calls.

Specific Implementation:

1. Added Debug Visualizer Property and Registration Method:

// In shapeManager.mjs constructor - added property
constructor() {
    // ... existing properties ...
    this.debugVisualizer = null; // Added: Track debug visualizer reference
}

// NEW METHOD: Register debug visualizer
setDebugVisualizer(debugVisualizer) {
    this.debugVisualizer = debugVisualizer;
}

2. Updated immediateVisualUpdate to Accept and Use Debug Visualizer:

// BEFORE: Didn't pass debug visualizer
immediateVisualUpdate() {
    if (!this.renderer) return;

    const now = Date.now();
    if (now - this.lastVisualUpdate >= this.visualUpdateThrottle) {
        requestAnimationFrame(() => {
            this.renderer.redraw(); // No debug visualizer passed
            this.lastVisualUpdate = Date.now();
        });
    }
}

// AFTER: Accepts debug visualizer parameter and uses stored one as fallback
immediateVisualUpdate(debugVisualizer = null) {
    if (!this.renderer) return;

    const now = Date.now();
    if (now - this.lastVisualUpdate >= this.visualUpdateThrottle) {
        requestAnimationFrame(() => {
            // Use provided debugVisualizer parameter OR stored one
            // This allows passing it explicitly or using the stored reference
            const dv = debugVisualizer || this.debugVisualizer;
            this.renderer.redraw(dv); // Pass debug visualizer to renderer
            this.lastVisualUpdate = Date.now();
        });
    }
}

3. Updated updateShapeParameter to Pass Debug Visualizer:

// Updated method signature to pass debug visualizer through
updateShapeParameter(shapeName, paramName, value, source = 'unknown') {
    // Update shape object immediately
    this.immediateShapeUpdate(shapeName, paramName, value);

    // Update visual immediately (throttled) - NOW PASSES DEBUG VISUALIZER
    this.immediateVisualUpdate(this.debugVisualizer); // Added: Pass stored debug visualizer

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

    // Schedule code update (if not from code/editor)
    if (source !== 'code' && source !== 'editor') {
        this.scheduleCodeUpdate();
    }
}

4. Registration in app.js:

// Initialize debug visualizer early
if (renderer.coordinateSystem) {
    debugVisualizer = new DebugVisualizer(canvas.getContext('2d'), renderer.coordinateSystem);
    debugVisualizer.setEnabled(debugEnabled);
    window.debugVisualizer = debugVisualizer; // Store globally too
}

// Register with shape manager
shapeMgr.setDebugVisualizer(debugVisualizer); // NEW: Register debug visualizer

Why This Was Necessary:

  1. Overlay Drawing: Debug visualizer needs to draw overlays (bounds, FPS, etc.) on top of shapes. This happens during renderer.redraw(), so the visualizer must be passed to that method.
  2. Parameter Updates: When shapes are updated via sliders or canvas interaction, shape manager triggers redraws. Without passing the debug visualizer, debug overlays wouldn't appear during these updates.
  3. Flexible Passing: The method accepts an optional parameter, allowing explicit passing when needed, but falls back to the stored reference for convenience.
  4. Central Coordination: Shape manager coordinates all updates, so it's the logical place to ensure debug visualizer is always passed through the rendering pipeline.

What Happened Without This:

  • Debug overlays would only appear on initial render
  • When shapes were updated (via sliders, canvas drag, etc.), debug overlays would disappear
  • Debug mode would appear "broken" because overlays weren't updating
  • Performance metrics (FPS, shape count) wouldn't update correctly

Related Changes in Renderer:

The renderer's redraw() method was also updated to accept and use the debug visualizer:

// In renderer.mjs
redraw(debugVisualizer = null) {
    // ... draw shapes ...

    // Update debug visualizer counts
    if (debugVisualizer) {
        debugVisualizer.updateCounts(shapeCount, edgeCount);
    }

    // Draw debug overlay (if enabled and visualizer provided)
    if (debugVisualizer && this.shapes) {
        debugVisualizer.drawOverlay(this.shapes, this.coordinateSystem);
    }
}

Step 3: Implement Immediate Shape Update

immediateShapeUpdate(shapeName, paramName, value) {
  // Find the shape
  const shape = this.shapes.get(shapeName);
  if (!shape) {
    console.warn(`Shape not found: ${shapeName}`);
    return false;
  }

  // Update the parameter
  if (paramName === 'radius') {
    shape.params.radius = value;
  } else if (paramName === 'width') {
    shape.params.width = value;
  } else if (paramName === 'height') {
    shape.params.height = value;
  } else if (paramName === 'position_x') {
    if (!shape.transform) shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
    shape.transform.position[0] = value;
  } else if (paramName === 'position_y') {
    if (!shape.transform) shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
    shape.transform.position[1] = value;
  } else {
    // Generic parameter update
    shape.params[paramName] = value;
  }

  return true;
}

Test: Update a shape parameter:

// Assuming you have a shape 'c1' with radius 50
shapeManager.immediateShapeUpdate('c1', 'radius', 75);
console.log(shapeManager.shapes.get('c1').params.radius); // Should be 75

Step 4: Implement Immediate Visual Update

immediateVisualUpdate() {
  const now = performance.now();

  // Throttle to 60fps
  if (now - this.lastVisualUpdate < this.visualUpdateThrottle) {
    return;
  }

  if (this.renderer && this.renderer.redraw) {
    this.renderer.redraw();
    this.lastVisualUpdate = now;
  }
}

Test: Update shape and verify renderer redraws:

shapeManager.immediateShapeUpdate('c1', 'radius', 100);
shapeManager.immediateVisualUpdate();
// Shape should appear larger on canvas

Step 5: Implement Immediate UI Sync

immediateUISync(shapeName, paramName, value, source) {
  // Only update UI if change came from canvas (not from slider)
  if (source === 'canvas' && this.parameterManager) {
    this.updateSliderValueImmediate(shapeName, paramName, value);
  }
}

updateSliderValueImmediate(shapeName, paramName, value) {
  if (!this.parameterManager) return;

  // Find slider elements
  const elements = this.parameterManager.paramsList?.querySelectorAll(
    `[data-shape-name="${shapeName}"][data-param-name="${paramName}"]`
  );

  if (elements) {
    elements.forEach(element => {
      if (element.tagName === 'INPUT') {
        element.value = value;
        // Trigger input event so any listeners fire
        element.dispatchEvent(new Event('input', { bubbles: true }));
      }
    });
  }
}

Test: Update shape from canvas, verify slider updates:

// Assuming slider exists for c1.radius
shapeManager.immediateShapeUpdate('c1', 'radius', 80, 'canvas');
shapeManager.immediateUISync('c1', 'radius', 80, 'canvas');
// Slider should show 80

Step 6: Implement Scheduled Code Update

scheduleCodeUpdate(shapeName, paramName, value) {
  // Clear any pending update
  if (this.codeUpdateTimer) {
    clearTimeout(this.codeUpdateTimer);
  }

  // Schedule new update
  this.codeUpdateTimer = setTimeout(() => {
    this.executeCodeUpdate(shapeName, paramName, value);
  }, this.codeUpdateDelay);
}

executeCodeUpdate(shapeName, paramName, value) {
  if (!this.editor || !this.parameterManager) return;

  // Disable auto-run
  this.disableAutoRun();

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

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

Test: Update shape, verify code updates after delay:

shapeManager.immediateShapeUpdate('c1', 'radius', 90);
shapeManager.scheduleCodeUpdate('c1', 'radius', 90);
// Wait 200ms, check editor - should show radius: 90

Step 7: Implement Auto-Run Control

disableAutoRun() {
  this.autoRunDisabled = true;

  // Clear any existing timer
  if (this.autoRunDisableTimer) {
    clearTimeout(this.autoRunDisableTimer);
  }
}

enableAutoRunDelayed() {
  // Re-enable after delay (longer than code update delay)
  this.autoRunDisableTimer = setTimeout(() => {
    this.autoRunDisabled = false;
  }, 300);
}

isAutoRunEnabled() {
  return !this.autoRunDisabled;
}

Integrate with app.js:

// In app.js runCode():
function runCode() {
  if (shapeManager.isAutoRunEnabled()) {
    // ... run code ...
  }
}

Step 8: Implement the Core Update Method

updateShapeParameter(shapeName, paramName, value, source = 'unknown') {
  // 1. Update shape object immediately
  const updated = this.immediateShapeUpdate(shapeName, paramName, value);
  if (!updated) return false;

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

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

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

  return true;
}

Test: Complete update flow:

// Update from slider
shapeManager.updateShapeParameter('c1', 'radius', 100, 'slider');
// Shape should update, renderer should redraw, code should update after delay

// Update from canvas
shapeManager.updateShapeParameter('c1', 'radius', 110, 'canvas');
// Shape should update, renderer should redraw, slider should update, code should update

// Update from code
shapeManager.updateShapeParameter('c1', 'radius', 120, 'code');
// Shape should update, renderer should redraw, but code should NOT update (no loop)

Step 9: Add Canvas Interaction Handlers

onCanvasShapeChange(shapeName, paramName, value) {
  this.updateShapeParameter(shapeName, paramName, value, 'canvas');
}

onCanvasPositionChange(shapeName, x, y) {
  this.updateShapeParameter(shapeName, 'position_x', x, 'canvas');
  this.updateShapeParameter(shapeName, 'position_y', y, 'canvas');
}

onCanvasRotationChange(shapeName, rotation) {
  if (!this.shapes.get(shapeName).transform) {
    const shape = this.shapes.get(shapeName);
    shape.transform = { position: [0, 0], rotation: 0, scale: [1, 1] };
  }
  this.shapes.get(shapeName).transform.rotation = rotation;
  this.immediateVisualUpdate();
  this.scheduleCodeUpdate(shapeName, 'rotation', rotation);
}

Wire into interaction handler:

// In renderer/interactionHandler.mjs
handleMouseMove(e) {
  if (this.dragging) {
    const newX = /* calculate */;
    const newY = /* calculate */;
    shapeManager.onCanvasPositionChange(this.selectedShape.name, newX, newY);
  }
}

Step 10: Add Slider Interaction Handler

onSliderChange(shapeName, paramName, value) {
  this.updateShapeParameter(shapeName, paramName, value, 'slider');
}

Wire into parameter manager:

// In 2Dparameters.mjs
slider.addEventListener('input', (e) => {
  const value = parseFloat(e.target.value);
  shapeManager.onSliderChange(shapeName, paramName, value);
});

Step 11: Integrate with Code Execution

markCodeRunning() {
  this.isCodeRunning = true;
}

updateFromCode() {
  // Called after code execution
  if (this.interpreter && this.interpreter.env) {
    this.shapes = this.interpreter.env.shapes;
  }
  this.isCodeRunning = false;
}

Wire into app.js:

function runCode() {
  shapeManager.markCodeRunning();
  // ... execute code ...
  shapeManager.updateFromCode();
}

Common Issues and Fixes

Issue: Infinite update loops

  • Check source parameter is set correctly
  • Check auto-run is disabled during code updates
  • Check sync flags aren't stuck

Issue: Updates don't propagate

  • Check components are registered
  • Check shape exists in shapes Map
  • Check renderer.redraw() is being called

Issue: Code doesn't update

  • Check parameterManager.updateCodeInEditor exists
  • Check editor is registered
  • Check code update timer isn't being cleared

Issue: Performance problems

  • Check visual update throttling (should be 60fps max)
  • Check code update debouncing (should be 200ms)
  • Check for unnecessary redraws

Common Gotchas

Gotcha 1: Not checking source Always check the source before updating code. If source is 'code', don't update code.

Gotcha 2: Forgetting to throttle Visual updates without throttling = slow. Always throttle to 60fps.

Gotcha 3: Not debouncing code updates Code updates without debouncing = editor spam. Always debounce.

Gotcha 4: Circular references If shape manager references renderer, and renderer references shape manager, you get circular dependencies. Keep references one-way where possible.

Gotcha 5: Race conditions If code is running and user drags a shape at the same time, you get conflicts. Use isCodeRunning flag to prevent this.

results matching ""

    No results matching ""