How Everything Ties Together - Complete Beginner's Guide
What is This Chapter About?
This is the big picture chapter. You've learned about all the individual pieces (lexer, parser, interpreter, renderer, shape manager, etc.) - now let's see how they all connect and work together as one system.
Why This Chapter Matters:
Understanding individual components is important, but understanding how they integrate is critical. This chapter shows you:
- How all the pieces fit together
- Why things are done in a specific order
- How data flows through the system
- How to debug when things break
- How to extend the system without breaking things
The Big Picture - Explained Simply
Real-World Analogy:
Think of Otto like a factory assembly line:
- Raw material (code) goes in one end
- Gets processed through multiple stages (lexer, parser, interpreter)
- Finished products (shapes on canvas) come out the other end
But unlike a normal factory, this one is bidirectional:
- You can modify the products (drag shapes)
- And the raw material (code) updates automatically!
- It's like a factory that can work backwards!
Visual Example:
Normal Factory (One Way):
Raw Material → Stage 1 → Stage 2 → Stage 3 → Product
(code) (lexer) (parser) (interpreter) (shapes)
Otto Factory (Bidirectional):
Raw Material ↔ Stage 1 ↔ Stage 2 ↔ Stage 3 ↔ Product
(code) (lexer) (parser) (interpreter) (shapes)
You can change code → shapes update
You can change shapes → code updates
Both directions work!
Another Analogy:
Think of it like a spreadsheet:
- You type a formula in a cell (code)
- The value appears (shapes)
- But you can also change the value
- And the formula updates automatically!
Otto works the same way - change code, see shapes. Change shapes, code updates.
What This Chapter Covers - Explained Simply
This chapter walks through how everything connects:
Startup Sequence:
- What happens when the app first loads?
- In what order are things created?
- Why does the order matter?
Component Initialization:
- How are all the pieces created?
- What depends on what?
- Why must some things be created before others?
Data Flow:
- How does data move through the system?
- Where does it start?
- Where does it end up?
Event Handling:
- What happens when you click something?
- How do mouse events become shape changes?
- How do changes trigger updates?
Integration Patterns:
- How do components talk to each other?
- How do they avoid talking in circles (infinite loops)?
- How do they stay in sync?
The Complete Picture:
- How does it all work end-to-end?
- From typing code to seeing shapes
- From dragging shapes to updating code
Why This Matters - Explained Simply
The Problem:
You know how each piece works individually:
- Lexer breaks code into tokens ✓
- Parser builds an AST ✓
- Interpreter creates shapes ✓
- Renderer draws shapes ✓
But how do they work together? That's what this chapter explains.
Why It's Important:
Understanding integration helps you:
- Fix bugs - When something breaks, you know where to look
- Add features - You know where to add new code
- Understand the system - You see the whole picture, not just pieces
- Debug effectively - You can trace problems through the system
Real-World Example:
It's like understanding how a car works:
- You know the engine works (component knowledge)
- You know the wheels work (component knowledge)
- But you also need to know how the engine makes the wheels turn (integration knowledge)
Without integration knowledge, you can't fix problems or improve the system!
Building the Startup Sequence From Scratch - Explained Simply
What You're Building:
The startup sequence is the code that runs when your application first loads. It's like the "startup checklist" for your app - it makes sure everything is set up correctly before the user can interact with it.
Real-World Analogy:
Think of it like opening a restaurant:
- First, you unlock the doors (DOM loaded)
- Then you turn on the lights (initialize components)
- Then you set up the kitchen equipment (create renderer, etc.)
- Then you prepare ingredients (set up event handlers)
- Then you open for customers (app is ready!)
The startup sequence does all of this automatically when the page loads.
Why This Sequence Matters:
Components have dependencies - some things must exist before others can be created:
Renderer must exist before shape manager can register it
- Shape manager needs to tell renderer to redraw
- Can't tell renderer to redraw if renderer doesn't exist!
Editor must exist before event handlers can attach to it
- Event handlers need to listen to editor changes
- Can't listen to changes if editor doesn't exist!
Canvas must exist before renderer can use it
- Renderer needs canvas to draw on
- Can't draw if canvas doesn't exist!
The Problem Without Proper Sequence:
If you create things in the wrong order, you get errors:
// WRONG ORDER:
shapeManager.registerRenderer(renderer); // ❌ Error: renderer doesn't exist yet!
renderer = new Renderer(canvas);
// CORRECT ORDER:
renderer = new Renderer(canvas); // ✓ Create renderer first
shapeManager.registerRenderer(renderer); // ✓ Then register it
The Solution:
The startup sequence ensures everything is created in the correct order, preventing errors and making sure the system is ready to use.
How to Build It Step by Step:
Step 1: Create the DOMContentLoaded Event Listener
document.addEventListener('DOMContentLoaded', function() {
What is DOMContentLoaded?
DOMContentLoaded is an event that fires when the HTML document has finished loading and parsing. Think of it like a signal that says "the page is ready, but images might still be loading."
Why Wait for DOMContentLoaded?
You can't access DOM elements until they exist. If you try to get an element before the page loads, it will be null:
// TOO EARLY (runs immediately, before page loads):
const canvas = document.getElementById('canvas'); // ❌ null! Element doesn't exist yet!
// CORRECT (waits for DOM to load):
document.addEventListener('DOMContentLoaded', function() {
const canvas = document.getElementById('canvas'); // ✓ Works! Element exists now.
});
Understanding Event Listeners:
addEventListener tells the browser: "When this event happens, run this function." It's like setting an alarm - you tell it what to do when the alarm goes off.
// Step 1.1: Initialize core components
// This creates the renderer, shape manager, constraint engine, etc.
// These are the foundation - everything else depends on them
initializeComponents();
What initializeComponents() Does:
This function creates all the core components:
- Renderer (draws shapes)
- Shape Manager (coordinates updates)
- Constraint Engine (solves constraints)
- Drag and Drop System (shape palette)
Why First:
These are the foundation. Everything else needs them to exist first. Like building a house - you need the foundation before you can build the walls.
// Step 1.2: Set up event handlers
// Event handlers need components to exist first
// They attach listeners to buttons, keyboard shortcuts, etc.
setupEventHandlers();
What setupEventHandlers() Does:
This function sets up all the event listeners:
- Button clicks (run code, export, etc.)
- Keyboard shortcuts (Ctrl+Enter to run code)
- Window resize (resize canvas)
- etc.
Why After Components:
Event handlers reference components. For example, a "run code" button needs to call runCode(), which needs the editor, renderer, etc. These must exist first!
// Step 1.3: Set up CodeMirror editor
// The editor needs its textarea element to exist in the DOM
// CodeMirror attaches to the textarea and transforms it into a code editor
setupCodeMirror();
What setupCodeMirror() Does:
This function creates the code editor. CodeMirror is a library that turns a plain <textarea> into a fancy code editor with syntax highlighting, line numbers, etc.
Why After DOM Ready:
The <textarea> element must exist in the DOM before CodeMirror can attach to it. DOMContentLoaded ensures the DOM is ready, but we also need to ensure our initialization functions have run.
// Step 1.4: Load documentation
// Documentation can load asynchronously - doesn't block other initialization
loadDocumentation();
What loadDocumentation() Does:
This function loads help documentation (like this guide!). It's asynchronous - it doesn't block other initialization.
Why It's Separate:
Documentation isn't critical for the app to work. If it fails to load, the app still works. So we load it separately and don't wait for it.
// Step 1.5: Initialize Blockly
// Blockly needs its container div to exist in the DOM
// It creates the visual block editor in that container
initBlockly();
What initBlockly() Does:
This function creates the Blockly workspace (visual block editor). It needs a <div> element in the DOM to inject into.
Why After DOM:
The Blockly container <div id="blocklyDiv"> must exist in the DOM before Blockly can create the workspace inside it.
// Step 1.6: Final wiring and first run
// Some connections need everything to be fully initialized
// We use setTimeout to ensure DOM is ready and components are connected
setTimeout(() => {
Why setTimeout?
Some operations need a tiny delay to ensure everything is fully ready:
- CodeMirror might need a moment to finish attaching
- Blockly might need a moment to finish rendering
- Canvas might need a moment to finish sizing
setTimeout with 100ms delay gives everything time to finish before we do final setup.
Understanding setTimeout:
setTimeout schedules code to run later. The first parameter is the function to run, the second is the delay in milliseconds:
setTimeout(() => {
// This code runs 100 milliseconds later
}, 100);
// Step 1.6.1: Ensure all connections are proper
// This verifies that components are wired together correctly
ensureProperConnections();
What ensureProperConnections() Does:
This function double-checks that all components are connected correctly:
- Shape manager has renderer registered
- Editor is registered with shape manager
- All callbacks are set up correctly
Why This Check:
It's like checking all the wires are connected before turning on a machine. If something is disconnected, we want to know now, not later when it breaks.
// Step 1.6.2: Apply layout
// Some layouts need components to be initialized first
applyNewLayout();
What applyNewLayout() Does:
This function sets up the UI layout (where panels are positioned, how they're sized, etc.). It needs components to exist so it can position them correctly.
Why After Components:
Layout depends on component sizes and positions. Can't lay out things that don't exist yet!
// Step 1.6.3: Run code if editor has content
// If there's saved code or default code, run it immediately
// This makes shapes appear right away instead of requiring user to click run
if (editor && editor.getValue().trim()) {
runCode();
}
What This Does:
Checks if the editor exists and has code, and if so, runs it automatically.
Why This Check:
editor- Editor might not be ready yet (safety check)editor.getValue()- Gets the code text from editor.trim()- Removes whitespace (empty strings become empty after trim)- If code exists (not empty), run it
Why Run Code Automatically:
If there's saved code or default example code, we want shapes to appear immediately. Better user experience - they see something right away instead of an empty canvas.
If you see an error at this step:
Error: TypeError: document.addEventListener is not a function
- What this means: Not running in browser environment or document is not available
- Common causes:
- Node.js environment: Running in Node.js (not browser)
- document not available: document object doesn't exist
- Wrong context: Code running in wrong context (worker, etc.)
- Fix: Ensure code runs in browser environment, check document exists:
if (typeof document === 'undefined') throw new Error('DOM not available');, verify running in browser context
Error: TypeError: initializeComponents is not a function
- What this means: initializeComponents function not defined
- Common causes:
- Function not defined: initializeComponents() not implemented
- Function not in scope: Defined in different file, not accessible
- Typo in function name: initializeComponent vs initializeComponents
- Fix: Implement initializeComponents() function, check function is in correct scope, verify function name spelling
Error: Components initialized but not working (renderer is null, etc.)
- What this means: Components not properly initialized or not accessible
- Common causes:
- Initialization failed: initializeComponents() threw error, didn't complete
- Variables not global: Components created but stored in local scope
- Initialization order wrong: Components created in wrong order, dependencies not met
- Fix: Check initializeComponents() completes without errors, ensure component variables are in global/module scope, verify initialization order is correct
Error: TypeError: Cannot read property 'getValue' of null
- What this means: editor is null when trying to get code
- Common causes:
- Editor not created: setupCodeMirror() not called or failed
- Editor not ready: setTimeout delay too short, editor not initialized yet
- Editor variable not set: setupCodeMirror() didn't set global editor variable
- Fix: Check setupCodeMirror() is called and completes successfully, increase setTimeout delay if needed, verify editor variable is set:
editor = CodeMirror.fromTextArea(...);
Error: Code runs but shapes don't appear
- What this means: runCode() executes but renderer not drawing
- Common causes:
- Renderer not initialized: initializeComponents() didn't create renderer
- Shapes not passed to renderer: runCode() doesn't call renderer.setShapes()
- Renderer not redrawing: renderer.setShapes() doesn't trigger redraw
- Fix: Check renderer is created in initializeComponents(), verify runCode() calls renderer.setShapes(), ensure renderer redraws after setting shapes
Error: setTimeout callback never runs (code doesn't execute)
- What this means: setTimeout callback not executing
- Common causes:
- Syntax error: Syntax error in setTimeout callback prevents execution
- Error in callback: Callback throws error, stops execution
- Browser issue: Very rare, but setTimeout could fail
- Fix: Check for syntax errors in setTimeout callback, wrap callback in try-catch to catch errors, verify setTimeout is supported (should be in all browsers)
Why This Order Matters:
Each step depends on the previous ones. It's like building a house:
Foundation first (initializeComponents):
- You need the foundation before you can build walls
- Components are the foundation - everything else builds on them
Wiring second (setupEventHandlers):
- You need walls before you can install electrical wiring
- Event handlers need components to exist (they reference them)
Plumbing third (setupCodeMirror):
- You need walls before you can install plumbing
- Editor needs DOM elements to exist (textarea must be in DOM)
Finishing touches last (setTimeout block):
- You do final checks after everything is built
- Final wiring needs everything to be ready
Why setTimeout is Needed:
Some operations need the DOM to be fully ready, not just loaded. There's a difference:
- DOM loaded: Elements exist in the DOM
- DOM ready: Elements are fully rendered, sized, and positioned
What Needs DOM Ready:
CodeMirror:
- Needs textarea element to be fully rendered
- Needs to measure the element's size
- Can't do this if element is still being laid out
Blockly:
- Needs container div to have dimensions
- Uses dimensions to size the workspace
- If dimensions are 0, workspace won't work
Canvas:
- Needs to be sized before renderer can use it
- Renderer needs canvas width/height for coordinate system
- If canvas is 0x0, coordinate system breaks
Layout:
- Needs all elements positioned
- Can't calculate layout if elements are still moving
Why 100ms?
100 milliseconds is usually enough for:
- Browser to finish rendering
- Elements to get their final sizes
- CodeMirror/Blockly to attach
It's fast enough that users don't notice the delay, but long enough for everything to be ready.
What Happens If You Skip the Timeout:
If you try to do everything immediately, you get errors:
// WITHOUT setTimeout (WRONG):
setupCodeMirror(); // ❌ textarea might not be fully rendered yet
const code = editor.getValue(); // ❌ editor might not be attached yet
// WITH setTimeout (CORRECT):
setTimeout(() => {
setupCodeMirror(); // ✓ textarea is fully rendered
const code = editor.getValue(); // ✓ editor is attached
}, 100);
Common Issues Without Timeout:
- CodeMirror errors: "Cannot read property of null" - textarea not ready
- Blockly errors: "Container not found" - container not in DOM yet
- Canvas errors: "Width is 0" - canvas not sized yet
- Layout errors: "Cannot read property 'offsetWidth'" - elements not positioned yet
Visual Timeline:
t=0ms: DOMContentLoaded fires
t=1ms: initializeComponents() starts
t=5ms: setupEventHandlers() starts
t=10ms: setupCodeMirror() starts
t=15ms: initBlockly() starts
t=20ms: All functions complete, but...
t=20ms: DOM still rendering, elements still sizing...
t=50ms: DOM rendering complete
t=100ms: setTimeout callback runs (everything is ready!)
t=101ms: Final wiring happens
t=102ms: Code runs (if exists)
The Final Check Explained:
if (editor && editor.getValue().trim()) {
runCode();
}
Understanding the Check:
editor- Check if editor exists- Might be null if setupCodeMirror() failed
- Safety check to prevent errors
editor.getValue()- Get code from editor- Returns string of all code
- Example:
"shape circle c1 { radius: 50 }"
.trim()- Remove whitespace" "becomes""(empty)" shape circle c1 "becomes"shape circle c1"
Check if truthy - Empty string is falsy
""is falsy (empty, no code)"shape circle c1"is truthy (has code)
Why Check All Three:
- Check editor exists (prevents error)
- Get code value (need to check if it's empty)
- Trim whitespace (user might have typed just spaces)
Example Scenarios:
// Scenario 1: Editor not ready
if (editor && ...) // editor is null, check fails, doesn't run code ✓
// Scenario 2: Editor empty
if (editor && editor.getValue().trim()) // "" is falsy, doesn't run code ✓
// Scenario 3: Editor has code
if (editor && editor.getValue().trim()) // "shape circle c1" is truthy, runs code! ✓
The Final Check:
if (editor && editor.getValue().trim()) - This checks:
- Editor exists (might not be ready yet)
- Editor has content (not empty)
- Content is not just whitespace (trim() removes spaces)
If all conditions are met, we run the code immediately so shapes appear right away. This improves user experience - they see results immediately instead of needing to click a run button.
Building This Step by Step:
- Add
DOMContentLoadedevent listener to document - Call
initializeComponents()first (creates foundation) - Call
setupEventHandlers()second (needs components) - Call
setupCodeMirror()third (needs DOM ready) - Call
loadDocumentation()fourth (can be async) - Call
initBlockly()fifth (needs DOM ready) - Add setTimeout with 100ms delay
- Inside timeout, call
ensureProperConnections() - Call
applyNewLayout() - Check if editor exists and has content
- If yes, call
runCode()to execute initial code - This sequence ensures proper initialization order
Building Component Initialization From Scratch - Explained Simply
What You're Building:
The initializeComponents() function creates all the core system components. Think of it like setting up a factory - it creates all the machines (components) needed for production.
Real-World Analogy:
Think of it like setting up a restaurant kitchen:
- First, you install the stovetop (renderer)
- Then you connect it to gas (register with shape manager)
- Then you set up other equipment (constraint engine, etc.)
- Everything must be in place before you can cook!
Why This Function Exists:
All components need to be created before the system can function. This function:
- Centralizes creation - All component creation in one place
- Ensures correct order - Things created in dependency order
- Satisfies dependencies - Makes sure everything needed exists first
It's the foundation that everything else builds on. Without it, nothing works!
The Problem Without This Function:
If you create components randomly:
// WRONG - Random order, dependencies not satisfied:
constraintEngine = new ConstraintEngine(renderer, ...); // ❌ renderer doesn't exist yet!
renderer = new Renderer(canvas); // Create renderer AFTER constraint engine? No!
The Solution:
initializeComponents() creates everything in the correct order:
// CORRECT - Proper order, dependencies satisfied:
renderer = new Renderer(canvas); // ✓ Create renderer first
constraintEngine = new ConstraintEngine(renderer, ...); // ✓ Then constraint engine (has renderer now!)
How to Build It Step by Step:
Step 1: Get DOM Elements
function initializeComponents() {
// Step 1.1: Get canvas element
// The canvas is where all shapes are drawn
// We need it before creating the renderer
canvas = document.getElementById('canvas');
What This Does:
Gets the HTML canvas element from the DOM. This is the <canvas> tag in your HTML where shapes will be drawn.
Why We Need This:
The renderer needs the canvas element to:
- Get the 2D drawing context
- Know the canvas size (width, height)
- Draw shapes on it
Understanding getElementById:
document.getElementById('canvas') searches the DOM for an element with id="canvas". It returns the element, or null if not found.
If Element Doesn't Exist:
If the canvas element isn't found, you'll get null, and later code will fail:
canvas = document.getElementById('canvas'); // Returns null if not found
renderer = new Renderer(canvas); // ❌ Error: Cannot read property of null
Safety Check (Recommended):
canvas = document.getElementById('canvas');
if (!canvas) {
throw new Error('Canvas element not found! Make sure <canvas id="canvas"> exists in HTML.');
}
// Step 1.2: Get AST output element
// This is for debugging - shows the parsed code structure
// Helps developers understand what the parser produced
astOutput = document.getElementById('ast-output');
What This Does:
Gets the element that displays the Abstract Syntax Tree (AST). This is for debugging - shows developers what the parser created from the code.
What is an AST?
AST = Abstract Syntax Tree. It's the parsed code structure. Instead of seeing:
Code: "shape circle c1 { radius: 50 }"
You see:
AST: {
"type": "shape",
"shapeType": "circle",
"name": "c1",
"params": {
"radius": { "type": "number", "value": 50 }
}
}
Why Show AST:
Helps debug parsing issues. If code doesn't work, you can see what the parser actually created. Very useful for development!
If Element Doesn't Exist:
AST output is optional (just for debugging). If it doesn't exist, that's okay - the app still works, you just don't see the AST.
// Step 1.3: Get error output element
// This displays errors when code fails to parse or execute
// Users see error messages here instead of console
errorOutput = document.getElementById('error-output');
What This Does:
Gets the element that displays errors. When code fails to parse or execute, error messages appear here instead of just in the browser console.
Why Show Errors to Users:
Users can't see the browser console easily. Showing errors on the page makes them visible and helpful. Users can see what went wrong and fix it.
If Element Doesn't Exist:
Errors won't display on the page (they'll still go to console). The app works, but users won't see helpful error messages.
If you see an error at this step:
Error: TypeError: Cannot read property 'getContext' of null
- What this means: canvas is null (element not found)
- Common causes:
- Element doesn't exist: No
<canvas id="canvas">in HTML - Wrong ID: Element has different ID (e.g.,
id="myCanvas") - Script runs too early: Runs before DOM loads (but DOMContentLoaded should prevent this)
- Element doesn't exist: No
- Fix: Check HTML has
<canvas id="canvas">, verify ID matches exactly (case-sensitive), ensure script runs after DOMContentLoaded
Error: TypeError: document.getElementById is not a function
- What this means: Not running in browser environment
- Common causes:
- Node.js environment: Running in Node.js (not browser)
- document not available: document object doesn't exist
- Wrong context: Code running in wrong context
- Fix: Ensure code runs in browser environment, check document exists:
if (typeof document === 'undefined') throw new Error('DOM not available');
Error: ReferenceError: canvas is not defined (later in code)
- What this means: canvas variable not accessible where it's used
- Common causes:
- Variable in wrong scope: canvas declared inside function, used outside
- Variable not declared: Forgot
let canvas;orvar canvas;at module level - Typo in variable name: canvas vs canvass (typo)
- Fix: Declare canvas at module/global scope:
let canvas;at top of file, ensure variable is accessible where used, check variable name spelling
Why Get DOM Elements First: These HTML elements must exist before components can use them. The canvas is needed by the renderer. The output elements are needed for debugging and error display. We get them first to ensure they're available when components are created.
Step 2: Create Renderer
// Step 2.1: Create renderer instance
// The renderer is the visual foundation - everything draws through it
// When you create a Renderer, it internally creates many subsystems:
renderer = new Renderer(canvas);
What This Does:
Creates the main Renderer instance. The renderer is the visual foundation - everything that gets drawn goes through it.
What Happens Inside Renderer Constructor:
When you create a Renderer, its constructor internally creates many subsystems:
CoordinateSystem- Converts between world coordinates (mm) and screen coordinates (pixels)ShapeStyleManager- Manages colors, fills, strokes, opacityShapeRenderer- Draws individual shapes (circles, rectangles, etc.)PathRenderer- Draws complex paths and polygonsBooleanOperationRenderer- Draws results of boolean operationsSelectionSystem- Handles selection highlighting and outlinesHandleSystem- Draws interactive handles (corners, rotation, etc.)DebugVisualizer- Draws debug overlays (bounds, grid, etc.)TransformManager- Handles transformations (position, rotation, scale)InteractionHandler- Processes mouse events (clicks, drags, etc.)
Why So Many Systems?
All of these are related to drawing and interaction, so they're grouped together in the Renderer. It's like a toolbox - all the drawing tools in one place.
Understanding the Canvas Parameter:
The renderer needs the canvas element to:
- Get the 2D drawing context (
canvas.getContext('2d')) - Know the canvas size
- Draw on it
// Step 2.2: Initialize shapes map
// The renderer needs a Map to store shapes
// We initialize it empty because no code has run yet
renderer.shapes = new Map();
What This Does:
Creates an empty Map to store shapes. This Map will hold all the shapes that get created when code runs.
Why a Map?
A Map is perfect for storing shapes:
- Key = shape name (string like "c1", "r1")
- Value = shape object (
{ type: 'circle', params: { radius: 50 }, ... }) - Fast lookups by name
- Easy to iterate through all shapes
Why Empty Initially?
No code has run yet, so no shapes exist. The Map starts empty and gets populated when code executes.
Example After Code Runs:
// After running: shape circle c1 { radius: 50 }
renderer.shapes = new Map([
['c1', { type: 'circle', params: { radius: 50 }, transform: { ... } }]
]);
// You can access shapes:
const circle = renderer.shapes.get('c1'); // Get shape by name
renderer.shapes.forEach((shape, name) => { // Loop through all shapes
console.log(name, shape);
});
If you see an error at this step:
Error: TypeError: Renderer is not a constructor
- What this means: Renderer class not imported or not a class
- Common causes:
- Renderer not imported: Missing
import { Renderer } from './renderer.mjs'; - Renderer not exported: Renderer exists but not exported
- Wrong import: Imported wrong thing (function instead of class)
- Renderer not imported: Missing
- Fix: Check import statement, verify Renderer is exported:
export class Renderer { ... }, ensure importing class not function
Error: TypeError: Cannot read property 'getContext' of null
- What this means: canvas is null when passed to Renderer constructor
- Common causes:
- Canvas not found: getElementById('canvas') returned null
- Canvas not passed: Passed undefined instead of canvas element
- Canvas wrong type: Passed wrong element (not canvas)
- Fix: Check canvas exists before creating renderer:
if (!canvas) throw new Error('Canvas required');, verify canvas is HTMLCanvasElement, ensure getElementById succeeded
Error: TypeError: renderer.shapes is not a Map (later errors)
- What this means: shapes property not initialized or overwritten
- Common causes:
- Not initialized: Forgot
renderer.shapes = new Map(); - Overwritten: Something set shapes to object/array instead of Map
- Property missing: Renderer class doesn't have shapes property
- Not initialized: Forgot
- Fix: Ensure shapes is initialized:
renderer.shapes = new Map();, check nothing overwrites it, verify Renderer class allows shapes property
Error: Renderer created but nothing draws
- What this means: Renderer created but not properly set up
- Common causes:
- Canvas context failed: canvas.getContext('2d') returned null
- Shapes not set: renderer.setShapes() never called
- Redraw not triggered: Shapes set but renderer.redraw() not called
- Fix: Check canvas context is valid, ensure renderer.setShapes() is called with shapes, verify renderer.redraw() is called after setting shapes
Why Renderer Creation is Complex:
When you create a Renderer, its constructor internally creates:
CoordinateSystem- Converts between world coordinates (mm) and screen coordinates (pixels)ShapeStyleManager- Manages colors, fills, strokes, opacityShapeRenderer- Draws individual shapes (circles, rectangles, etc.)PathRenderer- Draws complex paths and polygonsBooleanOperationRenderer- Draws results of boolean operationsSelectionSystem- Handles selection highlighting and outlinesHandleSystem- Draws interactive handles (corners, rotation, etc.)DebugVisualizer- Draws debug overlays (bounds, grid, etc.)TransformManager- Handles transformations (position, rotation, scale)InteractionHandler- Processes mouse events (clicks, drags, etc.)
All of this happens inside the Renderer constructor. It's a lot, but it's all related to drawing and interaction, so it makes sense to group it together.
Why Initialize Shapes Map:
renderer.shapes = new Map() - We initialize an empty Map. This will hold all the shapes that get created when code runs. It starts empty because no code has run yet. The Map uses shape names as keys and shape objects as values.
// Step 3: Register renderer with shape manager
shapeManager.registerRenderer(renderer);
What This Does:
Tells the shape manager about the renderer. This connects the two systems so they can work together.
What is the Shape Manager?
The shape manager is a singleton (only one instance exists). It coordinates updates between:
- Renderer (draws shapes)
- Editor (code)
- Parameter Manager (sliders)
- Interpreter (shapes data)
Think of it as the "traffic controller" - it directs updates to the right places.
Why Register the Renderer?
The shape manager needs to tell the renderer to redraw when shapes change. By registering, the shape manager stores a reference to the renderer so it can call renderer.redraw() later.
What Happens During Registration:
// Inside shapeManager.registerRenderer():
registerRenderer(rendererInstance) {
this.renderer = rendererInstance; // Store reference
// Now shape manager can call this.renderer.redraw() when needed
}
Why This Order:
Renderer must exist before we can register it. Can't register something that doesn't exist yet!
If you see an error at this step:
Error: TypeError: shapeManager.registerRenderer is not a function
- What this means: shapeManager doesn't have registerRenderer method or shapeManager is not the expected object
- Common causes:
- shapeManager not imported: Missing
import { shapeManager } from './shapeManager.mjs'; - shapeManager is null: shapeManager is null/undefined
- Method not defined: registerRenderer() method not implemented in ShapeManager class
- shapeManager not imported: Missing
- Fix: Check shapeManager is imported correctly, verify shapeManager is not null, ensure registerRenderer() method exists in ShapeManager class
Understanding Shape Manager:
The shape manager is a singleton - there's only one instance, created when the module loads. Think of it like a central switchboard - all communication goes through it.
One-Way Registration:
Shape manager knows about renderer, but renderer doesn't know about shape manager directly. Instead, renderer uses callbacks. This prevents circular dependencies:
Shape Manager → knows → Renderer (can call renderer.redraw())
Renderer → uses → Callbacks (calls callback functions, doesn't know about shape manager)
Why This Design:
If renderer knew about shape manager, and shape manager knew about renderer, you'd have a circular dependency. That's hard to manage and can cause problems. Callbacks break the cycle!
// Step 4: Set callback for shape changes
renderer.setUpdateCodeCallback(updateCodeFromShapeChange);
What This Does:
Tells the renderer what function to call when a shape is changed (e.g., when user drags a shape handle). This is how the renderer can update the code without knowing about the editor directly.
What is a Callback?
A callback is a function you give to another component. When something happens, that component calls your function. It's like giving someone your phone number - they can call you when something happens.
Why Use Callbacks?
Callbacks allow components to communicate without directly depending on each other:
- Renderer doesn't need to know about the editor
- Renderer just calls the callback function
- The callback function (updateCodeFromShapeChange) knows how to update the editor
What updateCodeFromShapeChange Does:
This function (defined elsewhere) updates the code in the editor when a shape changes. For example:
- User drags circle's corner handle
- Circle radius changes from 50 to 60
- This callback updates code:
shape circle c1 { radius: 50 }→shape circle c1 { radius: 60 }
The Flow:
User drags shape handle
→ Renderer detects change
→ Renderer calls updateCodeFromShapeChange()
→ updateCodeFromShapeChange() updates editor code
→ Code now matches shape!
If you see an error at this step:
Error: TypeError: renderer.setUpdateCodeCallback is not a function
- What this means: Renderer doesn't have setUpdateCodeCallback method
- Common causes:
- Method not implemented: setUpdateCodeCallback() not defined in Renderer class
- Wrong renderer type: renderer is not a Renderer instance
- Method name typo: setUpdateCodeCallBack vs setUpdateCodeCallback
- Fix: Implement setUpdateCodeCallback() method in Renderer class, verify renderer is Renderer instance, check method name spelling
Step 4: Callback setup. When a shape is dragged on the canvas, the renderer needs a way to update the code. This callback is that way. The renderer doesn't know about the editor directly - it just calls this function when something changes. This keeps the renderer decoupled from the editor.
// Step 5: Initialize drag and drop system
initializeDragDropSystem();
What This Does:
Creates the drag-and-drop system, which provides the shape palette UI. Users can drag shapes from the palette onto the canvas.
What is Drag and Drop System?
The drag-and-drop system provides:
- A floating palette panel with shape icons
- Ability to drag shapes from palette to canvas
- Visual feedback while dragging
- Automatic code generation when shape is dropped
Why After Renderer:
The drag-and-drop system needs the renderer to know where to place shapes on the canvas. It also needs the renderer to trigger redraws after adding shapes.
What initializeDragDropSystem() Does:
This function (defined elsewhere) creates a DragDropSystem instance and sets it up. It needs:
- Renderer (to know canvas and place shapes)
- Editor (will be registered later, after editor is created)
- Shape Manager (to properly update shapes)
If you see an error at this step:
Error: TypeError: initializeDragDropSystem is not a function
- What this means: Function not defined or not in scope
- Common causes:
- Function not defined: initializeDragDropSystem() not implemented
- Function not in scope: Defined in different file, not accessible
- Typo in function name: initializeDragDrop vs initializeDragDropSystem
- Fix: Implement initializeDragDropSystem() function, check function is in correct scope, verify function name spelling
Step 5: Drag and drop. This creates the shape palette UI. Users can drag shapes from the palette onto the canvas. It needs the renderer (to know where to place shapes) and will need the editor (to insert code). We'll wire the editor later.
// Step 6: Create constraint engine
constraintEngine = new ConstraintEngine(renderer, shapeManager, updateCodeFromShapeChange);
constraintEngine.installLiveEnforcer(shapeManager);
What This Does:
Creates the constraint engine, which solves geometric constraints (like "keep two circles 50mm apart").
What is the Constraint Engine?
The constraint engine:
- Solves geometric constraints (distance, coincident, horizontal, vertical, etc.)
- Automatically adjusts shapes to satisfy constraints
- Maintains relationships between shapes
Why These Parameters:
The constraint engine needs:
- renderer - To access shapes (read their positions, update them)
- shapeManager - To properly update shapes when constraints solve
- updateCodeFromShapeChange - Callback to update code when constraints change shapes
What is installLiveEnforcer?
installLiveEnforcer() hooks the constraint engine into the shape manager. When shapes change (user drags them), constraints are automatically enforced (other shapes adjust to maintain relationships).
Why "Live":
"Live" means constraints are enforced automatically in real-time. User drags a shape → constraints solve → other shapes adjust immediately. No need to manually trigger constraint solving!
If you see an error at this step:
Error: TypeError: ConstraintEngine is not a constructor
- What this means: ConstraintEngine not imported or not a class
- Common causes:
- ConstraintEngine not imported: Missing
import { ConstraintEngine } from './constraintEngine.mjs'; - ConstraintEngine not exported: Class exists but not exported
- Wrong import: Imported wrong thing (function instead of class)
- ConstraintEngine not imported: Missing
- Fix: Check import statement, verify ConstraintEngine is exported:
export class ConstraintEngine { ... }, ensure importing class not function
Error: TypeError: constraintEngine.installLiveEnforcer is not a function
- What this means: installLiveEnforcer method not defined
- Common causes:
- Method not implemented: installLiveEnforcer() not defined in ConstraintEngine class
- Method name typo: installLiveEnforer vs installLiveEnforcer
- Wrong instance: constraintEngine is not ConstraintEngine instance
- Fix: Implement installLiveEnforcer() method, check method name spelling, verify constraintEngine is ConstraintEngine instance
Step 6: Constraint engine. This is the geometric constraint solver. It needs:
- The renderer (to know about shapes)
- The shape manager (to update shapes when constraints solve)
- The update callback (to update code when constraints change)
installLiveEnforcer - This hooks the constraint engine into the shape manager. When shapes change, constraints are automatically enforced. This is the "live" part - constraints are always active.
// Step 7: Create constraint overlay
const overlay = new ConstraintOverlay(renderer, constraintEngine);
constraintEngine.onListChanged((list) => {
overlay.refresh();
if (!_syncingConstraints) {
updateCodeFromConstraints(list);
}
});
renderer.constraintEngine = constraintEngine;
}
What This Does:
Creates the constraint overlay, which draws visual indicators for constraints (lines connecting anchor points, etc.).
What is the Constraint Overlay?
The constraint overlay draws visual feedback for constraints:
- Lines showing which anchors are connected
- Labels showing constraint types
- Visual indicators when constraints are satisfied/violated
Why These Parameters:
The overlay needs:
- renderer - To draw on the canvas
- constraintEngine - To know what constraints exist
What is onListChanged?
onListChanged() is a callback that fires when constraints are added or removed. When this happens:
- Overlay refreshes (redraws constraint indicators)
- Code updates (if not currently syncing from code)
Why Check _syncingConstraints Flag:
if (!_syncingConstraints) - This prevents infinite loops:
- If we're updating constraints from code, don't update code from constraints
- That would create a loop: code → constraints → code → constraints → ...
Why Store in Renderer:
renderer.constraintEngine = constraintEngine - The renderer needs to know about the constraint engine so it can pass it to other systems that need it.
If you see an error at this step:
Error: TypeError: ConstraintOverlay is not a constructor
- What this means: ConstraintOverlay not imported or not a class
- Common causes:
- ConstraintOverlay not imported: Missing import statement
- ConstraintOverlay not exported: Class exists but not exported
- Wrong import: Imported wrong thing
- Fix: Check import statement, verify ConstraintOverlay is exported, ensure importing class
Error: TypeError: constraintEngine.onListChanged is not a function
- What this means: onListChanged method not defined
- Common causes:
- Method not implemented: onListChanged() not defined in ConstraintEngine
- Method name typo: onListChange vs onListChanged
- Wrong instance: constraintEngine is not ConstraintEngine instance
- Fix: Implement onListChanged() method, check method name spelling, verify constraintEngine type
Error: Infinite loop when constraints change (code updates, triggers constraints, updates code, etc.)
- What this means: _syncingConstraints flag not working correctly
- Common causes:
- Flag not checked: Missing
if (!_syncingConstraints)check - Flag not set: Forgetting to set
_syncingConstraints = truewhen syncing from code - Flag not cleared: Flag stays true forever, blocks all constraint updates
- Flag not checked: Missing
- Fix: Ensure flag is checked before updating code, set flag before syncing from code, clear flag after syncing completes (use try/finally)
Understanding the Initialization Order:
The order matters! Here's why each step must come in this specific order:
1. Renderer First (Step 2):
- Everything else needs the renderer
- Shape manager needs it to tell it to redraw
- Constraint engine needs it to access shapes
- Drag and drop needs it to place shapes
2. Shape Manager Registration (Step 3):
- Must happen after renderer exists
- Shape manager stores reference to renderer
- Now shape manager can trigger redraws
3. Callback Setup (Step 4):
- Must happen after renderer exists
- Renderer stores the callback function
- Now renderer can update code when shapes change
4. Drag and Drop (Step 5):
- Must happen after renderer exists
- Needs renderer to know canvas and place shapes
- Will need editor later, but that's set up separately
5. Constraint Engine (Step 6):
- Must happen after renderer and shape manager exist
- Needs renderer to access shapes
- Needs shape manager to update shapes when constraints solve
6. Constraint Overlay (Step 7):
- Must happen after renderer and constraint engine exist
- Needs renderer to draw
- Needs constraint engine to know what constraints exist
What Happens If You Change the Order:
// WRONG ORDER - Try constraint engine before renderer:
constraintEngine = new ConstraintEngine(renderer, ...); // ❌ renderer doesn't exist yet!
renderer = new Renderer(canvas); // Create renderer AFTER? No!
// Result: Error! Constraint engine can't access renderer that doesn't exist.
The Dependency Chain:
Renderer (foundation)
↓
Shape Manager Registration (needs renderer)
↓
Callback Setup (needs renderer)
↓
Drag and Drop (needs renderer)
↓
Constraint Engine (needs renderer + shape manager)
↓
Constraint Overlay (needs renderer + constraint engine)
Each step builds on the previous ones. You can't skip steps or do them out of order!
Editor Setup
setupCodeMirror() creates the text editor. CodeMirror is a third-party library that provides syntax highlighting, line numbers, and other editor features. We configure it for the AQUI language.
function setupCodeMirror() {
// 1. Create CodeMirror instance
editor = CodeMirror.fromTextArea(textarea, {
mode: 'aqui',
theme: 'default',
lineNumbers: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
lineWrapping: false,
extraKeys: {
'Shift-Enter': runCode,
'Ctrl-Enter': runCode,
'Cmd-Enter': runCode
}
});
Step 1: CodeMirror configuration.
mode: 'aqui'- We defined a custom mode earlier that highlights AQUI keywords, shapes, colors, etc.lineNumbers: true- Shows line numbers in the gutterautoCloseBrackets: true- When you type{, it automatically adds}matchBrackets: true- Highlights matching brackets when you click on oneindentUnit: 2- Two spaces per indent levelextraKeys- Keyboard shortcuts. Shift+Enter or Ctrl+Enter runs the code
The editor is now ready, but it's not connected to anything yet. We need to wire it up.
// 2. Register with shape manager
shapeManager.registerEditor(editor);
Step 2: Shape manager registration. The shape manager needs the editor so it can update code when shapes change. This is the reverse direction - shapes → code. The shape manager will call editor.setValue() when it needs to update the code.
// 3. Set up change listener (bidirectional sync)
editor.on('change', () => {
if (syncingFromBlocks || _writingEditorFromShape || _writingEditorFromConstraints) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
runCode(); // Execute code when user types
if (editorMode === 'blocks' && blocklyWorkspace) {
rebuildWorkspaceFromAqui(editor.getValue(), blocklyWorkspace);
refreshBlockly();
}
}, 300);
});
}
Step 3: Change listener. This is the heart of the bidirectional sync. Every time the user types, this fires.
The flags check: if (syncingFromBlocks || _writingEditorFromShape || _writingEditorFromConstraints) return;
This prevents infinite loops. If blocks are updating the editor, we don't want to run code (it would update blocks, which would update editor, which would run code, etc.). If we're writing to the editor from shape changes, we don't want to run code (we just updated code to match shapes - running it would be redundant). If we're writing from constraints, same thing.
The debounce: clearTimeout(timeout); timeout = setTimeout(...)
This waits 300ms after the user stops typing before running code. If you run code on every keystroke, it's slow and janky. Debouncing means we wait until the user pauses, then run once. If they keep typing, we cancel the previous timeout and start a new one.
Inside the timeout:
runCode()- This executes the code. It goes through lexer → parser → interpreter → shapes → renderer.if (editorMode === 'blocks' && blocklyWorkspace)- If we're in blocks mode, we also need to rebuild the blocks to match the text. This is the text → blocks sync.
The change listener: This is where text → code → shapes happens. User types, code runs, shapes appear. But it's debounced, so it doesn't run on every keystroke - only after the user pauses.
Blockly Setup
initBlockly() creates the visual editor. Blockly is Google's visual programming library. Instead of typing code, users drag and connect blocks. Each block represents a piece of code.
async function initBlockly() {
// 1. Create Blockly workspace
blocklyWorkspace = Blockly.inject('blocklyDiv', {
toolbox: TOOLBOX_XML,
grid: { spacing: 20, length: 3, colour: '#ccc', snap: true },
zoom: { controls: true, wheel: true },
renderer: 'thrasos'
});
Step 1: Blockly workspace creation.
Blockly.inject('blocklyDiv', ...)- Creates a Blockly workspace in the div with id 'blocklyDiv'. This div must exist in the HTML.toolbox: TOOLBOX_XML- This is an XML string that defines what blocks are available. It's defined at the top of app.js. It includes categories like "Shapes", "Turtle", "Parameters", etc.grid: { spacing: 20, length: 3, colour: '#ccc', snap: true }- Shows a grid on the workspace. Blocks snap to grid positions. Makes it easier to align blocks.zoom: { controls: true, wheel: true }- Allows zooming with controls or mouse wheel.renderer: 'thrasos'- The visual style. 'thrasos' is a modern, clean renderer.
The workspace is now ready, but it's empty. Blocks will be added when code is converted to blocks, or when users drag them from the toolbox.
// 2. Set up change listener (blocks → text sync)
blocklyWorkspace.addChangeListener(event => {
if (event.type === Blockly.Events.UI || syncingFromBlocks) return;
Step 2: Change listener setup. Blockly fires events for everything: block creation, deletion, movement, connection, field changes, etc. We listen to all of them.
The filter: if (event.type === Blockly.Events.UI || syncingFromBlocks) return;
Blockly.Events.UI- These are UI-only events, like dragging a block. We don't care about these - we only care when the block structure actually changes.syncingFromBlocks- If we're currently syncing from blocks (updating the text editor), we don't want to sync back. That would create a loop.
const code = Blockly.JavaScript.workspaceToCode(blocklyWorkspace);
Code generation. Blockly has a built-in code generator. Blockly.JavaScript.workspaceToCode() takes the current block structure and converts it to JavaScript code. But wait - we're using AQUI, not JavaScript!
This is where blocks-umd.js comes in. We defined custom code generators for each AQUI block. When Blockly generates code, it uses our custom generators, which output AQUI syntax, not JavaScript. The function is still called workspaceToCode because Blockly expects JavaScript, but our generators output AQUI.
if (editor.getValue() !== code) {
syncingFromBlocks = true;
editor.operation(() => {
editor.setValue(code);
runCode();
});
syncingFromBlocks = false;
}
});
}
The sync logic:
if (editor.getValue() !== code)- Only update if the code actually changed. If it's the same, don't do anything. This prevents unnecessary updates.syncingFromBlocks = true- Set the flag. This tells the editor's change listener to ignore this update (otherwise it would run code, which would update blocks, which would update editor, etc.).editor.operation(() => {...})- CodeMirror batches multiple edits into one operation. This prevents the editor from firing multiple change events. We set the value and run code, but it's all one operation.editor.setValue(code)- Update the text editor with the generated code.runCode()- Execute the code. This updates the shapes.syncingFromBlocks = false- Clear the flag. We're done syncing.
Bidirectional sync: Blocks change → generate code → update text editor → run code → update shapes. The text editor's change listener is disabled during this (because of the flag), so it doesn't try to sync back to blocks.
Why both editors? Some users prefer typing code. Some prefer dragging blocks. We support both, and they stay in sync. Change one, the other updates automatically.
Building the Code Execution Flow From Scratch - Explained Simply
What You're Building:
The runCode() function executes the complete pipeline from code string to visual shapes. This is the main execution flow - it takes code and turns it into shapes on the canvas.
Real-World Analogy:
Think of it like a recipe:
- Ingredients (code) - What you start with
- Prepare (lexer/parser) - Process the ingredients
- Cook (interpreter) - Turn ingredients into food (shapes)
- Serve (renderer) - Present the food on a plate (canvas)
runCode() is the chef that follows the recipe step by step.
Why This Flow Exists:
Code execution requires multiple transformations:
- String (code text) →
- Tokens (lexer breaks it apart) →
- AST (parser builds structure) →
- Shapes (interpreter creates objects) →
- Visual output (renderer draws on canvas)
Each step transforms the data into a different format, getting closer to the final result.
When runCode() is Called:
This function is called:
- When user types code (debounced, after they stop typing)
- When user moves blocks (blocks generate code, code runs)
- When user clicks "Run" button
- On startup (if editor has code)
The Complete Pipeline:
User types: "shape circle c1 { radius: 50 }"
↓
runCode() called
↓
Lexer: Breaks into tokens
↓
Parser: Builds AST
↓
Interpreter: Creates shape object
↓
Renderer: Draws circle on canvas
↓
User sees: Circle appears!
How to Build It Step by Step:
Step 1: Clear Canvas
function runCode() {
try {
// Step 1.1: Clear canvas and redraw background
// Before drawing new shapes, we clear the old ones
// renderer.clear() does more than just clear - it also:
// - Clears the canvas (removes old shapes)
// - Redraws the background (grid, axes, rulers)
// - Resets the drawing state
// This gives us a fresh canvas to draw on
renderer.clear();
What This Does:
Clears the canvas before drawing new shapes. Think of it like erasing a whiteboard before drawing something new.
Why Clear First:
If you don't clear, old shapes remain visible. You'd see:
- Old shapes (from previous code run)
- New shapes (from current code run)
- Everything overlapping and messy!
Clearing gives you a clean slate.
What renderer.clear() Actually Does:
It does more than just clear:
- Clears the canvas - Removes all previous drawings
- Redraws the background - Grid, axes, rulers, etc.
- Resets drawing state - Colors, line styles, etc. back to defaults
Real-World Analogy:
Like clearing a table before setting it for a new meal:
- Remove old dishes (clear canvas)
- Put down fresh tablecloth (redraw background)
- Reset place settings (reset drawing state)
Why try/catch:
The try { block wraps all the code execution. If anything goes wrong (parse error, runtime error, etc.), we catch it and display an error instead of crashing the app.
If you see an error at this step:
Error: TypeError: renderer.clear is not a function
- What this means: renderer doesn't have clear method or renderer is not a Renderer instance
- Common causes:
- Method not implemented: clear() not defined in Renderer class
- renderer is null: renderer not initialized
- Wrong type: renderer is not a Renderer instance
- Fix: Implement clear() method in Renderer class, check renderer is initialized:
if (!renderer) throw new Error('Renderer not initialized');, verify renderer is Renderer instance
Error: Canvas not clearing (old shapes still visible)
- What this means: clear() not working or shapes being redrawn immediately
- Common causes:
- clear() not called: Forgot to call renderer.clear()
- Shapes redrawn before clear: Code draws shapes, then clears (wrong order)
- clear() implementation wrong: clear() doesn't actually clear canvas
- Fix: Ensure clear() is called first, verify clear() implementation clears canvas, check drawing order (clear → draw, not draw → clear)
Why Clear First: We need a clean slate before drawing new shapes. If we don't clear, old shapes would remain visible, creating visual artifacts. The clear operation also redraws the background (grid, axes, rulers), ensuring they're visible.
Step 2: Get Code from Editor
// Step 2.1: Get code string from editor
// We read the current code from the CodeMirror editor
// This is a string - raw text that the user typed
// or that was generated from blocks
const code = editor.getValue();
What This Does:
Gets the code text from the editor. This is the raw string that the user typed (or that was generated from blocks).
Understanding getValue():
editor.getValue() returns the entire editor content as a string. For example:
// Editor contains:
shape circle c1 { radius: 50 }
shape rectangle r1 { width: 100, height: 50 }
// editor.getValue() returns:
"shape circle c1 { radius: 50 }\nshape rectangle r1 { width: 100, height: 50 }"
Why Store in Variable:
We store it in code variable so we can use it in later steps. It's easier to reference code than to call editor.getValue() multiple times.
If Code is Empty:
If the editor is empty, code will be an empty string "". Later steps will handle this (no shapes to create, so nothing happens).
If you see an error at this step:
Error: TypeError: Cannot read property 'getValue' of null
- What this means: editor is null (not initialized)
- Common causes:
- Editor not created: setupCodeMirror() not called or failed
- Editor variable not set: setupCodeMirror() didn't set global editor variable
- Editor in wrong scope: editor is local variable, not accessible here
- Fix: Check setupCodeMirror() is called and completes, verify editor variable is set:
editor = CodeMirror.fromTextArea(...);, ensure editor is in correct scope (global/module level)
Error: TypeError: editor.getValue is not a function
- What this means: editor is not a CodeMirror instance
- Common causes:
- Wrong object: editor is plain textarea instead of CodeMirror instance
- Editor not initialized: CodeMirror.fromTextArea() didn't work
- Editor type wrong: editor is different type of object
- Fix: Verify editor is CodeMirror instance, check CodeMirror.fromTextArea() succeeded, ensure using CodeMirror editor not plain textarea
Why Get Code:
The editor contains the source code as a string. We need this string to begin the execution pipeline. getValue() returns the entire editor content as a string.
Step 3: Lex and Parse
// Step 3.1: Create lexer and tokenize
// The lexer breaks the code string into tokens
// Tokens are meaningful chunks: keywords, identifiers, numbers, operators
const lexer = new Lexer(code);
What is a Lexer?
A lexer (also called a tokenizer) breaks code into tokens. Tokens are meaningful chunks of code.
Example:
// Code string:
"shape circle c1 { radius: 50 }"
// After lexer (tokens):
[
{ type: 'SHAPE', value: 'shape' },
{ type: 'IDENTIFIER', value: 'circle' },
{ type: 'IDENTIFIER', value: 'c1' },
{ type: 'LBRACE', value: '{' },
{ type: 'IDENTIFIER', value: 'radius' },
{ type: 'COLON', value: ':' },
{ type: 'NUMBER', value: 50 },
{ type: 'RBRACE', value: '}' }
]
Why Tokenize:
The parser works with tokens, not raw strings. Tokens are easier to process - each token has a type and value, making it clear what each piece of code is.
// Step 3.2: Create parser and build AST
// The parser reads tokens and builds an Abstract Syntax Tree
// The AST represents the code's structure as a tree
const parser = new Parser(lexer);
What is a Parser?
A parser reads tokens and builds an Abstract Syntax Tree (AST). The AST represents the code's structure as a tree of objects.
What is an AST?
AST = Abstract Syntax Tree. It's a tree structure representing code. Think of it like an outline:
- The code is like a paragraph (hard to work with)
- The AST is like an outline (easy to navigate and understand)
Example:
// Code:
"shape circle c1 { radius: 50 }"
// AST (simplified):
{
type: 'Program',
statements: [{
type: 'ShapeStatement',
shapeType: 'circle',
name: 'c1',
params: {
radius: { type: 'NumberLiteral', value: 50 }
}
}]
}
Why Create AST:
The interpreter works with AST, not tokens. The AST shows the structure clearly - you can see this is a shape statement, it's a circle, named c1, with radius 50.
// Step 3.3: Parse the code
// This actually runs the parser, producing the AST
// The AST is a JavaScript object representing the code structure
const ast = parser.parse();
What parse() Does:
This actually runs the parser. It reads all the tokens from the lexer and builds the AST. If the code has syntax errors, this will throw an error.
If Parsing Fails:
If code has syntax errors (like missing brace, invalid syntax), parse() throws an error. The try/catch block catches it and displays an error message to the user.
If you see an error at this step:
Error: TypeError: Lexer is not a constructor
- What this means: Lexer not imported or not a class
- Common causes:
- Lexer not imported: Missing
import { Lexer } from './lexer.mjs'; - Lexer not exported: Class exists but not exported
- Wrong import: Imported wrong thing
- Lexer not imported: Missing
- Fix: Check import statement, verify Lexer is exported:
export class Lexer { ... }, ensure importing class
Error: SyntaxError: Unexpected token or parsing errors
- What this means: Code has syntax errors, parser can't parse it
- Common causes:
- Invalid syntax: Code has syntax errors (missing braces, invalid characters, etc.)
- Lexer issue: Lexer produced invalid tokens
- Parser issue: Parser can't handle the token sequence
- Fix: Check code syntax for errors, verify lexer produces correct tokens, check parser handles all syntax correctly, display error to user (don't crash)
Error: TypeError: parser.parse is not a function
- What this means: Parser doesn't have parse method
- Common causes:
- Method not implemented: parse() not defined in Parser class
- Parser not initialized: new Parser() failed or returned wrong type
- Method name typo: pars vs parse
- Fix: Implement parse() method in Parser class, verify Parser constructor works, check method name spelling
Why Lex and Parse: This is the language processing pipeline:
- Lexer: Breaks code into tokens. For example,
"shape circle c1 { radius: 50 }"becomes tokens:[SHAPE, IDENTIFIER("circle"), IDENTIFIER("c1"), LBRACE, IDENTIFIER("radius"), COLON, NUMBER(50), RBRACE] - Parser: Reads tokens and builds an Abstract Syntax Tree (AST). The AST represents the code's structure as a tree of objects.
- AST: A JavaScript object representing the code. For example,
shape circle c1 { radius: 50 }becomes:{ type: 'shape', shapeType: 'circle', name: 'c1', params: { radius: { type: 'number', value: 50 } } }
Why This Transformation: The AST makes it easier to execute code. Instead of parsing strings repeatedly, we parse once into an AST, then execute the AST. The AST is also easier to manipulate (for code generation, optimization, etc.).
// Step 4: Display AST (for debugging)
astOutput.textContent = JSON.stringify(ast, null, 2);
What This Does:
Displays the AST in a debug panel. This is for developers to see what the parser produced.
Understanding JSON.stringify:
JSON.stringify(ast, null, 2) converts the AST object to a JSON string:
ast- The object to stringifynull- Replacer function (not needed, so null)2- Indentation (2 spaces) - makes it readable!
Example Output:
{
"type": "Program",
"statements": [
{
"type": "ShapeStatement",
"shapeType": "circle",
"name": "c1",
"params": {
"radius": {
"type": "NumberLiteral",
"value": 50
}
}
}
]
}
Why Display AST:
Helps debug parsing issues. If shapes don't appear, you can check:
- Did the parser create the right AST?
- Is the structure correct?
- What did the parser actually see?
If astOutput is null:
If the AST output element doesn't exist, this line will throw an error. But that's okay - AST display is optional (just for debugging). You can add a safety check:
if (astOutput) {
astOutput.textContent = JSON.stringify(ast, null, 2);
}
If you see an error at this step:
Error: TypeError: Cannot set property 'textContent' of null
- What this means: astOutput element doesn't exist (null)
- Common causes:
- Element not in HTML: No element with id="ast-output" in HTML
- Element not found: getElementById('ast-output') returned null
- Wrong ID: Element has different ID
- Fix: Add safety check:
if (astOutput) astOutput.textContent = ...;, verify HTML has element, check ID matches exactly (case-sensitive)
// Step 5: Interpret AST → Create shapes
interpreter = new Interpreter();
const result = interpreter.interpret(ast);
What This Does:
Creates a new interpreter and runs it on the AST. This is where shapes are actually created!
What is an Interpreter?
An interpreter executes code (in this case, executes the AST). It walks through the AST and creates shapes based on what it finds.
Why New Interpreter Each Time:
Each code run gets a fresh interpreter with a fresh environment. This means:
- Old shapes are discarded
- Old parameters are cleared
- Fresh start every time
What interpret() Does:
The interpreter walks through the AST:
- Sees a
ShapeStatement→ Creates a shape object - Sees a
ParamStatement→ Stores a parameter - Sees a
ConstraintsStatement→ Extracts constraints
What the Result Contains:
The result contains:
shapes- Map of all created shapesconstraints- Array of extracted constraints- Other data (parameters, layers, etc.)
Example:
// AST has: shape circle c1 { radius: 50 }
// After interpret():
result = {
shapes: new Map([
['c1', { type: 'circle', params: { radius: 50 }, transform: { ... } }]
]),
constraints: [],
// ... other data
}
If you see an error at this step:
Error: TypeError: Interpreter is not a constructor
- What this means: Interpreter not imported or not a class
- Common causes:
- Interpreter not imported: Missing
import { Interpreter } from './interpreter.mjs'; - Interpreter not exported: Class exists but not exported
- Wrong import: Imported wrong thing
- Interpreter not imported: Missing
- Fix: Check import statement, verify Interpreter is exported, ensure importing class
Error: TypeError: interpreter.interpret is not a function
- What this means: interpret method not defined
- Common causes:
- Method not implemented: interpret() not defined in Interpreter class
- Method name typo: interpre vs interpret
- Wrong instance: interpreter is not Interpreter instance
- Fix: Implement interpret() method, check method name spelling, verify interpreter is Interpreter instance
Error: Runtime error during interpretation (shapes not created)
- What this means: Interpretation failed (invalid AST, runtime error, etc.)
- Common causes:
- Invalid AST: AST structure wrong, interpreter can't process it
- Runtime error: Interpreter code has bug (division by zero, etc.)
- Missing shape types: AST references shape type that doesn't exist
- Fix: Check AST structure is valid, verify interpreter code handles all cases, ensure shape types are defined, catch and display errors
Step 5: Interpret. This is where the magic happens. The interpreter walks the AST and actually creates shapes:
new Interpreter()- Creates a new interpreter. Each run gets a fresh interpreter with a fresh environment (parameters, shapes, layers, etc.).interpreter.interpret(ast)- Walks the AST and executes it. For eachshapestatement, it creates a shape object. For eachparamstatement, it stores a parameter. For eachconstraintsblock, it extracts constraints.
The result contains the shapes map and other data. The shapes are JavaScript objects with type, params, and transform properties.
// Step 6: Store interpreter globally
window.interpreter = interpreter;
What This Does:
Stores the interpreter in window.interpreter so other parts of the system can access it.
Why Store Globally:
Other parts of the system need access to the interpreter:
- Parameter Manager - Needs it to show sliders (reads shape parameters)
- Export Functions - Need it to export shapes (reads shape data)
- Constraint Engine - Needs it to get constraints (reads constraint data)
- Debug Tools - May need it for debugging
Why window.interpreter:
Using window.interpreter makes it accessible from:
- Other JavaScript files
- HTML event handlers
- Browser console (for debugging)
Alternative Approaches:
You could also:
- Pass interpreter as parameter (more explicit, but verbose)
- Store in module-level variable (cleaner, but not accessible from HTML)
- Use a global state object (more organized, but more complex)
For simplicity, window.interpreter works well.
If you see an error at this step:
Error: TypeError: Cannot set property 'interpreter' of undefined
- What this means: window object doesn't exist (very rare, not in browser)
- Common causes:
- Not in browser: Running in Node.js or other non-browser environment
- window undefined: window object doesn't exist (extremely rare)
- Fix: Check running in browser environment, verify window exists:
if (typeof window !== 'undefined') window.interpreter = interpreter;
Error: interpreter undefined when accessed later
- What this means: interpreter variable not set or window.interpreter not accessible
- Common causes:
- interpreter is null: new Interpreter() failed
- Assignment failed: window.interpreter assignment didn't work
- Scope issue: Accessing window.interpreter from wrong scope
- Fix: Check interpreter is created successfully, verify window.interpreter is set:
console.log(window.interpreter);, ensure accessing from correct scope
// Step 7: Update renderer with shapes
renderer.setShapes(result.shapes);
What This Does:
Passes the shapes to the renderer. The renderer stores them and draws them on the canvas.
What setShapes() Does:
renderer.setShapes() does several things:
- Stores shapes - Saves them in
renderer.shapesMap - Sorts by render order - Determines drawing order (which shapes appear on top)
- Triggers redraw - Automatically redraws the canvas with new shapes
Why This Order:
Shapes must be created (by interpreter) before they can be drawn (by renderer). You can't draw shapes that don't exist yet!
What Happens Next:
After setShapes() is called:
- Shapes are stored in renderer
- Canvas is cleared
- Shapes are drawn on canvas
- User sees the shapes appear!
If you see an error at this step:
Error: TypeError: renderer.setShapes is not a function
- What this means: setShapes method not defined in Renderer
- Common causes:
- Method not implemented: setShapes() not defined in Renderer class
- renderer is null: renderer not initialized
- Wrong type: renderer is not Renderer instance
- Fix: Implement setShapes() method in Renderer class, check renderer is initialized, verify renderer is Renderer instance
Error: Shapes set but nothing appears on canvas
- What this means: setShapes() doesn't trigger redraw or redraw fails
- Common causes:
- Redraw not triggered: setShapes() doesn't call redraw()
- Canvas context issue: getContext('2d') failed or context invalid
- Shapes empty: result.shapes is empty Map, nothing to draw
- Fix: Ensure setShapes() calls redraw(), verify canvas context is valid, check result.shapes has shapes:
if (result.shapes.size === 0) console.warn('No shapes to draw');
// Step 8: Update constraint engine
if (constraintEngine) {
constraintEngine.clearAllConstraints();
// Add constraints from code
for (const c of interpreter.constraints) {
if (c.type === 'coincident') constraintEngine.addCoincidentAnchors(c.a, c.b);
else if (c.type === 'distance') constraintEngine.addDistance(c.a, c.b, c.dist);
// ... more constraint types
}
constraintEngine.rebuild();
}
What This Does:
Updates the constraint engine with constraints from the code. If constraints are defined in code, they're added to the engine.
Why Check constraintEngine:
The if (constraintEngine) check ensures the constraint engine exists. It should exist (created in initializeComponents), but this is a safety check.
What clearAllConstraints() Does:
Removes all existing constraints. We're about to add new ones from the code, so we clear old ones first. This ensures constraints match the code.
Why Loop Through Constraints:
The interpreter extracted constraints from the AST. Each constraint has:
type- What kind of constraint ('distance', 'coincident', etc.)a,b- Anchor points (which points are constrained)dist- For distance constraints, the target distance
We loop through them and add each one to the constraint engine.
Why rebuild():
rebuild() is important! It:
- Extracts anchor points from all shapes
- Creates anchor objects that link to shape parameters
- Prepares the anchor system for constraint solving
Without rebuild, the engine doesn't know where anchor points are or how to adjust them.
If you see an error at this step:
Error: TypeError: constraintEngine.clearAllConstraints is not a function
- What this means: clearAllConstraints method not defined
- Common causes:
- Method not implemented: clearAllConstraints() not defined in ConstraintEngine
- constraintEngine is null: Constraint engine not created
- Wrong type: constraintEngine is not ConstraintEngine instance
- Fix: Implement clearAllConstraints() method, check constraintEngine is created, verify constraintEngine is ConstraintEngine instance
Error: Constraints added but not solving
- What this means: Constraints added but rebuild() not called or solving not triggered
- Common causes:
- rebuild() not called: Forgot to call rebuild() after adding constraints
- Live enforcement off: constraintEngine.liveEnforce is false
- Solving not triggered: Need to call constraintEngine.solve() or enable live enforcement
- Fix: Ensure rebuild() is called after adding constraints, check liveEnforce is true, trigger solving if needed
Step 8: Update constraints. If there's a constraint engine (there should be, but we check just in case):
clearAllConstraints()- Remove all existing constraints. We're about to add new ones from the code.- Loop through
interpreter.constraints- The interpreter extracted constraints from the AST. Each constraint has a type and anchor points. - Add each constraint to the engine - The engine stores them and will use them to solve.
rebuild()- Rebuild the anchor system. This extracts anchor points from shapes and prepares them for constraint solving.
Constraints are now active. If live enforcement is enabled, they'll automatically adjust shapes to satisfy constraints.
// 9. Register interpreter with shape manager
shapeManager.registerInterpreter(interpreter);
Step 9: Register interpreter. The shape manager needs the interpreter so it can access shapes. When a shape is updated via slider or canvas drag, the shape manager needs to find the shape in the interpreter's environment and update it.
// 10. Update parameter manager if visible
if (parameterManager && parameterManager.menuVisible) {
parameterManager.updateWithLatestInterpreter();
}
Step 10: Update parameter manager. If the parameter manager UI is visible, we update it with the new shapes. This refreshes the sliders and shape list. We only do this if the menu is visible because updating it when it's hidden is wasteful.
} catch (error) {
displayErrors([error]);
}
}
Error handling. If anything goes wrong (parse error, runtime error, etc.), we catch it and display it in the error panel. The user sees what went wrong instead of the app crashing.
The complete flow:
- Code (string) → Lexer → Tokens (array of token objects)
- Tokens → Parser → AST (tree structure)
- AST → Interpreter → Shapes (JavaScript objects in a Map)
- Shapes → Renderer → Canvas (visual representation)
- Constraints → Constraint Engine → Applied (geometric relationships enforced)
Each step transforms the data into a different representation, getting closer to the final result (shapes on screen).
Building the Shape Update Flow From Scratch
What You're Building: The complete flow that handles user interactions with shapes on the canvas. When a user drags a shape handle (corner for scaling, rotation handle for rotating), this flow updates the shape, redraws it, updates sliders, and eventually updates the code. This is the reverse direction - visual manipulation updates code.
Why This Flow: Users need to interact with shapes directly on the canvas. This flow ensures that when they drag handles, the shape updates immediately (for responsive feel), sliders update (for UI consistency), and code updates (for bidirectional sync). It's complex because it coordinates multiple systems.
How to Build It Step by Step:
Step 1: Mouse Movement Detection
The starting point: User clicks and drags a handle on a shape. The handle could be a corner handle (for scaling) or a rotation handle (for rotating).
// Step 1.1: Interaction handler detects drag
// The interaction handler listens to mouse events on the canvas
// When the mouse moves, it checks if we're in a scaling operation
handleMouseMove(event) {
// Step 1.2: Check if we're scaling
// this.scaling is a flag set when the user started dragging a scale handle
// It stays true until mouse up (when dragging ends)
if (this.scaling && this.selectedShape) {
// Step 1.3: Calculate mouse movement delta
// dx, dy are the change in mouse position since the last move
// These are in screen coordinates (pixels)
const dx = event.clientX - this.lastMouseX;
const dy = event.clientY - this.lastMouseY;
// Step 1.4: Delegate to scaling handler
// The interaction handler doesn't know how to scale shapes
// It just detects the interaction and delegates to the appropriate system
this.handleParameterScaling(dx, dy);
}
}
Why This Detection: The interaction handler is the entry point for all mouse interactions. It:
- Listens to mouse events on the canvas
- Checks if we're in an interaction mode (scaling, rotating, etc.)
- Calculates movement deltas (how far the mouse moved)
- Delegates to specialized handlers (doesn't do the work itself)
Why Check Flags:
this.scaling- Only process scaling if we're in scaling mode (user started dragging a scale handle)this.selectedShape- Only scale if there's a selected shape (can't scale nothing)
Why Calculate Delta: We need to know how far the mouse moved, not just where it is. The delta (dx, dy) tells us the movement amount, which we use to calculate how much to scale or rotate.
// 2. Transform manager calculates new values
handleParameterScaling(shape, activeHandle, dx, dy, scaleFactor, shapeName, shapeManager) {
// Calculate new parameter value
const newValue = calculateNewValue(...);
// 3. Shape manager updates shape
shapeManager.updateShapeParameter(shapeName, paramName, newValue, 'canvas');
}
Step 2: Transform calculation. The transform manager knows how to scale different shape types:
- For rectangles: scales width and height
- For circles: scales radius
- For polygons: scales radius (uniform scaling)
- For paths: scales all points
The transform manager calculates the new parameter value based on:
- The current value (e.g., current radius is 50)
- The handle movement (e.g., dragged 10 pixels right)
- The handle type (e.g., corner handle vs. edge handle)
- The shape type (different shapes scale differently)
It doesn't update the shape directly - it calculates the new value and passes it to the shape manager.
Step 3: Shape manager update. The shape manager is the central coordinator. It receives the update request with:
shapeName- Which shape to update (e.g., "c1")paramName- Which parameter (e.g., "radius")newValue- The new value (e.g., 60)'canvas'- The source. This tells the shape manager the change came from canvas interaction.
// 4. Shape manager updates everything
updateShapeParameter(shapeName, paramName, value, source) {
// Update shape object immediately
this.immediateShapeUpdate(shapeName, paramName, value);
Step 4a: Immediate shape update. The shape manager finds the shape in the interpreter's environment and updates it directly:
const shape = this.shapes.get(shapeName);
if (paramName === 'radius') {
shape.params.radius = value;
} else if (paramName.startsWith('position_')) {
const index = paramName === 'position_x' ? 0 : 1;
shape.transform.position[index] = value;
}
// ... more parameter types
The shape object is now updated. But nothing is visible yet - we need to redraw.
// Update visual immediately
this.immediateVisualUpdate();
Step 4b: Immediate visual update. The shape manager tells the renderer to redraw:
immediateVisualUpdate() {
if (this.renderer && this.renderer.redraw) {
this.renderer.redraw();
}
}
The renderer reads the updated shape objects and draws them. The user sees the change immediately. This is throttled to 60fps, but it's still immediate from the user's perspective.
// Update UI (sliders) immediately
this.immediateUISync(shapeName, paramName, value, source);
Step 4c: Immediate UI sync. If the parameter manager UI is visible, we update the sliders:
immediateUISync(shapeName, paramName, value, source) {
if (source === 'canvas' && this.parameterManager) {
this.updateSliderValueImmediate(shapeName, paramName, value);
}
}
We only update sliders if the source is 'canvas'. If the source is 'slider', the slider already has the right value (we just set it), so no need to update it.
The slider value is updated in the DOM. The user sees the slider move to match the shape.
// Schedule code update (debounced)
if (source !== 'code' && source !== 'editor') {
this.scheduleCodeUpdate(shapeName, paramName, value);
}
}
Step 4d: Schedule code update. We don't update code immediately. If we did, we'd be updating code on every pixel of dragging, which is slow and wasteful. Instead, we schedule it:
scheduleCodeUpdate(shapeName, paramName, value) {
if (this.codeUpdateTimer) {
clearTimeout(this.codeUpdateTimer);
}
this.codeUpdateTimer = setTimeout(() => {
this.executeCodeUpdate(shapeName, paramName, value);
}, 200);
}
We wait 200ms after the last change. If the user keeps dragging, we cancel the previous timeout and start a new one. Only when they stop dragging do we update the code.
// 5. Code update happens after delay
executeCodeUpdate(shapeName, paramName, value) {
this.disableAutoRun();
this.parameterManager.updateCodeInEditor(shapeName, paramName, value);
this.enableAutoRunDelayed();
}
Step 5: Code update execution. After the delay, we actually update the code:
disableAutoRun()- This is critical. When we update the editor, it fires a 'change' event. That would normally triggerrunCode(), which would re-execute everything. But we just updated the code to match the shape - we don't want to re-execute it. So we disable auto-run temporarily.updateCodeInEditor()- This finds the shape definition in the code and updates the parameter:
// Find: shape circle c1 { radius: 50 }
// Update to: shape circle c1 { radius: 60 }
It uses regex to find the shape block and update the parameter value.
enableAutoRunDelayed()- After 300ms, we re-enable auto-run. By then, the editor change event has fired and been ignored. Auto-run is safe to re-enable.
The complete cycle:
- User drags handle → Mouse move event
- Interaction handler detects it → Delegates to transform manager
- Transform manager calculates new value → Based on handle movement and shape type
- Shape manager updates shape object → Direct modification of shape.params or shape.transform
- Renderer redraws (immediate) → User sees the change
- Sliders update (immediate) → UI stays in sync
- Code update scheduled (debounced) → Wait 200ms after last change
- Code update executes → Find shape in code, update parameter
- Editor fires change event → But auto-run is disabled
- Auto-run re-enables → After 300ms, safe to re-enable
Why this complexity? We want immediate visual feedback (user sees shape move as they drag), but we don't want to spam the editor with updates. The debouncing and auto-run disabling prevent performance issues and infinite loops.
What if auto-run wasn't disabled? The code would update, which would trigger runCode(), which would re-execute the code, which would create new shapes, which would overwrite the user's changes. The shape would jump back to the code value, ignoring the user's drag. That's why we disable auto-run.
Building the Constraint Flow From Scratch
What You're Building: The complete flow that handles geometric constraints. When constraints are defined in code, shapes automatically adjust to satisfy geometric relationships. This is one of the most complex flows because it involves parsing, equation generation, numerical solving, and bidirectional updates.
Why This Flow: Constraints enable parametric design - users define relationships rather than absolute positions. When one shape changes, constrained shapes adjust automatically. This flow ensures constraints are extracted from code, solved numerically, and applied to shapes.
How to Build It Step by Step:
Step 1: User Defines Constraints in Code
The starting point: User writes constraints in code:
// Step 1.1: Code has constraints block
// Constraints are defined in a special block in the code
constraints {
// Step 1.2: Distance constraint
// This says: "Keep the centers of c1 and c2 exactly 100 units apart"
distance c1.center c2.center 100
// Step 1.3: Coincident constraint
// This says: "Make r1's top-left corner coincide with r2's bottom-right corner"
coincident r1.topLeft r2.bottomRight
}
Why Constraints Block: Constraints are grouped in a dedicated block to make them easy to find and manage. The syntax is clear: constraint type, first anchor, second anchor, and optional value (for distance constraints).
Step 2: Interpreter Extracts Constraints
// Step 2.1: Interpreter parses constraints block
// When the interpreter encounters a constraints block, it extracts each constraint
visitConstraintsStatement(node) {
// Step 2.2: Initialize constraints array
// We'll collect all constraints here
const constraints = [];
// Step 2.3: Process each constraint in the block
for (const constraint of node.constraints) {
// Step 2.4: Extract constraint data
// Each constraint has:
// - type: The constraint type ('distance', 'coincident', 'horizontal', 'vertical')
// - a: First anchor point (shape name and anchor name)
// - b: Second anchor point (shape name and anchor name)
// - dist: For distance constraints, the target distance
constraints.push({
type: constraint.type,
a: { shape: constraint.a.shape, anchor: constraint.a.anchor },
b: { shape: constraint.b.shape, anchor: constraint.b.anchor },
dist: constraint.dist
});
}
// Step 2.5: Store constraints in interpreter
// The interpreter doesn't solve constraints - it just extracts them
// The constraint engine will handle solving
this.constraints = constraints;
}
Why Interpreter Extraction: The interpreter's job is to extract constraints from code, not to solve them. It:
- Parses the constraints block syntax
- Extracts constraint type and anchor points
- Stores them in a structured format
- Leaves solving to the constraint engine
Why Store in Interpreter: Constraints are part of the code's semantics. They're stored in the interpreter so they can be accessed later when setting up the constraint engine. The interpreter acts as a bridge between code and constraint system.
// 3. Constraint engine receives them
for (const c of interpreter.constraints) {
if (c.type === 'distance') {
constraintEngine.addDistance(c.a, c.b, c.dist);
}
// ... more types
}
constraintEngine.rebuild();
Step 3: Constraint engine setup. After code execution, constraints are added to the engine:
addDistance()- Adds a distance constraint. The engine stores it internally.addCoincidentAnchors()- Adds a coincident constraint (two points must be at the same location).addHorizontal()- Adds a horizontal constraint (two points must have the same Y coordinate).addVertical()- Adds a vertical constraint (two points must have the same X coordinate).
rebuild() - This is important. It:
- Extracts anchor points from all shapes (center, topLeft, bottomRight, etc.)
- Creates anchor objects that link to shape parameters (e.g., c1.center links to c1's position_x and position_y)
- Prepares the anchor system for constraint solving
Without rebuild, the engine doesn't know where the anchor points are or how to adjust them.
// 4. Constraint engine solves
applyConstraints() {
// Generate equations from constraints
const equations = this.generateEquations();
Step 4a: Equation generation. Each constraint becomes one or more equations:
distance c1.center c2.center 100becomes:sqrt((c1.x - c2.x)^2 + (c1.y - c2.y)^2) - 100 = 0
coincident r1.topLeft r2.bottomRightbecomes:r1.x - r2.x = 0(X coordinates must match)r1.y - r2.y = 0(Y coordinates must match)
The engine generates these equations as strings. Each equation represents a constraint that must be satisfied.
// Solve using Levenberg-Marquardt
const solution = solveSystem(equations, variables);
Step 4b: Numerical solving. The equations are non-linear (they have squares, square roots, etc.), so we use Levenberg-Marquardt algorithm:
- Start with current shape parameter values
- Calculate how far off we are from satisfying constraints (the "error")
- Calculate derivatives (how changing each parameter affects the error)
- Build a Jacobian matrix (derivatives of all equations with respect to all variables)
- Solve a linear system to find parameter adjustments
- Apply adjustments
- Repeat until error is small enough or max iterations reached
The solver returns new parameter values that satisfy all constraints (or get as close as possible).
// Update shape parameters
for (const [varName, value] of Object.entries(solution)) {
const [shapeName, paramName] = varName.split('.');
shapeManager.updateShapeParameter(shapeName, paramName, value, 'constraints');
}
}
Step 4c: Apply solution. The solution is a map like:
{
'c1.position_x': 50.5,
'c1.position_y': 30.2,
'c2.position_x': 150.5,
'c2.position_y': 30.2,
'r1.position_x': 100,
'r1.position_y': 200
}
We iterate through it and update each shape parameter via the shape manager. The source is 'constraints', which tells the shape manager this change came from constraint solving (not user interaction).
// 5. Shape manager updates shapes
// 6. Renderer redraws
// 7. Code updates (if constraints changed)
Steps 5-7: The update chain.
- Shape manager updates shape objects (same as in shape update flow)
- Renderer redraws (shapes move to satisfy constraints)
- Code updates (if constraints were added/removed via UI, the code block updates)
The complete constraint cycle:
- Code defines constraints → User writes
constraints { ... } - Interpreter extracts them → Parses constraint syntax, extracts anchor points
- Constraint engine adds them → Stores constraints internally
- Engine rebuilds anchors → Extracts anchor points from shapes, links to parameters
- Engine generates equations → Each constraint becomes mathematical equations
- Engine solves equations → Levenberg-Marquardt finds parameter values that satisfy constraints
- Shape manager updates shapes → Applies solution to shape objects
- Renderer redraws → User sees shapes move to satisfy constraints
- Code updates → If constraints changed via UI, code block updates
Live enforcement: If live enforcement is enabled, constraints are automatically enforced whenever shapes change. User drags a shape → constraints solve → other shapes adjust to maintain relationships. This makes the system feel "magnetic" - shapes snap to constraint relationships.
Why this is complex: Constraints create a system of equations. Solving them requires numerical methods. The solver must handle:
- Multiple constraints simultaneously
- Over-constrained systems (more constraints than degrees of freedom)
- Under-constrained systems (fewer constraints than degrees of freedom)
- Numerical stability (avoiding division by zero, handling edge cases)
But from the user's perspective, it's simple: write constraints, shapes adjust automatically.
Export Flow
When user clicks export, shapes are converted from internal JavaScript objects to file formats (SVG for web/graphics, DXF for CAD). This is a one-way transformation - we read shapes and write files.
// 1. User clicks export button
exportSVG.addEventListener('click', () => {
handleSVGExport();
});
Step 1: User interaction. The export button is in the UI. When clicked, it calls handleSVGExport(). There are two export buttons: one for SVG, one for DXF. They work similarly but produce different formats.
// 2. Export function called
function handleSVGExport() {
if (!window.interpreter) {
alert('No shapes to export. Please run some code first.');
return;
}
// 3. Export function processes shapes
exportToSVG(window.interpreter, canvas);
}
Step 2: Validation and delegation. Before exporting, we check:
- Is there an interpreter? (If not, no code has run, so no shapes exist)
- If no interpreter, show an alert and return early
If validation passes, we call the actual export function. We pass:
window.interpreter- Contains all shapes ininterpreter.env.shapescanvas- Needed to get canvas dimensions and coordinate system info
// 4. Export function iterates shapes
export function exportToSVG(interpreter, canvas, filename) {
// Get canvas dimensions
const canvasDims = getCanvasDimensions(canvas);
Step 3: Canvas dimensions. We need to know the canvas size because:
- SVG needs a viewBox (the coordinate space)
- We need to convert world coordinates to screen coordinates
- We need to know the center point (shapes are positioned relative to center)
getCanvasDimensions() reads the canvas element's size and calculates:
- Width and height in pixels
- Center point (offsetX, offsetY)
- Scale factor (if any)
- Dimensions in millimeters (for metadata)
// Create SVG element
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("xmlns", svgNS);
svg.setAttribute("width", `${canvasDims.width.toFixed(2)}px`);
svg.setAttribute("height", `${canvasDims.height.toFixed(2)}px`);
svg.setAttribute("viewBox", `0 0 ${canvasDims.width.toFixed(2)} ${canvasDims.height.toFixed(2)}`);
Step 4: SVG structure creation. We create the root SVG element:
document.createElementNS(svgNS, "svg")- Creates an SVG element in the SVG namespace. This is required for proper SVG.- Set width and height attributes (in pixels)
- Set viewBox (defines the coordinate system)
- Add xmlns (XML namespace declaration)
The SVG is now ready, but empty. We need to add shapes to it.
// Create main group (centered, like renderer)
const mainGroup = document.createElementNS(svgNS, "g");
mainGroup.setAttribute("transform", `translate(${canvasDims.offsetX}, ${canvasDims.offsetY})`);
svg.appendChild(mainGroup);
Step 5: Main group creation. Shapes in Otto are positioned relative to the canvas center (0,0 is at center). SVG's coordinate system has (0,0) at top-left. So we create a group and translate it to the center. All shapes will be added to this group, so they're automatically positioned correctly.
// Convert each shape
interpreter.env.shapes.forEach((shape, shapeName) => {
if (shape._consumedByBoolean) return; // Skip shapes used in boolean ops
const shapeElement = createSVGShapeWithMMConversion(shape, shapeName, svgNS, canvasTransform);
if (shapeElement) {
mainGroup.appendChild(shapeElement);
}
});
Step 6: Shape conversion. We iterate through all shapes:
interpreter.env.shapes- This is a Map of shape names to shape objectsshape._consumedByBoolean- Shapes used in boolean operations are marked with this flag. We skip them because they're already part of the boolean result shape.createSVGShapeWithMMConversion()- This is the conversion function. It:- Converts shape parameters from millimeters to pixels (SVG uses pixels)
- Creates the appropriate SVG element (circle, rect, path, etc.)
- Applies styling (fill, stroke, colors)
- Applies transforms (position, rotation, scale)
- Returns the SVG element
Each shape type has its own converter. A circle becomes an SVG <circle>. A rectangle becomes an SVG <rect>. A path becomes an SVG <path> with a path data string.
// Serialize and download
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svg);
downloadSVG(svgString, filename);
}
Step 7: Serialization and download.
XMLSerializer- Browser API that converts DOM elements to XML stringsserializeToString(svg)- Converts the entire SVG DOM tree to a stringdownloadSVG()- Creates a blob, creates a download link, clicks it, cleans up
The blob is created with MIME type image/svg+xml. The browser treats it as an SVG file and downloads it.
The complete export flow:
- User clicks export → Button click event
- Validation → Check if shapes exist
- Get canvas dimensions → Read canvas size, calculate center, scale
- Create SVG structure → Root element, main group, namespaces
- Convert each shape → Shape object → SVG element (with unit conversion, styling, transforms)
- Serialize to string → DOM tree → XML string
- Create blob and download → Blob → Download link → File saved
DXF export is similar but:
- Uses DXF format (text-based CAD format)
- No unit conversion needed (DXF uses millimeters, same as AQUI)
- Different element types (CIRCLE, LINE, ARC, etc. instead of SVG elements)
- More complex structure (DXF has sections: HEADER, TABLES, ENTITIES, etc.)
Why export? Users need to get their designs out of Otto:
- SVG for web graphics, illustrations, laser cutting
- DXF for CAD software, CNC machining, manufacturing
The export system bridges the gap between Otto's internal representation and external file formats.
The Complete Data Flow - Explained Simply
What This Section Shows:
This section shows how data moves through the system. It's the big picture - you'll follow data from user input (typing code) all the way to visual output (shapes on canvas) and back again.
Why Understanding Data Flow Matters:
Understanding data flow helps you:
- Trace problems (follow the data to find where it breaks)
- Add features (know where data comes from and goes to)
- Debug effectively (see the path data takes through the system)
- Understand integration (see how components work together)
User Input
↓
[Text Editor] or [Blockly]
↓
Code String
↓
[Lexer] → Tokens
↓
[Parser] → AST
↓
[Interpreter] → Shapes + Constraints
↓
[Shape Manager] ← → [Renderer] → Canvas
↓ ↓
[Parameter Manager] [Constraint Engine]
↓ ↓
Sliders UI Constraint Overlay
Forward flow (code → shapes):
User Input - User types code or moves blocks
- Text editor: Raw text string
- Blockly: Block structure (converted to code string)
Code String - The source code as a string
- Example:
"shape circle c1 { radius: 50 }"
- Example:
Lexer - Breaks code into tokens
- Input:
"shape circle c1 { radius: 50 }" - Output:
[SHAPE, IDENTIFIER("circle"), IDENTIFIER("c1"), LBRACE, IDENTIFIER("radius"), COLON, NUMBER(50), RBRACE]
- Input:
Parser - Builds Abstract Syntax Tree
- Input: Token array
- Output: AST object like
{ type: 'shape', shapeType: 'circle', name: 'c1', params: { radius: { type: 'number', value: 50 } } }
Interpreter - Executes AST, creates shapes
- Input: AST
- Output: Shape objects in
interpreter.env.shapesMap - Shape object:
{ type: 'circle', params: { radius: 50 }, transform: { position: [0, 0], rotation: 0, scale: [1, 1] } }
Shape Manager - Coordinates updates
- Receives shapes from interpreter
- Registers them internally
- Passes them to renderer
Renderer - Draws shapes to canvas
- Input: Shape objects
- Output: Visual representation on HTML5 canvas
- Uses coordinate system to convert world coordinates to screen coordinates
- Uses style manager to apply colors, fills, strokes
- Uses shape renderer to draw each shape type
Canvas - The visual output
- User sees shapes on screen
Side flows:
Parameter Manager - Reads shapes, creates sliders
- Input: Shape objects from interpreter
- Output: UI sliders for each parameter
- Updates when shapes change
Constraint Engine - Reads constraints, solves them
- Input: Constraints from interpreter
- Output: Updated shape parameters (via shape manager)
- Solves equations, adjusts shapes
Reverse flow (shapes → code):
User Interaction - User drags shape handle or changes slider
- Canvas: Mouse drag event
- Sliders: Input change event
Interaction Handler / Parameter Manager - Detects change
- Calculates new parameter value
- Calls shape manager
Shape Manager - Updates shape object
- Modifies shape.params or shape.transform
- Triggers renderer redraw (immediate)
- Schedules code update (debounced)
Code Update - Updates editor
- Finds shape definition in code
- Updates parameter value
- Editor fires change event (but auto-run disabled)
Editor - Code string updated
- Text now matches shape
- If in blocks mode, blocks also update
Bidirectional arrows explained:
Shape Manager ↔ Renderer: Shape manager tells renderer to redraw when shapes change. Renderer calls shape manager callback when user interacts with shapes.
Shape Manager ↔ Parameter Manager: Shape manager updates sliders when shapes change. Parameter manager calls shape manager when sliders change.
Shape Manager ↔ Constraint Engine: Constraint engine calls shape manager to update shapes when constraints solve. Shape manager notifies constraint engine when shapes change (for live enforcement).
Code ↔ Shapes: Code creates shapes (forward). Shape changes update code (reverse). This is the bidirectional sync we discussed earlier.
Data transformations:
At each stage, data is transformed:
- String → Tokens (lexical analysis)
- Tokens → AST (parsing)
- AST → Objects (interpretation)
- Objects → Visual (rendering)
- Visual → Objects (interaction)
- Objects → String (code update)
Each transformation changes the representation but preserves the meaning. A circle is a circle whether it's "shape circle c1 { radius: 50 }" or a JavaScript object or pixels on a canvas.
Why this architecture?
Separation of concerns: Each stage has one job. Lexer only tokenizes. Parser only parses. Interpreter only executes. Renderer only draws.
Bidirectional flow: Changes can come from anywhere (code, blocks, canvas, sliders) and propagate everywhere else.
Central coordination: Shape manager is the hub. Everything flows through it. This prevents chaos and ensures consistency.
Reactive updates: When something changes, everything that depends on it updates automatically. No manual synchronization needed.
Component Dependencies - Explained Simply
What This Section Shows:
This section shows which components depend on which other components. It's like a map showing what depends on what.
Why Understanding Dependencies Matters:
Understanding dependencies helps you:
- Know what breaks - If you change component A, what else breaks?
- Know initialization order - What must be created first?
- Avoid circular dependencies - Don't create dependency loops
- Make safe changes - Understand impact before changing code
Real-World Analogy:
Think of it like a construction project:
- Foundation must be built before walls (dependencies)
- Walls must be built before roof (initialization order)
- Can't have circular dependencies (wall needs roof, roof needs wall = impossible!)
Understanding dependencies is like understanding the construction blueprint.
app.js (orchestrator)
├── Renderer
│ ├── CoordinateSystem
│ ├── ShapeStyleManager
│ ├── ShapeRenderer
│ ├── PathRenderer
│ ├── BooleanOperationRenderer
│ ├── SelectionSystem
│ ├── HandleSystem
│ ├── DebugVisualizer
│ ├── TransformManager
│ └── InteractionHandler
│
├── Interpreter
│ ├── Environment
│ ├── Lexer
│ ├── Parser
│ └── Shapes (imported)
│
├── ShapeManager (singleton)
│ └── Coordinates everything
│
├── ConstraintEngine
│ ├── Constraints
│ ├── Math (solveSystem, autodiff)
│ └── ConstraintOverlay
│
├── ParameterManager
│ └── Uses ShapeManager
│
├── DragDropSystem
│ └── Uses Renderer, Editor, ShapeManager
│
└── Export Systems
└── Use Interpreter, Canvas
Dependency levels:
Level 1: Foundation (no dependencies)
Lexer- Only needs input stringParser- Only needs lexerEnvironment- Only needs itselfShapes- Pure shape classes, no dependenciesMathsystem - Pure mathematical functions
These are the building blocks. They don't depend on anything else, so they're safe to use anywhere.
Level 2: Core systems (depend on foundation)
Interpreter- Depends on Lexer, Parser, Environment, ShapesCoordinateSystem- Only needs canvas elementShapeStyleManager- Only needs color systemTransformManager- Pure math, no dependencies
These use the foundation but don't depend on higher-level systems.
Level 3: Rendering (depend on core)
ShapeRenderer- Depends on CoordinateSystem, ShapeStyleManagerPathRenderer- Depends on CoordinateSystem, ShapeStyleManagerBooleanOperationRenderer- Depends on CoordinateSystem, ShapeStyleManagerSelectionSystem- Depends on CoordinateSystemHandleSystem- Depends on CoordinateSystem, TransformManagerDebugVisualizer- Depends on canvas context
These handle drawing. They need coordinate systems and styling, but not the full renderer.
Level 4: Integration (depend on rendering)
Renderer- Depends on all rendering subsystemsInteractionHandler- Depends on Renderer, CoordinateSystemConstraintEngine- Depends on Renderer, ShapeManager, Math systemConstraintOverlay- Depends on Renderer, ConstraintEngine
These tie rendering systems together and add interaction.
Level 5: UI and coordination (depend on integration)
ShapeManager- Depends on Renderer, Interpreter, Editor, ParameterManager (but coordinates them, doesn't directly use them)ParameterManager- Depends on ShapeManager, Interpreter, EditorDragDropSystem- Depends on Renderer, Editor, ShapeManagerExport Systems- Depend on Interpreter, Canvas
These provide user interfaces and coordinate between systems.
Level 6: Orchestration (depends on everything)
app.js- Depends on everything. It's the conductor of the orchestra.
Key dependency rules:
Renderer is central - Almost everything depends on it or is used by it. It's the visual foundation.
ShapeManager coordinates - It doesn't directly depend on much, but everything depends on it. It's the hub.
Interpreter is independent - It only depends on foundation (lexer, parser, environment). It doesn't know about rendering or UI.
ConstraintEngine is complex - It depends on Renderer (for shapes), ShapeManager (for updates), and Math (for solving). It's a bridge between systems.
Export systems are read-only - They read from Interpreter and Canvas but don't modify anything. Safe to call anytime.
Circular dependencies to avoid:
- Renderer ↔ ShapeManager: Renderer doesn't directly reference ShapeManager (uses callbacks instead)
- Interpreter ↔ Renderer: Interpreter doesn't know about Renderer (shapes are just objects)
- ConstraintEngine ↔ ShapeManager: ConstraintEngine calls ShapeManager, but ShapeManager doesn't call ConstraintEngine directly
Initialization order matters:
Because of dependencies, you must initialize in this order:
- Foundation (Lexer, Parser, Environment, Shapes, Math)
- Core systems (Interpreter, CoordinateSystem, etc.)
- Rendering subsystems (ShapeRenderer, etc.)
- Integration (Renderer, ConstraintEngine)
- UI (ParameterManager, DragDropSystem)
- Orchestration (app.js wiring)
If you initialize out of order, things break. Try creating Renderer before CoordinateSystem - it won't work.
Testing dependencies:
To test a component in isolation:
- Mock its dependencies
- Or test it with minimal dependencies
- Or test the whole chain together (integration test)
For example, to test the Interpreter:
- You can test it with just Lexer, Parser, Environment
- You don't need Renderer or ShapeManager
- This makes interpreter tests fast and isolated
Event Propagation - Explained Simply
What This Section Shows:
This section shows how events (like mouse clicks, keyboard presses) flow through the system. We'll trace a complete event from start to finish to see how it triggers updates.
Why Understanding Event Flow Matters:
Understanding event flow helps you:
- Debug interaction issues - Why isn't my click working?
- Add new interactions - Where do I hook in new event handlers?
- Understand user input - How does clicking become a shape change?
- Follow the chain - See how events trigger updates through the system
Real-World Analogy:
Think of it like a phone call:
- Someone calls (user clicks)
- Receptionist answers (event handler receives event)
- Receptionist transfers call (event delegates to correct handler)
- Department handles request (component processes event)
- Response goes back (update propagates back)
Understanding event flow is like understanding the phone system in a company.
Mouse Event
↓
[Canvas] → InteractionHandler
↓
[HandleSystem] or [Shape Hit Test]
↓
[TransformManager] or [ShapeManager]
↓
[ShapeManager.updateShapeParameter]
↓
[Renderer.redraw] (immediate)
[ParameterManager.updateSlider] (immediate)
[Code Update] (debounced)
↓
[Editor.setValue] → Editor 'change' event
↓
[But auto-run disabled] → No re-execution
Step-by-step event flow:
1. Mouse Event Origin
canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
The canvas element listens to mouse events. When user clicks, the browser fires a mousedown event. The canvas receives it and forwards it to the interaction handler.
2. Interaction Handler Processing
handleMouseDown(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const worldPos = this.coordinateSystem.screenToWorld(x, y);
// Check for handle hit
if (this.selectedShape) {
const handleHit = this.renderer.handleSystem.getHandleAtPoint(x, y, this.selectedShape);
if (handleHit) {
this.activeHandle = handleHit;
this.scaling = true;
return;
}
}
}
The interaction handler:
- Converts screen coordinates to canvas coordinates (subtract canvas offset)
- Converts canvas coordinates to world coordinates (use coordinate system)
- Checks if mouse is over a handle (if shape is selected)
- Sets flags (
scaling = true,activeHandle = handleHit)
3. Handle System Hit Test
getHandleAtPoint(x, y, shape) {
const handles = this.getHandlePositions(shape);
for (const handle of handles) {
const distance = Math.sqrt(
Math.pow(x - handle.x, 2) + Math.pow(y - handle.y, 2)
);
if (distance < this.handleRadius) {
return handle;
}
}
return null;
}
The handle system:
- Gets all handle positions for the shape (corners, rotation handle, etc.)
- Calculates distance from mouse to each handle
- Returns the handle if mouse is within radius
- Returns null if no handle hit
4. Mouse Move Event
handleMouseMove(event) {
if (this.scaling && this.selectedShape) {
this.handleParameterScaling(dx, dy);
}
}
When mouse moves while scaling:
- Calculate delta (dx, dy) from last position
- Call
handleParameterScalingwith the delta - This happens on every mouse move (could be 60+ times per second)
5. Transform Manager Calculation
handleParameterScaling(shape, activeHandle, dx, dy, scaleFactor, shapeName, shapeManager) {
// Calculate scale based on handle movement
const distance = Math.sqrt(dx * dx + dy * dy);
const scaleFactor = 1 + (distance / 100);
// Calculate new parameter value
const currentValue = shape.params[paramName];
const newValue = currentValue * scaleFactor;
// Update via shape manager
shapeManager.updateShapeParameter(shapeName, paramName, newValue, 'canvas');
}
The transform manager:
- Calculates how much to scale based on mouse movement
- Gets current parameter value
- Calculates new value
- Delegates to shape manager (doesn't update shape directly)
6. Shape Manager Update Chain
updateShapeParameter(shapeName, paramName, value, source) {
// Immediate updates
this.immediateShapeUpdate(shapeName, paramName, value);
this.immediateVisualUpdate();
this.immediateUISync(shapeName, paramName, value, source);
// Scheduled update
if (source !== 'code') {
this.scheduleCodeUpdate(shapeName, paramName, value);
}
}
Shape manager triggers three immediate updates and one scheduled update:
- Shape object update (modify the data)
- Visual update (redraw canvas)
- UI sync (update sliders)
- Code update (scheduled, debounced)
7. Renderer Redraw
redraw() {
this.clear();
for (const [name, shape] of this.shapes) {
this.drawShape(shape, isSelected, isHovered, name);
}
}
Renderer:
- Clears canvas (and redraws background, grid, etc.)
- Iterates through all shapes
- Draws each shape using appropriate renderer (ShapeRenderer, PathRenderer, etc.)
This happens immediately, so user sees the change as they drag.
8. Parameter Manager Update
updateSliderValueImmediate(shapeName, paramName, value) {
const elements = this.paramsList.querySelectorAll(
`[data-shape-name="${shapeName}"][data-param-name="${paramName}"]`
);
elements.forEach(element => {
element.value = value;
});
}
Parameter manager:
- Finds slider elements matching shape and parameter
- Updates their values
- Sliders move to match the shape
9. Code Update (Debounced)
scheduleCodeUpdate(shapeName, paramName, value) {
if (this.codeUpdateTimer) {
clearTimeout(this.codeUpdateTimer);
}
this.codeUpdateTimer = setTimeout(() => {
this.executeCodeUpdate(shapeName, paramName, value);
}, 200);
}
Code update is scheduled:
- Cancel any pending update
- Schedule new update after 200ms
- Only executes if user stops dragging
10. Editor Update
executeCodeUpdate(shapeName, paramName, value) {
this.disableAutoRun();
this.parameterManager.updateCodeInEditor(shapeName, paramName, value);
this.enableAutoRunDelayed();
}
When code update executes:
- Disable auto-run (prevents re-execution)
- Update code in editor (find shape, update parameter)
- Editor fires 'change' event
- But auto-run is disabled, so
runCode()doesn't execute - Re-enable auto-run after delay
Why disable auto-run? We just updated code to match the shape. If we re-execute, we'd update the shape again, causing a loop. The shape would jump back to the code value, ignoring the user's drag.
Event flow characteristics:
- Immediate updates: Visual and UI updates happen immediately for responsive feel
- Debounced updates: Code updates are debounced to avoid spam
- Flag-based flow control: Flags (
syncingFromBlocks,_writingEditorFromShape) prevent loops - Delegation: Each component delegates to the next, doesn't do everything itself
- Separation of concerns: Each component has one job in the event flow
Common event flow issues:
- Missing flag check: Forgetting to check
syncingFromBlockscauses infinite loops - Wrong coordinate system: Using screen coordinates instead of world coordinates (or vice versa)
- Timing issues: Updating code before shape is updated, or vice versa
- Event propagation: Events bubbling up when they shouldn't, or not bubbling when they should
Understanding event flow helps you debug these issues and add new features correctly.
The Global State - Explained Simply
What This Section Shows:
This section explains the global variables that hold everything together. These are defined at the top of app.js and are accessible throughout the module.
What are Global Variables?
Global variables are variables that can be accessed from anywhere in your code. They're like shared storage - any part of your code can read or modify them.
Why Understanding Global State Matters:
Understanding global state helps you:
- Know what's accessible - What variables are available everywhere?
- Understand architecture - Why are these global instead of passed as parameters?
- Debug effectively - Where did this value come from?
- Make safe changes - What breaks if I change this variable?
Real-World Analogy:
Think of global variables like a shared whiteboard:
- Everyone can see it (accessible everywhere)
- Everyone can write on it (any code can modify it)
- It's permanent (lasts for the app's lifetime)
Understanding global state is like understanding what's written on the shared whiteboard.
let editor; // CodeMirror instance
let canvas; // HTML5 canvas element
let renderer; // Renderer instance
let interpreter; // Current interpreter instance
let parameterManager; // Parameter UI manager
let dragDropSystem; // Drag and drop system
let blocklyWorkspace; // Blockly workspace
let constraintEngine; // Constraint solver
Component instances:
editor- The CodeMirror text editor instance. Created insetupCodeMirror(). Used to read/write code. Also stored inwindow.editorfor global access.canvas- The HTML5 canvas element. Retrieved from DOM ininitializeComponents(). This is where shapes are drawn. The renderer uses it to get the 2D context.renderer- The main Renderer instance. Created ininitializeComponents(). Handles all drawing, coordinate systems, interaction, etc. This is the visual foundation.interpreter- The current Interpreter instance. Created fresh each timerunCode()is called. Contains the environment (parameters, shapes, layers) from the last code execution. Also stored inwindow.interpreterfor global access.parameterManager- The ParameterManager instance. Created lazily when user opens the parameter menu. Handles the slider UI. Can be null if menu hasn't been opened yet.dragDropSystem- The DragDropSystem instance. Created ininitializeDragDropSystem(). Handles the shape palette and drag-and-drop functionality.blocklyWorkspace- The Blockly workspace instance. Created ininitBlockly(). This is the visual block editor. Can be null if Blockly isn't loaded yet.constraintEngine- The ConstraintEngine instance. Created ininitializeComponents(). Handles geometric constraint solving. Always exists (created early).
// Sync flags
let syncingFromBlocks = false;
let _writingEditorFromShape = false;
let _writingEditorFromConstraints = false;
let _syncingConstraints = false;
Synchronization flags:
These flags prevent infinite loops in bidirectional sync. They're boolean flags that indicate "we're currently syncing in this direction, so don't sync back."
syncingFromBlocks- Set totruewhen blocks are updating the text editor. Prevents the editor's change listener from syncing back to blocks. Cleared after sync completes._writingEditorFromShape- Set totruewhen shape changes are updating the editor. Prevents the editor's change listener from running code (which would re-execute and overwrite the shape change). Cleared after update._writingEditorFromConstraints- Set totruewhen constraint changes are updating the editor. Similar to above, but for constraints. Prevents code re-execution._syncingConstraints- Set totruewhen constraints are being synced from code. Prevents constraint changes from updating code (which would update constraints, which would update code, etc.).
Flag usage pattern:
// Before updating
syncingFromBlocks = true;
try {
// Do the update
editor.setValue(code);
} finally {
// Always clear, even on error
syncingFromBlocks = false;
}
Always use try/finally to clear flags. If an error happens, the flag stays set and sync breaks forever.
// Shape manager is a singleton
import { shapeManager } from './shapeManager.mjs';
Shape manager singleton:
The shape manager is a singleton - there's only one instance, created when the module loads. It's exported and imported wherever needed. This ensures there's only one shape manager coordinating everything.
Why globals?
Some components need access to others:
runCode()needseditor,renderer,interpreter,constraintEnginehandleSVGExport()needswindow.interpreter,canvasupdateCodeFromShapeChange()needseditor,renderer,constraintEngine- Event handlers need access to multiple components
Alternatives to globals:
- Dependency injection: Pass components as parameters. More explicit, but verbose.
- Event system: Components communicate via events. More decoupled, but harder to trace.
- Service locator: Central registry of components. Similar to globals but more organized.
We use globals because:
- Simple and direct
- Easy to access from anywhere
- No need to thread dependencies through many layers
- The app is small enough that globals are manageable
Global state issues:
- Initialization order: Globals must be initialized in the right order. If
runCode()runs beforerendererexists, it crashes. - Null checks: Always check if globals exist before using them.
if (renderer && renderer.redraw) { ... } - Testing: Globals make testing harder. You can't easily mock them. But for a small app, it's acceptable.
- State consistency: Multiple places can modify globals. Need to ensure they stay consistent.
Best practices with globals:
- Initialize early: Set up globals in
initializeComponents()orsetupCodeMirror() - Check before use: Always verify globals exist:
if (renderer) { ... } - Document dependencies: Comment what each global is used for
- Minimize globals: Only make things global if they're truly needed everywhere
- Use flags carefully: Sync flags must be set/cleared correctly or everything breaks
The global namespace:
We also use window for some things:
window.editor- For HTML event handlers that need editor accesswindow.interpreter- For export functions and other global accesswindow.runCode- For HTML buttons and keyboard shortcutswindow.aqui- Namespace object with all public APIs
This makes some things accessible from HTML and other scripts, but most of the system uses module-level globals.
Common Gotchas - Explained Simply
What are "Gotchas"?
"Gotchas" are common mistakes or issues that trip people up. These are things that seem obvious once you know them, but are easy to miss the first time.
Why This Section Matters:
Understanding common gotchas helps you:
- Avoid mistakes - Don't repeat errors others have made
- Debug faster - Recognize common problems quickly
- Write better code - Learn from past mistakes
- Save time - Don't spend hours debugging known issues
Gotcha 1: Initialization Order
The Problem: Components must be initialized in the right order. If you create things in the wrong order, things break.
The Rule:
- Renderer before everything else
- Shape manager registration after components are created
- Editor after DOM is ready
- Blockly after DOM is ready
Example of Wrong Order:
// WRONG - Trying to use renderer before it exists:
shapeManager.registerRenderer(renderer); // ❌ Error: renderer is undefined!
renderer = new Renderer(canvas);
Example of Correct Order:
// CORRECT - Create renderer first:
renderer = new Renderer(canvas); // ✓ Create first
shapeManager.registerRenderer(renderer); // ✓ Use after creation
Why This Happens: Some components need other components to exist first (dependencies). You can't use something that doesn't exist yet!
Gotcha 2: Circular Dependencies
The Problem: If component A depends on component B, and component B depends on component A, you get a circular dependency. This causes problems.
Example:
// Component A needs B:
class A {
constructor() {
this.b = new B(this); // A needs B
}
}
// Component B needs A:
class B {
constructor(a) {
this.a = a; // B needs A
}
}
// Problem: Can't create A without B, can't create B without A!
The Solution: Use callbacks or events to break the cycle. Instead of direct references, use functions:
// A doesn't directly reference B:
class A {
constructor(callback) {
this.callback = callback; // Store callback instead
}
}
// B calls the callback instead of referencing A directly:
class B {
doSomething(a) {
a.callback(); // Call callback, don't store reference
}
}
Why This Happens: Sometimes components need to communicate with each other. Direct references create cycles. Callbacks break the cycle.
Gotcha 3: Sync Flags
The Problem: Forgetting to set or clear sync flags causes infinite loops. Code updates trigger updates which trigger updates forever.
Example of Missing Flag:
// WRONG - No flag, infinite loop:
editor.on('change', () => {
blocks.update(code); // Blocks update
// Blocks trigger editor change...
// Which triggers this again...
// Which triggers blocks update...
// INFINITE LOOP! 💥
});
Example of Correct Flag Usage:
// CORRECT - Use flag to prevent loop:
editor.on('change', () => {
if (syncingFromBlocks) return; // ✓ Check flag
syncingFromBlocks = true; // ✓ Set flag
try {
blocks.update(code);
} finally {
syncingFromBlocks = false; // ✓ Always clear flag
}
});
Why try/finally:
Always use try/finally to clear flags. If an error happens, the flag still gets cleared. Otherwise, the flag stays true forever and breaks everything!
Gotcha 4: Timing Issues
The Problem: Some operations need delays. If you try to do things immediately, they fail because things aren't ready yet.
Examples:
- Canvas resize needs time for browser to calculate new size
- Editor refresh needs time for CodeMirror to finish attaching
- Blockly render needs time for DOM to be ready
Solution:
Use setTimeout with a small delay (100ms is usually enough):
setTimeout(() => {
// Things that need delay:
editor.refresh();
blockly.resize();
canvas.resize();
}, 100);
Why 100ms: 100 milliseconds is usually enough for:
- Browser to finish rendering
- Elements to get their final sizes
- Libraries to finish initializing
It's fast enough that users don't notice, but long enough for everything to be ready.
Gotcha 5: State Consistency
The Problem: Shapes exist in multiple places:
interpreter.env.shapes(interpreter's copy)renderer.shapes(renderer's copy)shapeManager.shapes(shape manager's copy)
If these get out of sync, you see wrong shapes or errors.
The Rule: Keep all copies in sync! When you update a shape, update all places where it's stored.
Example:
// WRONG - Only update one place:
interpreter.env.shapes.get('c1').radius = 60;
// renderer.shapes still has old value (50)!
// User sees wrong shape! ❌
// CORRECT - Update all places:
const shape = interpreter.env.shapes.get('c1');
shape.radius = 60;
renderer.shapes.set('c1', shape); // ✓ Update renderer too
shapeManager.updateShape('c1', shape); // ✓ Update shape manager too
Better Solution: Use the shape manager! It handles updating all places automatically:
// BEST - Let shape manager handle it:
shapeManager.updateShapeParameter('c1', 'radius', 60);
// Shape manager updates interpreter, renderer, and itself! ✓
Gotcha 6: Event Handlers
The Problem: Event handlers can fire in unexpected orders. Multiple handlers might fire, or handlers might fire when you don't expect them to.
Example:
// Handler 1:
editor.on('change', () => {
runCode(); // Runs code
});
// Handler 2:
button.on('click', () => {
runCode(); // Also runs code
});
// Problem: If button updates editor, BOTH handlers fire!
// Code runs twice! 💥
The Solution: Check flags, validate state, handle errors gracefully:
editor.on('change', () => {
if (syncingFromBlocks) return; // ✓ Check flag
if (!editor.getValue().trim()) return; // ✓ Validate state
try {
runCode(); // ✓ Try to run
} catch (error) {
displayError(error); // ✓ Handle errors gracefully
}
});
Why This Matters: Events can fire in any order, or multiple times. You need to handle all cases safely. Always check flags, validate state, and catch errors!
Testing the Integration - Explained Simply
What This Section Shows:
This section provides a checklist to verify everything works correctly. It's like a test plan - you try each thing and make sure it works.
Why Testing Matters:
Testing helps you:
- Verify everything works - Make sure all features function correctly
- Find broken connections - Discover when components aren't connected properly
- Debug effectively - Know what should happen, so you can spot when it doesn't
- Document behavior - Understand how the system should behave
How to Use This Checklist:
Go through each item one by one:
- Try the action (e.g., type code)
- Verify the expected result (e.g., shapes appear)
- If it doesn't work, check the flow (something is disconnected)
The Test Checklist:
Test 1: Type Code → Shapes Appear
What to do:
- Type code in the editor:
shape circle c1 { radius: 50 } - Wait a moment (debounce delay)
Expected result:
- Circle appears on canvas
- Circle has radius 50
If it doesn't work:
- Check if
runCode()is being called - Check if lexer/parser/interpreter work
- Check if renderer receives shapes
- Check browser console for errors
Test 2: Move Block → Code Updates → Shapes Update
What to do:
- Switch to blocks mode
- Drag a block (e.g., change circle radius block from 50 to 60)
- Wait a moment
Expected result:
- Code in editor updates:
radius: 50→radius: 60 - Shapes update: circle radius changes from 50 to 60
If it doesn't work:
- Check if Blockly change listener is set up
- Check if
syncingFromBlocksflag is working - Check if code generation works
- Check if
runCode()is called after code update
Test 3: Drag Shape → Shape Moves → Slider Updates → Code Updates
What to do:
- Select a shape (click on it)
- Drag a corner handle (to resize)
- Wait a moment (debounce delay)
Expected result:
- Shape resizes immediately (visual feedback)
- Slider updates to match new size
- Code updates after delay (200ms)
If it doesn't work:
- Check if interaction handler detects drag
- Check if shape manager updates shape
- Check if renderer redraws
- Check if slider updates
- Check if code update is scheduled and executed
Test 4: Change Slider → Shape Updates → Code Updates
What to do:
- Open parameter panel
- Change a slider value (e.g., radius slider from 50 to 60)
Expected result:
- Shape updates immediately (radius changes)
- Code updates after delay (200ms)
If it doesn't work:
- Check if parameter manager detects slider change
- Check if shape manager receives update
- Check if renderer redraws
- Check if code update is scheduled
Test 5: Add Constraint → Shape Adjusts → Code Updates
What to do:
- Write code with constraints:
shape circle c1 { radius: 50 } shape circle c2 { radius: 30 } constraints { distance c1.center c2.center 100 }
Expected result:
- Shapes adjust to satisfy constraint (circles are 100 units apart)
- Code reflects constraints
If it doesn't work:
- Check if interpreter extracts constraints
- Check if constraint engine receives constraints
- Check if constraint engine solves correctly
- Check if shapes update after solving
Test 6: Export → File Downloads with Correct Shapes
What to do:
- Create some shapes (circles, rectangles, etc.)
- Click export button (SVG or DXF)
Expected result:
- File downloads
- File contains correct shapes
- Shapes are positioned correctly
If it doesn't work:
- Check if interpreter exists (
window.interpreter) - Check if export function is called
- Check if shapes are converted correctly
- Check if file is created and downloaded
If Any Test Fails:
If any of these tests fail, something is disconnected. Check:
- The flow - Follow the data path to find where it breaks
- Error messages - Check browser console for errors
- Component connections - Verify components are wired together
- Initialization order - Make sure components are initialized in correct order
- Flags and state - Check if sync flags are set correctly, state is consistent
The Big Picture - Explained Simply
What This Section Summarizes:
This section summarizes the entire system in a few key insights. It's the "takeaway" - the most important things to remember.
The Core Concept:
Otto is a reactive system. Everything is connected and responds to changes:
- Code changes → Shapes update automatically
- Shape changes → Code updates automatically
- Constraint changes → Shapes solve → Code updates automatically
- UI changes (sliders) → Shapes update → Code updates automatically
Real-World Analogy:
Think of it like a synchronized dance:
- When one dancer moves, others adjust
- Everyone stays in sync
- Changes ripple through the whole system
- Everything responds to everything else
The Hub: Shape Manager
The Shape Manager is the hub. It coordinates everything:
Code Editor
↓
Shape Manager ← → Renderer
↑ ↓
Parameter Manager Canvas
↑
Constraint Engine
Why Shape Manager is the Hub:
The shape manager:
- Receives updates from anywhere (code, blocks, canvas, sliders)
- Coordinates updates (makes sure everything stays in sync)
- Prevents chaos (ensures consistent state)
- Routes updates to the right places
Without the shape manager, you'd have chaos:
- Components wouldn't know about each other
- Updates wouldn't propagate correctly
- State would get out of sync
- Nothing would work together
The Key Insight:
Everything flows through Shape Manager.
Forward flow (code → shapes):
Code → Interpreter → Shape Manager → Renderer → Canvas
Reverse flow (canvas → code):
Canvas → Shape Manager → Code Editor
Side flows:
Slider → Shape Manager → Renderer + Code Editor
Constraints → Shape Manager → Renderer + Code Editor
Why This Architecture Works:
This architecture makes it possible to:
- Edit from anywhere - Code, blocks, canvas, sliders - all work!
- Stay in sync - Changes propagate everywhere automatically
- Avoid loops - Shape manager coordinates, prevents infinite loops
- Scale easily - Add new input methods (new way to edit) easily
The Trade-off:
It's complex, but it works. The complexity comes from:
- Multiple ways to edit (code, blocks, canvas, sliders)
- Bidirectional sync (changes flow both ways)
- Preventing loops (flags, debouncing, coordination)
But the complexity is worth it because:
- Users can edit however they want
- Everything stays in sync automatically
- System is flexible and extensible
Remember:
- Shape Manager is the hub - Everything flows through it
- Reactive system - Changes trigger updates automatically
- Bidirectional sync - Code ↔ Shapes (both directions work)
- Complex but works - The complexity enables the features
This is how Otto ties everything together!
Implementation Adjustments and Notes
This section documents important adjustments and lessons learned during actual implementation.
Initialization Order and Timing
Issue: The initial implementation tried to initialize everything synchronously, but components have dependencies that aren't immediately available:
- CodeMirror editor takes time to initialize after
fromTextArea()call - Blockly scripts load asynchronously from CDN
- Button event handlers execute immediately, but components may not exist yet
- Components need other components that initialize later
What Was Added: Staged initialization with specific delays for each component based on its dependencies.
Specific Implementation - Initialization Sequence:
document.addEventListener('DOMContentLoaded', () => {
// STEP 1: Get DOM elements (synchronous)
canvas = document.getElementById('canvas');
astOutput = document.getElementById('ast-output');
// ... other DOM elements ...
// STEP 2: Initialize core components (can be synchronous)
if (canvas) {
renderer = new Renderer(canvas);
window.renderer = renderer;
// Initialize debug visualizer immediately (depends only on renderer)
if (renderer.coordinateSystem) {
debugVisualizer = new DebugVisualizer(canvas.getContext('2d'), renderer.coordinateSystem);
debugVisualizer.setEnabled(debugEnabled);
window.debugVisualizer = debugVisualizer;
}
}
initializeCanvas();
initializeRulers();
initializeEditor(); // This initializes CodeMirror, but it may not be ready immediately
// STEP 3: Initialize shape manager (core coordinator, no dependencies)
shapeMgr = shapeManager;
shapeMgr.registerRenderer(renderer);
if (debugVisualizer) {
shapeMgr.setDebugVisualizer(debugVisualizer); // Register debug visualizer
}
// STEP 4: Initialize components that depend on editor (delay needed)
// Editor (CodeMirror) needs time to fully initialize after fromTextArea()
setTimeout(() => {
if (editor && renderer) {
// DragDropSystem needs editor to insert code
dragDropSystem = new DragDropSystem(renderer, editor, shapeMgr);
window.dragDropSystem = dragDropSystem; // Store globally
// InteractionHandler doesn't need editor, can be created here
interactionHandler = new InteractionHandler(renderer, shapeMgr);
}
}, 100); // 100ms delay ensures CodeMirror is ready
// STEP 5: Initialize Blockly (needs editor + Blockly scripts loaded from CDN)
setTimeout(() => {
// Check Blockly is loaded (CDN scripts load asynchronously)
if (editor && typeof Blockly !== 'undefined') {
blocklyIntegration = new BlocklyIntegration(editor, interpreter, shapeMgr);
window.blocklyIntegration = blocklyIntegration; // Store globally
} else {
console.warn('Blockly not available or editor not ready');
}
}, 500); // Longer delay: Blockly scripts + editor initialization
// STEP 6: Setup button handlers (after components are initialized)
setTimeout(() => {
setupButtonHandlers(); // Buttons need components to exist
}, 300); // After drag-and-drop, before Blockly completes
// STEP 7: Initialize constraint engine (depends on renderer and shapeMgr)
if (renderer && shapeMgr) {
constraintEngine = new ConstraintEngine(renderer, shapeMgr, (shapeName, paramName, value) => {
shapeMgr.updateShapeParameter(shapeName, paramName, value, 'constraint');
});
}
// STEP 8: Initial code execution (wait for everything)
setTimeout(() => {
if (editor && editor.getValue().trim()) {
runCode(); // Execute initial code after all components ready
}
}, 800); // Longest delay: ensures all async initialization completes
});
Why Specific Delays Were Chosen:
- 100ms for DragDropSystem: CodeMirror's
fromTextArea()returns immediately, but internal initialization takes ~50-100ms. This delay ensures the editor is fully functional. - 300ms for Button Handlers: Components need time to create and store globally. Button handlers check for components, so they need to run after component creation but before user interaction.
- 500ms for Blockly: Blockly scripts load from CDN asynchronously. Need to wait for both scripts to load AND editor to be ready. 500ms is conservative to handle slower connections.
- 800ms for Initial Code Run: This is the longest delay, ensuring all components are initialized, button handlers are set up, and the system is ready for code execution.
What Happened Without Proper Timing:
- Button clicks would fail because components were undefined
- Blockly initialization would fail with "Blockly is not defined" errors
- Drag-and-drop wouldn't work because editor wasn't ready
- Initial code execution might fail or run before components were ready
- Console errors about "Cannot read property X of undefined"
Global References for Cross-Component Access
Issue: Components are initialized asynchronously with different delays, but button handlers and other code need access to them immediately. Without global storage, components initialized later couldn't be accessed by code that executes earlier.
What Was Added: Global storage pattern using window object for all components that need cross-component access.
Specific Implementation:
// In app.js - Store components globally as they're created
// 1. Renderer (created early, used by many)
renderer = new Renderer(canvas);
window.renderer = renderer; // Added: Global access
// 2. Debug Visualizer (created early, used throughout)
debugVisualizer = new DebugVisualizer(canvas.getContext('2d'), renderer.coordinateSystem);
window.debugVisualizer = debugVisualizer; // Added: Global access
// 3. DragDropSystem (created async, accessed by button handlers)
setTimeout(() => {
if (editor && renderer) {
dragDropSystem = new DragDropSystem(renderer, editor, shapeMgr);
window.dragDropSystem = dragDropSystem; // Added: Global access
}
}, 100);
// 4. BlocklyIntegration (created async, accessed by button handlers)
setTimeout(() => {
if (editor && typeof Blockly !== 'undefined') {
blocklyIntegration = new BlocklyIntegration(editor, interpreter, shapeMgr);
window.blocklyIntegration = blocklyIntegration; // Added: Global access
}
}, 500);
// 5. Interpreter (created during runCode, accessed by exports and other code)
function runCode() {
// ... create interpreter ...
interpreter = new Interpreter();
window.interpreter = interpreter; // Added: Global access
// ... rest of code ...
}
// 6. runCode function (needed by HTML buttons and keyboard shortcuts)
window.runCode = runCode; // Added: Global access
Usage in Button Handlers:
Button handlers use global references because they execute before async initialization completes:
// Palette toggle button handler
const paletteToggleBtn = document.getElementById('palette-toggle');
if (paletteToggleBtn) {
paletteToggleBtn.addEventListener('click', () => {
// Use global reference because dragDropSystem may not be in local scope yet
if (window.dragDropSystem) {
window.dragDropSystem.toggle();
} else if (dragDropSystem) { // Fallback to local if available
dragDropSystem.toggle();
}
});
}
// Blocks toggle button handler
const blocksToggleBtn = document.getElementById('blocks-toggle-button');
if (blocksToggleBtn) {
blocksToggleBtn.addEventListener('click', () => {
// Try global first, then local
const integration = blocklyIntegration || window.blocklyIntegration;
if (integration) {
integration.toggleVisibility();
}
});
}
Usage in Export Functions:
Export functions also use global references:
// In svgExport.mjs or dxfExport.mjs
export function exportToSVG(interpreter, canvas, filename) {
// interpreter parameter, but can also use window.interpreter as fallback
const interp = interpreter || window.interpreter;
const canv = canvas || window.canvas;
// ... export code ...
}
// Called from button handler
const exportBtn = document.getElementById('export-button');
if (exportBtn) {
exportBtn.addEventListener('click', () => {
// Use global references because interpreter is created during runCode
if (!window.interpreter || !window.canvas) {
alert('No shapes to export');
return;
}
exportToSVG(window.interpreter, window.canvas, 'otto_drawing.svg');
});
}
Why This Pattern Was Necessary:
- Async Initialization: Components initialize at different times. Global storage ensures they're accessible regardless of initialization timing.
- Button Handlers: Event listeners execute immediately, but components may initialize later. Global references bridge this timing gap.
- Cross-Module Access: Export functions and other modules need access to components created in app.js. Global storage provides this access without tight coupling.
- Fallback Pattern: Code checks both local and global references, providing flexibility for different execution contexts.
Alternative Approaches Considered:
- Dependency Injection: Would require threading dependencies through many layers, making code verbose
- Event System: More complex, harder to debug, adds latency
- Service Locator Pattern: Similar to globals but with more structure (could be future improvement)
What Happened Without Global Storage:
- Button handlers would fail with "dragDropSystem is not defined" errors
- Export functions couldn't access interpreter
- Components initialized in different scopes couldn't communicate
- Code would have to pass dependencies through many function calls
Debug Visualizer Integration
Issue: Debug visualizer needs to be accessible throughout the rendering pipeline.
Solution: Register debug visualizer with shape manager and pass it through renderer calls:
// Initialize debug visualizer early
debugVisualizer = new DebugVisualizer(canvas.getContext('2d'), renderer.coordinateSystem);
window.debugVisualizer = debugVisualizer;
// Register with shape manager
shapeMgr.setDebugVisualizer(debugVisualizer);
// Pass to renderer calls
renderer.redraw(debugVisualizer);
renderer.setHoveredShape(shapeName, debugVisualizer);
renderer.setSelectedShape(shapeName, debugVisualizer);
Why: Debug visualizer needs to draw overlays during redraws. By passing it through the rendering pipeline, we ensure debug information appears correctly without coupling components too tightly.
Component Registration Timing
Issue: The shape manager needs references to interpreter and editor to coordinate updates, but these are created at different times:
- Editor is created during
initializeEditor()(CodeMirror initialization) - Interpreter is created fresh each time
runCode()is called - Shape manager needs both to coordinate code ↔ shape synchronization
What Was Added: Registration calls immediately after component creation, ensuring shape manager always has current references.
Specific Implementation:
function runCode() {
try {
if (!editor || !renderer) return;
const code = editor.getValue();
if (!code.trim()) {
renderer.clear();
return;
}
// ... lex and parse ...
const lexer = new Lexer(code);
const parser = new Parser(lexer);
const ast = parser.parse();
// ... store AST ...
currentAST = ast;
// Create interpreter (NEW instance each time code runs)
interpreter = new Interpreter();
const result = interpreter.interpret(ast);
// Store interpreter globally
window.interpreter = interpreter;
// NEW: Register interpreter and editor with shape manager immediately after creation
// This ensures shape manager has current references for coordination
if (shapeMgr) {
shapeMgr.registerInterpreter(interpreter); // Register current interpreter
shapeMgr.registerEditor(editor); // Register editor (may have changed)
}
// Update renderer
if (renderer) {
renderer.setShapes(result.shapes, debugVisualizer);
}
// Update shape manager with new shapes
if (shapeMgr) {
shapeMgr.updateFromCode({ shapes: result.shapes });
}
// Update parameter manager
if (parameterManager) {
setTimeout(() => {
parameterManager.updateWithLatestInterpreter(interpreter);
}, 50);
}
// Rebuild constraint anchors
if (constraintEngine) {
constraintEngine.rebuild();
}
} catch (error) {
// ... error handling ...
}
}
Why Registration Happens in runCode():
- Interpreter is Recreated: Each code execution creates a new interpreter instance. Shape manager needs the latest one.
- Editor May Change: If editor is reinitialized, shape manager needs the new reference.
- Timing: Registration happens immediately after creation, before any updates are processed, ensuring coordination works correctly.
What Happened Without Proper Registration:
- Shape manager would have stale interpreter reference (from previous code execution)
- Updates from sliders/canvas wouldn't work correctly
- Code synchronization would fail
- Shape manager wouldn't know about new shapes after code execution
Error Handling and Null Checks
Issue: Components may not be initialized when accessed, causing errors.
Solution: Always check for component existence before use:
// Always check before using
if (renderer && renderer.redraw) {
renderer.redraw(debugVisualizer || window.debugVisualizer || null);
}
// Provide fallbacks
const integration = blocklyIntegration || window.blocklyIntegration;
if (integration) {
integration.toggleVisibility();
} else {
// Initialize if not ready
if (editor && typeof Blockly !== 'undefined') {
blocklyIntegration = new BlocklyIntegration(editor, interpreter, shapeMgr);
}
}
Why: With asynchronous initialization, components may not exist when code tries to use them. Null checks prevent crashes and allow graceful degradation or delayed initialization.
Shape Rendering Edge Cases
Issue: The initial rendering implementation assumed all shapes would have standard properties and methods, but in practice:
- Boolean operations consume source shapes (marked with
_consumedByBoolean) - Not all shapes implement
getPoints()method - Some shapes may have missing or null transform properties
- Edge counting would fail if
getPoints()doesn't exist or throws errors
What Was Added: Defensive programming with multiple checks and error handling.
Specific Implementation - Shape Rendering Loop:
// In renderer.mjs redraw() method
redraw(debugVisualizer = null) {
// ... clear canvas, draw background, draw grid ...
// Draw shapes with defensive checks
if (this.shapes) {
let shapeCount = 0;
let edgeCount = 0;
for (const [name, shape] of this.shapes) {
// CHECK 1: Skip boolean-consumed shapes
// Shapes that are inputs to boolean operations are marked and shouldn't be drawn
if (shape._consumedByBoolean) continue;
const isSelected = this.selectedShape === name;
const isHovered = this.hoveredShape === name;
// Draw shape (ShapeRenderer handles its own defensive checks)
this.shapeRenderer.drawShape(shape, isSelected, isHovered);
shapeCount++;
// CHECK 2: Safe edge counting with multiple validations
// Not all shapes support getPoints() - need to check before calling
if (shape.getPoints && typeof shape.getPoints === 'function') {
try {
const points = shape.getPoints();
// CHECK 3: Verify result is actually an array
if (Array.isArray(points)) {
edgeCount += points.length;
}
} catch (e) {
// Shape.getPoints() exists but throws error (shouldn't happen, but handle gracefully)
// Skip edge counting for this shape
console.warn(`Shape ${name} getPoints() failed:`, e);
}
}
// If shape doesn't have getPoints, just skip edge counting (not an error)
}
// Update debug visualizer counts (only if debug visualizer provided)
if (debugVisualizer) {
debugVisualizer.updateCounts(shapeCount, edgeCount);
}
}
// Draw debug overlay (only if enabled and visualizer provided)
if (debugVisualizer && this.shapes) {
debugVisualizer.drawOverlay(this.shapes, this.coordinateSystem);
}
}
Specific Implementation - Shape Renderer Defensive Checks:
// In renderer/shapeRenderer.mjs drawShape() method
drawShape(shape, isSelected = false, isHovered = false) {
// CHECK 1: Validate shape exists and isn't consumed
if (!shape || shape._consumedByBoolean) {
return; // Early exit - nothing to draw
}
this.ctx.save();
// CHECK 2: Safe property access with defaults
// Shape.params may not exist, provide defaults
const params = shape.params || {};
const fill = params.fill !== false; // Default to true
const stroke = params.stroke !== false; // Default to true
const color = params.color || params.fillColor || '#000000'; // Default color
const strokeColor = params.strokeColor || '#000000';
const strokeWidth = params.strokeWidth || 1;
// CHECK 3: Safe transform access with defaults
const transform = shape.transform || {
position: [0, 0],
rotation: 0,
scale: [1, 1]
};
const [posX, posY] = transform.position || [0, 0];
const rotation = transform.rotation || 0;
const [scaleX, scaleY] = transform.scale || [1, 1];
// ... rest of drawing code ...
}
Why These Checks Were Necessary:
- Boolean Operations: When shapes are used in boolean operations, source shapes are marked as consumed and shouldn't be drawn separately. Without this check, they would be drawn on top of the boolean result.
- Optional Methods: Not all shape types implement
getPoints(). Some shapes are simple (circle) and don't need edge representation. The check prevents "getPoints is not a function" errors. - Error Handling: Even if
getPoints()exists, it might throw an error (e.g., if shape is in invalid state). Try-catch prevents one bad shape from crashing the entire rendering. - Null Safety: Transform and params objects may not exist. Default values ensure rendering continues even with incomplete shape data.
What Happened Without These Checks:
- Rendering would crash with "Cannot read property 'getPoints' of undefined" errors
- Boolean-consumed shapes would render incorrectly (appearing twice)
- Invalid shapes would crash the entire render loop
- Edge counting would fail and debug visualizer would show wrong numbers