Building From Scratch

If you're starting fresh or need to understand how everything fits together, here's what you need to know.

Project Structure

The codebase is organized like this:

src/
├── lexer.mjs              # Tokenizes source code
├── parser.mjs             # Builds AST from tokens
├── interpreter.mjs        # Executes AST, creates shapes
├── environment.mjs        # Runtime state (parameters, shapes)
├── app.js                 # Main application (orchestrates everything)
├── renderer.mjs           # Draws shapes to canvas
├── shapeManager.mjs       # Manages shape updates
├── blocks-umd.js          # Blockly block definitions (UMD format)
│
├── renderer/              # Rendering subsystems
│   ├── shapeRenderer.mjs
│   ├── booleanRenderer.mjs
│   └── ...
│
├── constraints/           # Constraint system
│   ├── engine.mjs
│   └── constraints.mjs
│
└── math/                  # Math utilities
    ├── solveSystem.mjs    # Constraint solver
    └── ...

How It All Connects

The flow is: index.htmlapp.js → lexer/parser/interpreter → renderer → canvas

index.html loads:

  • External libraries (Blockly, CodeMirror, ClipperLib) from CDN
  • src/app.js as an ES module
  • src/blocks-umd.js as a regular script (needs to be UMD, not ES module)

app.js does:

  1. Sets up CodeMirror editor
  2. Sets up Blockly workspace
  3. Creates renderer with canvas
  4. Sets up event handlers
  5. When code changes, runs: lexer → parser → interpreter → renderer

The execution pipeline:

// In app.js, runCode() function
const code = editor.getValue();
const lexer = new Lexer(code);
const parser = new Parser(lexer);
const ast = parser.parse();
const interpreter = new Interpreter();
const result = interpreter.interpret(ast);
renderer.setShapes(result.shapes);
renderer.redraw();

Why this order matters: Each step transforms data into the next stage. The lexer creates tokens from raw text, the parser builds a tree structure from tokens, the interpreter executes that tree to create runtime objects, and the renderer draws those objects. You can't skip steps - the parser needs tokens, the interpreter needs an AST, and the renderer needs shape objects. If any step fails, nothing appears on screen. Also note: we create a new interpreter instance each time - this resets all state (parameters, shapes, etc.) so old code doesn't affect new code.

Setting Up the Project

There's no build step. Just serve the files with a web server:

  • Install "Live Server" extension
  • Right-click index.html → "Open with Live Server"

Why a web server? ES modules don't work with file:// URLs due to CORS. You need HTTP.

Dependencies

All dependencies are loaded from CDN in index.html:

  • Blockly - Visual programming blocks
  • CodeMirror - Text editor
  • ClipperLib - Boolean operations (Vatti algorithm)
  • Three.js - 3D rendering (unused in 2D mode)
  • Marked - Markdown rendering

No npm, no package.json dependencies, no build tools. Just CDN links.

File-by-File Breakdown

lexer.mjs - Standalone, no dependencies. Takes a string, returns tokens.

parser.mjs - Depends on lexer.mjs (imports Token class). Takes a lexer, returns AST.

interpreter.mjs - Depends on:

  • environment.mjs - Runtime state
  • BooleanOperators.mjs - Boolean operations
  • turtleDrawer.mjs - Turtle graphics

Takes an AST, returns shapes/parameters/etc.

app.js - The main orchestrator. Imports everything and wires it together. This is where you'll spend most of your time if you're modifying the UI or adding features.

renderer.mjs - Depends on all the renderer subsystems. Takes shapes, draws them to canvas.

blocks-umd.js - This is special. It's UMD format (not ES module) because it's loaded via <script> tag and needs to work with Blockly (which is also loaded via script tag). If you modify this, make sure it stays UMD - no import/export statements.

How to Modify Things

Adding a new shape type:

  1. The lexer/parser already handle shape <type> generically - no changes needed
  2. Add shape creation in interpreter.mjs or Shapes.mjs
  3. Add rendering in renderer/shapeRenderer.mjs
  4. Add Blockly block in blocks-umd.js (if you want visual blocks)

Adding a new keyword:

  1. Add to lexer keywords object
  2. Add parser method to handle it
  3. Add interpreter case to evaluate it

Modifying the UI:

  • app.js has all the UI setup
  • index.html has the HTML structure
  • src/styles.css has the styles

Changing how shapes render:

  • renderer.mjs coordinates everything
  • renderer/shapeRenderer.mjs has the actual drawing code
  • renderer/styleManager.mjs handles colors/fills/strokes

Debugging Tips

Check the AST: In app.js, there's an AST viewer. The AST shows exactly what the parser understood. If shapes aren't appearing, check:

  1. Is the AST correct? (parser issue)
  2. Does the interpreter create shapes? (interpreter issue)
  3. Does the renderer draw them? (renderer issue)

Console logging: Add console.log() in each phase:

  • Lexer: log tokens
  • Parser: log AST nodes
  • Interpreter: log shape objects
  • Renderer: log what it's trying to draw

Common issues:

  • Shapes not appearing: Check renderer.shapes - are they there?
  • Parameters not working: Check interpreter.env.parameters - is the value stored?
  • Boolean operations broken: Check if original shapes are marked _consumedByBoolean
  • Blocks not syncing: Check syncingFromBlocks flag - might be stuck

The blocks-umd.js File

This file is manually written (not generated). It defines Blockly blocks using Blockly.defineBlocksWithJsonArray().

Why UMD? Because it's loaded via <script> tag, not ES module. Blockly is also loaded via script tag, so they need to work together in the global scope.

If you modify it:

  • Keep it as UMD (no imports/exports)
  • Make sure blocks are defined before Blockly workspace is created
  • Each block needs a code generator in Blockly.JavaScript

Module System

Everything uses ES6 modules (import/export) except blocks-umd.js. The browser loads them directly - no bundler needed.

Import chain example:

app.js
  → imports lexer.mjs
  → imports parser.mjs (which imports lexer.mjs for Token)
  → imports interpreter.mjs (which imports environment.mjs, etc.)
  → imports renderer.mjs (which imports all renderer subsystems)

The browser resolves these imports automatically. Just make sure paths are correct.

Where Things Live

Language code: lexer.mjs, parser.mjs, interpreter.mjs, environment.mjs UI code: app.js, index.html Rendering code: renderer.mjs and everything in renderer/ Shape definitions: Shapes.mjs (if it exists) or in interpreter Blockly integration: blocks-umd.js and parts of app.js Constraints: constraints/ directory Math utilities: math/ directory

Making Changes

Safe to modify:

  • app.js - UI logic
  • renderer/ files - rendering logic
  • constraints/ files - constraint system
  • Adding new shapes/features

Be careful with:

  • lexer.mjs - Changes affect all parsing
  • parser.mjs - Grammar changes can break existing code
  • interpreter.mjs - Core execution logic
  • blocks-umd.js - Blockly integration is finicky

Test your changes:

  1. Write some test code
  2. Check the AST (use the AST viewer)
  3. Check the console for errors
  4. Verify shapes appear correctly
  5. Test both text and blocks modes

Getting Started

If you're new to the codebase:

  1. Start with app.js - This is where everything connects. Understand the flow.
  2. Look at runCode() - This is the main execution path.
  3. Trace through an example - Write simple code like shape circle c1 { radius: 50 } and follow it through lexer → parser → interpreter → renderer.
  4. Check the AST - Use the AST viewer to see what the parser produces.
  5. Modify something small - Change a shape color, add a console.log, see what happens.

The code is pretty straightforward - no magic, just straightforward JavaScript. If you understand the three-phase architecture (lexer/parser/interpreter), you can work with any part of it.

How to Build the Complete System - Step by Step

This section gives you a complete roadmap for building Otto from scratch. Follow these steps in order.

Phase 1: Setup the Foundation

Step 1.1: Create the HTML Structure

Create index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Otto</title>
  <style>
    body { margin: 0; padding: 0; }
    #canvas { border: 1px solid #ccc; }
  </style>
</head>
<body>
  <canvas id="canvas" width="800" height="600"></canvas>
  <script type="module" src="./src/app.js"></script>
</body>
</html>

Step 1.2: Create the Directory Structure

your-project/
├── index.html
└── src/
    ├── app.js
    ├── lexer.mjs
    ├── parser.mjs
    ├── interpreter.mjs
    ├── environment.mjs
    └── renderer.mjs

Step 1.3: Set Up a Local Server

You need an HTTP server (not file://). Options:

  • VS Code: Install "Live Server" extension, right-click index.html → "Open with Live Server"
  • Python: python3 -m http.server 8000
  • Node.js: npx http-server

Phase 2: Build the Language System

Step 2.1: Build the Lexer (See Chapter 02 for details)

  1. Create src/lexer.mjs
  2. Implement Token class
  3. Implement Lexer class with:
    • constructor(input) - Initialize with source code
    • getNextToken() - Main tokenization loop
    • advance() - Move to next character
    • skipWhitespace() - Skip spaces/tabs/newlines
    • identifier() - Read identifiers and keywords
    • number() - Read numbers (integers and decimals)
    • parseString() - Read string literals

Test the lexer:

import { Lexer } from './lexer.mjs';
const lexer = new Lexer('shape circle c1 { radius: 50 }');
let token = lexer.getNextToken();
while (token.type !== 'EOF') {
  console.log(token);
  token = lexer.getNextToken();
}

Step 2.2: Build the Parser (See Chapter 02 for details)

  1. Create src/parser.mjs
  2. Implement Parser class with:
    • constructor(lexer) - Initialize with lexer
    • parse() - Main parse method (returns AST)
    • parseStatement() - Parse individual statements
    • parseShape() - Parse shape definitions
    • parseExpression() - Parse expressions with precedence
    • eat(tokenType) - Consume expected tokens

Test the parser:

import { Lexer } from './lexer.mjs';
import { Parser } from './parser.mjs';
const code = 'shape circle c1 { radius: 50 }';
const lexer = new Lexer(code);
const parser = new Parser(lexer);
const ast = parser.parse();
console.log(JSON.stringify(ast, null, 2));

Step 2.3: Build the Environment (See Chapter 02 for details)

  1. Create src/environment.mjs
  2. Implement Environment class with:
    • parameters - Map of parameter name → value
    • shapes - Map of shape name → shape object
    • setParameter(name, value) - Store a parameter
    • getParameter(name) - Retrieve a parameter
    • createShapeWithName(type, name, params) - Create and store a shape

Step 2.4: Build the Interpreter (See Chapter 02 for details)

  1. Create src/interpreter.mjs
  2. Implement Interpreter class with:
    • constructor() - Create new environment
    • interpret(ast) - Main interpretation loop
    • evaluateNode(node) - Dispatch to specific evaluators
    • evaluateShape(node) - Create shape objects
    • evaluateExpression(node) - Evaluate expressions
    • evaluateParam(node) - Store parameters

Test the interpreter:

import { Lexer } from './lexer.mjs';
import { Parser } from './parser.mjs';
import { Interpreter } from './interpreter.mjs';
const code = 'param size 100\nshape circle c1 { radius: size }';
const lexer = new Lexer(code);
const parser = new Parser(lexer);
const ast = parser.parse();
const interpreter = new Interpreter();
const result = interpreter.interpret(ast);
console.log('Shapes:', Array.from(result.shapes.entries()));

Phase 3: Build the Renderer

Step 3.1: Create Basic Renderer (See Chapter 04 for details)

  1. Create src/renderer.mjs
  2. Implement basic Renderer class:
    • constructor(canvas) - Get canvas and context
    • setShapes(shapes) - Store shapes
    • redraw() - Draw all shapes
    • clear() - Clear canvas

Step 3.2: Add Coordinate System (See Chapter 04 for details)

  1. Create src/renderer/coordinateSystem.mjs
  2. Implement coordinate conversion:
    • World coordinates (mm) ↔ Screen coordinates (pixels)
    • Pan and zoom support
    • Grid and rulers

Step 3.3: Add Shape Rendering (See Chapter 04 for details)

  1. Create src/renderer/shapeRenderer.mjs
  2. Implement drawing for each shape type:
    • Circles: ctx.arc()
    • Rectangles: ctx.rect()
    • Polygons: ctx.beginPath() + ctx.lineTo()
    • Paths: ctx.moveTo() + ctx.lineTo()

Test the renderer:

import { Renderer } from './renderer.mjs';
const canvas = document.getElementById('canvas');
const renderer = new Renderer(canvas);
// After interpreter runs:
renderer.setShapes(result.shapes);
renderer.redraw();

Phase 4: Wire Everything Together

Step 4.1: Create app.js

  1. Create src/app.js
  2. Import all modules:

    import { Lexer } from './lexer.mjs';
    import { Parser } from './parser.mjs';
    import { Interpreter } from './interpreter.mjs';
    import { Renderer } from './renderer.mjs';
    
  3. Create runCode() function:

    function runCode() {
    const code = editor.getValue(); // You'll add editor later
    const lexer = new Lexer(code);
    const parser = new Parser(lexer);
    const ast = parser.parse();
    const interpreter = new Interpreter();
    const result = interpreter.interpret(ast);
    renderer.setShapes(result.shapes);
    renderer.redraw();
    }
    
  4. Call runCode() on page load:

    document.addEventListener('DOMContentLoaded', () => {
    const canvas = document.getElementById('canvas');
    renderer = new Renderer(canvas);
    // For now, test with hardcoded code:
    const testCode = 'shape circle c1 { radius: 50, x: 0, y: 0 }';
    // ... run through pipeline
    });
    

Test the complete pipeline:

  • Open index.html in browser
  • You should see a circle on the canvas

Phase 5: Add the Text Editor

Step 5.1: Add CodeMirror

In index.html, add CodeMirror:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.js"></script>

Step 5.2: Add Textarea to HTML

<textarea id="code-editor"></textarea>

Step 5.3: Initialize CodeMirror in app.js

import CodeMirror from 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.js';

let editor;
document.addEventListener('DOMContentLoaded', () => {
  const textarea = document.getElementById('code-editor');
  editor = CodeMirror.fromTextArea(textarea, {
    lineNumbers: true,
    mode: 'javascript', // You'll create custom mode later
  });

  editor.on('change', () => {
    clearTimeout(timeout);
    timeout = setTimeout(runCode, 300);
  });
});

Test: Type code, shapes should appear after 300ms delay.

Phase 6: Add Shape Manager

Step 6.1: Create Shape Manager (See Chapter 06 for details)

  1. Create src/shapeManager.mjs
  2. Implement singleton pattern: ```javascript class ShapeManager { constructor() { this.shapes = new Map(); this.renderer = null; this.editor = null; }

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

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

    updateShapeParameter(shapeName, paramName, value, source) { // Update shape object // Update renderer // Update code (if needed) } }

export const shapeManager = new ShapeManager();


**Step 6.2: Wire Shape Manager into app.js**

```javascript
import { shapeManager } from './shapeManager.mjs';

// In initializeComponents():
shapeManager.registerRenderer(renderer);
shapeManager.registerEditor(editor);

Phase 7: Add Interaction

Step 7.1: Add Mouse Event Handlers (See Chapter 13 for details)

  1. Create src/renderer/interactionHandler.mjs
  2. Implement:
    • handleMouseDown() - Detect clicks on shapes/handles
    • handleMouseMove() - Handle dragging
    • handleMouseUp() - End drag operation

Step 7.2: Add Handles (See Chapter 05 for details)

  1. Create src/renderer/handleSystem.mjs
  2. Draw handles on selected shapes
  3. Detect handle clicks
  4. Calculate new parameter values when dragging

Phase 8: Add Advanced Features

Step 8.1: Add Parameter UI (See Chapter 13 for details)

  1. Create src/2Dparameters.mjs
  2. Create sliders for each shape parameter
  3. Connect sliders to shape manager

Step 8.2: Add Constraints (See Chapter 09 for details)

  1. Create src/constraints/engine.mjs
  2. Implement constraint solver
  3. Add constraint UI

Step 8.3: Add Export (See Chapter 11 for details)

  1. Create src/svgExport.mjs
  2. Convert shapes to SVG format
  3. Add export button

Testing Each Phase

After each phase, test that:

  1. Phase 2 (Language): Can parse and interpret simple code
  2. Phase 3 (Renderer): Can draw shapes on canvas
  3. Phase 4 (Integration): Code → Shapes appears on screen
  4. Phase 5 (Editor): Typing code updates shapes
  5. Phase 6 (Manager): Shape changes update code
  6. Phase 7 (Interaction): Can drag shapes
  7. Phase 8 (Advanced): All features work together

Common Build Issues

Issue: "Cannot find module"

  • Check file paths are correct
  • Make sure you're using HTTP server (not file://)
  • Check file extensions (.mjs)

Issue: "Shapes don't appear"

  • Check console for errors
  • Verify interpreter creates shapes: console.log(result.shapes)
  • Verify renderer receives shapes: console.log(renderer.shapes)
  • Check canvas size is set correctly

Issue: "Code doesn't run"

  • Check editor is initialized
  • Check runCode() is being called
  • Check for parse errors in console

Issue: "Updates don't sync"

  • Check shape manager is registered
  • Check sync flags aren't stuck
  • Check event listeners are attached

Next Steps

Once you have the basic system working:

  1. Add more shape types
  2. Add more language features (loops, functions)
  3. Add Blockly integration (Chapter 08)
  4. Add more export formats
  5. Polish the UI

Each chapter has detailed instructions for building that specific component. Use this roadmap to understand the big picture, then dive into specific chapters for implementation details.

results matching ""

    No results matching ""