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.html → app.js → lexer/parser/interpreter → renderer → canvas
index.html loads:
- External libraries (Blockly, CodeMirror, ClipperLib) from CDN
src/app.jsas an ES modulesrc/blocks-umd.jsas a regular script (needs to be UMD, not ES module)
app.js does:
- Sets up CodeMirror editor
- Sets up Blockly workspace
- Creates renderer with canvas
- Sets up event handlers
- 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 stateBooleanOperators.mjs- Boolean operationsturtleDrawer.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:
- The lexer/parser already handle
shape <type>generically - no changes needed - Add shape creation in
interpreter.mjsorShapes.mjs - Add rendering in
renderer/shapeRenderer.mjs - Add Blockly block in
blocks-umd.js(if you want visual blocks)
Adding a new keyword:
- Add to lexer keywords object
- Add parser method to handle it
- Add interpreter case to evaluate it
Modifying the UI:
app.jshas all the UI setupindex.htmlhas the HTML structuresrc/styles.csshas the styles
Changing how shapes render:
renderer.mjscoordinates everythingrenderer/shapeRenderer.mjshas the actual drawing coderenderer/styleManager.mjshandles 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:
- Is the AST correct? (parser issue)
- Does the interpreter create shapes? (interpreter issue)
- 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
syncingFromBlocksflag - 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 logicrenderer/files - rendering logicconstraints/files - constraint system- Adding new shapes/features
Be careful with:
lexer.mjs- Changes affect all parsingparser.mjs- Grammar changes can break existing codeinterpreter.mjs- Core execution logicblocks-umd.js- Blockly integration is finicky
Test your changes:
- Write some test code
- Check the AST (use the AST viewer)
- Check the console for errors
- Verify shapes appear correctly
- Test both text and blocks modes
Getting Started
If you're new to the codebase:
- Start with
app.js- This is where everything connects. Understand the flow. - Look at
runCode()- This is the main execution path. - Trace through an example - Write simple code like
shape circle c1 { radius: 50 }and follow it through lexer → parser → interpreter → renderer. - Check the AST - Use the AST viewer to see what the parser produces.
- 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)
- Create
src/lexer.mjs - Implement
Tokenclass - Implement
Lexerclass with:constructor(input)- Initialize with source codegetNextToken()- Main tokenization loopadvance()- Move to next characterskipWhitespace()- Skip spaces/tabs/newlinesidentifier()- Read identifiers and keywordsnumber()- 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)
- Create
src/parser.mjs - Implement
Parserclass with:constructor(lexer)- Initialize with lexerparse()- Main parse method (returns AST)parseStatement()- Parse individual statementsparseShape()- Parse shape definitionsparseExpression()- Parse expressions with precedenceeat(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)
- Create
src/environment.mjs - Implement
Environmentclass with:parameters- Map of parameter name → valueshapes- Map of shape name → shape objectsetParameter(name, value)- Store a parametergetParameter(name)- Retrieve a parametercreateShapeWithName(type, name, params)- Create and store a shape
Step 2.4: Build the Interpreter (See Chapter 02 for details)
- Create
src/interpreter.mjs - Implement
Interpreterclass with:constructor()- Create new environmentinterpret(ast)- Main interpretation loopevaluateNode(node)- Dispatch to specific evaluatorsevaluateShape(node)- Create shape objectsevaluateExpression(node)- Evaluate expressionsevaluateParam(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)
- Create
src/renderer.mjs - Implement basic
Rendererclass:constructor(canvas)- Get canvas and contextsetShapes(shapes)- Store shapesredraw()- Draw all shapesclear()- Clear canvas
Step 3.2: Add Coordinate System (See Chapter 04 for details)
- Create
src/renderer/coordinateSystem.mjs - 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)
- Create
src/renderer/shapeRenderer.mjs - Implement drawing for each shape type:
- Circles:
ctx.arc() - Rectangles:
ctx.rect() - Polygons:
ctx.beginPath()+ctx.lineTo() - Paths:
ctx.moveTo()+ctx.lineTo()
- Circles:
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
- Create
src/app.js Import all modules:
import { Lexer } from './lexer.mjs'; import { Parser } from './parser.mjs'; import { Interpreter } from './interpreter.mjs'; import { Renderer } from './renderer.mjs';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(); }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.htmlin 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)
- Create
src/shapeManager.mjs 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)
- Create
src/renderer/interactionHandler.mjs - Implement:
handleMouseDown()- Detect clicks on shapes/handleshandleMouseMove()- Handle dragginghandleMouseUp()- End drag operation
Step 7.2: Add Handles (See Chapter 05 for details)
- Create
src/renderer/handleSystem.mjs - Draw handles on selected shapes
- Detect handle clicks
- Calculate new parameter values when dragging
Phase 8: Add Advanced Features
Step 8.1: Add Parameter UI (See Chapter 13 for details)
- Create
src/2Dparameters.mjs - Create sliders for each shape parameter
- Connect sliders to shape manager
Step 8.2: Add Constraints (See Chapter 09 for details)
- Create
src/constraints/engine.mjs - Implement constraint solver
- Add constraint UI
Step 8.3: Add Export (See Chapter 11 for details)
- Create
src/svgExport.mjs - Convert shapes to SVG format
- Add export button
Testing Each Phase
After each phase, test that:
- Phase 2 (Language): Can parse and interpret simple code
- Phase 3 (Renderer): Can draw shapes on canvas
- Phase 4 (Integration): Code → Shapes appears on screen
- Phase 5 (Editor): Typing code updates shapes
- Phase 6 (Manager): Shape changes update code
- Phase 7 (Interaction): Can drag shapes
- 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:
- Add more shape types
- Add more language features (loops, functions)
- Add Blockly integration (Chapter 08)
- Add more export formats
- 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.