Building UI Systems From Scratch - Complete Beginner's Guide
What is This Chapter About?
This chapter covers building user interface systems that make your design tool interactive and user-friendly. The main focus is on drag-and-drop functionality, but the principles apply to all UI interactions.
Real-World Analogy:
Think of UI systems like the controls on a car:
- Backend = Engine (powerful, does the work)
- UI Systems = Steering wheel, pedals, buttons (user-friendly interface)
- Users interact with simple controls, and the system does complex work behind the scenes
Why UI Systems Matter - Explained Simply
The Problem:
Users want intuitive ways to interact with your system:
- Drag shapes from a palette onto the canvas - Easy way to add shapes
- See visual feedback - Know what's happening (hover, select, drag)
- Get immediate updates - See changes right away
- Have controls that are easy to understand - No confusing interfaces
The Solution:
Good UI systems bridge the gap between your powerful backend (the complex code) and user-friendly frontend (what users see and interact with).
Real-World Example:
Without good UI:
- Users have to type code manually:
shape circle c1 { radius: 50 } - Hard to learn, easy to make mistakes
- No visual feedback
With good UI:
- Users drag a circle icon from a palette
- Drop it on the canvas
- Shape appears instantly
- Easy, intuitive, visual
The Drag-and-Drop Challenge - Explained Simply
What is Drag-and-Drop? - Much More Detailed Explanation
Drag-and-drop is when you click on something, hold the mouse button down, move the mouse, and release the button. Let's break this down completely:
Simple Definition: Drag-and-drop is a way of moving things on a computer screen by clicking, holding, moving, and releasing the mouse button.
The Four Steps - Explained Simply:
Step 1: Click (Mouse Down)
- You press the mouse button down
- This "picks up" the item
- Like grabbing something with your hand
Step 2: Hold (Mouse Button Down)
- You keep the mouse button pressed down
- You're still "holding" the item
- Like keeping your grip on something you're carrying
Step 3: Move (Mouse Move)
- While holding the button, you move the mouse
- The item follows your mouse cursor
- Like carrying something to a new location
Step 4: Release (Mouse Up)
- You let go of the mouse button
- The item "drops" at the new location
- Like putting something down where you want it
Real-World Analogy - Much More Detailed:
Think of it like moving furniture in your house:
Step 1: Pick it up (Click and Hold)
- You grab the furniture (click mouse button)
- You lift it up (button stays down)
- Like: Click on a chair → grab it
Step 2: Move it (Drag Mouse)
- You walk to where you want it (move mouse)
- The furniture moves with you (item follows cursor)
- Like: Walk to new spot → chair moves with you
Step 3: Put it down (Release)
- You set it down at the new location (release mouse button)
- The furniture stays there (item is placed)
- Like: Release the chair → it stays in the new spot
Visual Example:
Start:
┌─────┐
│ Box │ ← Mouse here
└─────┘
Step 1 (Click):
[Mouse down on box]
Box is "picked up"
Step 2 (Hold and Move):
┌─────┐
│ Box │ ← Moving...
╰─── Mouse cursor here (box follows)
Step 3 (Release):
┌─────┐
│ Box │ ← Dropped here!
└─────┘
Why Drag-and-Drop is Important:
Without Drag-and-Drop:
- Users have to type commands: "move box from (100, 100) to (200, 200)"
- Hard to do, requires knowing exact coordinates
- Not intuitive
With Drag-and-Drop:
- Users just click, drag, and drop
- Easy, visual, intuitive
- Like moving things in real life!
The Key Insight:
Drag-and-drop makes computers work like the real world - you grab things and move them, just like you would with physical objects!
Real-World Analogy:
Think of it like moving furniture:
- Pick it up (click and hold) - Drag start
- Move it (drag mouse) - Drag handling
- Put it down (release mouse) - Drop handling
The Challenge:
When users drag shapes from a palette onto the canvas, the system needs to:
- Create new shapes in code - Generate the code for the new shape
- Position them correctly - Calculate where on canvas the shape should be
- Update the editor - Add the code to the code editor
- Trigger rendering - Draw the shape on the canvas
- Provide visual feedback - Show preview while dragging, highlight drop zone
Why This is Complex:
This requires coordinating between multiple systems:
- DOM events - Mouse clicks, movements, releases
- Canvas coordinates - Converting mouse position to canvas position
- Code generation - Creating code from drag operation
- Shape manager - Updating the shape system
- Renderer - Drawing the shape
All of these need to work together seamlessly!
Visual Feedback - Explained Simply
What is Visual Feedback?
Visual feedback is showing users what's happening. When you interact with something, it should show you the result.
Types of Visual Feedback:
Hover States:
- "What will happen if I drop here?"
- Example: Palette item highlights when mouse hovers over it
- Example: Canvas area highlights when dragging shape over it
Selection Highlighting:
- "What's currently selected?"
- Example: Selected shape has a blue outline
- Example: Selected text is highlighted
Drag Previews:
- "Where will this go?"
- Example: Ghost image of shape follows mouse while dragging
- Example: Preview shows where shape will be placed
Loading States:
- "What's happening now?"
- Example: Spinner while processing
- Example: "Processing..." message
Why Visual Feedback Matters:
Without feedback, users don't know:
- If their click registered
- Where things will go
- What's selected
- If something is processing
With feedback, users always know what's happening!
The Problem
You need:
- A palette UI with shape icons
- Drag detection (mouse down on palette item)
- Drag preview (visual feedback while dragging)
- Drop detection (mouse up over canvas)
- Code generation (create shape code at drop position)
- Code insertion (add code to editor)
The Palette - Explained Simply
What is a Palette?
A palette is a UI panel that shows available shapes. Users can drag shapes from the palette onto the canvas. Think of it like a toolbox - all your tools (shapes) are in one place.
The DragDropSystem Class:
This class manages the entire drag-and-drop system. It needs references to several components to coordinate everything.
Constructor Parameters:
renderer- The renderer that draws shapes (needed to access canvas and trigger redraws)editor- The code editor (CodeMirror instance) (needed to insert code when shapes are dropped)shapeManager- The shape manager that coordinates updates (ensures UI, code, and shapes stay in sync)
State Properties:
isVisible- Whether palette is shown or hidden (false= hidden,true= visible)isDragging- Whether user is currently dragging (false= not dragging,true= actively dragging)draggedShape- Which shape type is being dragged (null= none,'circle','rectangle', etc. = type)dragOffset- Offset between mouse and shape while dragging (used to position drag preview correctly)shapeCounter- Counter for generating unique shape names (starts at 1, increments for each new shape)
Initialization Methods:
initializePalette()- Creates the palette DOM elementssetupEventListeners()- Sets up mouse/keyboard event handlersaddToggleButton()- Adds button to show/hide palette
export class DragDropSystem {
constructor(renderer, editor, shapeManager) {
// Store component references
this.renderer = renderer;
this.editor = editor;
this.shapeManager = shapeManager;
this.canvas = renderer.canvas;
// Initialize state
this.isVisible = false; // Palette visibility
this.isDragging = false; // Drag state tracking
this.draggedShape = null; // Shape type being dragged
this.dragOffset = { x: 0, y: 0 }; // Mouse offset for drag preview
this.shapeCounter = 1; // Counter for unique shape names
// Initialize palette and event handlers
this.initializePalette();
this.setupEventListeners();
this.addToggleButton();
}
}
If you see an error at this step:
Error: TypeError: Cannot read property 'canvas' of undefined
- What this means: renderer is undefined or doesn't have canvas property
- Common causes:
- renderer not passed:
new DragDropSystem(undefined, editor, shapeManager) - renderer not initialized: Renderer created but canvas not set
- Wrong object: Passed wrong object instead of renderer
- renderer not passed:
- Fix: Check renderer exists:
if (!renderer || !renderer.canvas) throw new Error('Renderer with canvas required');, verify renderer has canvas property, ensure renderer is initialized
Error: TypeError: Cannot read property 'setValue' of undefined
- What this means: editor is undefined or not a CodeMirror instance
- Common causes:
- editor not passed:
new DragDropSystem(renderer, undefined, shapeManager) - editor not initialized: Editor created but not properly set up
- Wrong editor type: Not a CodeMirror instance
- editor not passed:
- Fix: Check editor exists:
if (!editor || typeof editor.setValue !== 'function') throw new Error('Editor required');, verify editor is CodeMirror instance, ensure editor is initialized
Error: TypeError: Cannot read property 'updateShape' of undefined
- What this means: shapeManager is undefined or doesn't have expected methods
- Common causes:
- shapeManager not passed:
new DragDropSystem(renderer, editor, undefined) - shapeManager not initialized: Created but methods not implemented
- Wrong object: Passed wrong object instead of shapeManager
- shapeManager not passed:
- Fix: Check shapeManager exists, verify shapeManager has required methods (updateShape, etc.), ensure shapeManager is initialized
Error: TypeError: this.initializePalette is not a function
- What this means: initializePalette method not defined
- Common causes:
- Method not implemented: initializePalette() not defined in class
- Method name typo: initializePalet vs initializePalette
- Method in wrong scope: Defined outside class
- Fix: Implement initializePalette() method, check method name spelling, ensure method is in class body
Error: Palette doesn't appear (DOM not created)
- What this means: initializePalette() not called or failed
- Common causes:
- Method not called: Constructor doesn't call initializePalette()
- Method throws error: initializePalette() fails before creating DOM
- DOM not appended: Element created but not added to document.body
- Fix: Ensure constructor calls initializePalette(), check method doesn't throw errors, verify element appended to body:
document.body.appendChild(this.paletteContainer);
Palette Initialization - Explained Simply
What is Palette Initialization?
Palette initialization creates the HTML structure (DOM elements) for the palette. This is the visual UI that users see - the panel with shape icons.
The initializePalette() Method:
Creates a floating panel with a header (title and close button), content area, and a grid of shape items. The palette is a DOM element that gets appended to the document body.
Understanding the DOM Structure:
This creates a floating panel with:
- Header - Title and close button
- Content - Grid of shape items
- Shape items - Icons for each shape type
Step-by-Step:
- Create container element: Creates a new
<div>element and stores it inthis.paletteContainerfor later reference - Set CSS class: Adds CSS class for styling (position, colors, etc.)
- Set inner HTML: Sets the HTML content using template literal, with
${this.generateShapeItems()}generating shape HTML - Append to document: Adds the element to the page (without this, the element exists but isn't visible!)
Understanding the HTML Structure:
- Outer div - Main container (the floating panel)
- Header div - Top section (title and close button)
- Content div - Main area (where shapes are displayed)
- Grid div - Layout container (arranges shapes in a grid)
Why Use innerHTML?
innerHTML lets us set HTML content easily. We could create each element with createElement() and appendChild(), but innerHTML is simpler for this case.
initializePalette() {
// Create container element for the palette
this.paletteContainer = document.createElement('div');
this.paletteContainer.className = 'shape-palette-container';
// Set HTML content (header, content area, grid with shape items)
this.paletteContainer.innerHTML = `
<div class="palette-header">
<h3>Shape Palette</h3>
<button class="close-palette">×</button>
</div>
<div class="palette-content">
<div class="palette-grid">
${this.generateShapeItems()}
</div>
</div>
`;
// Append to document body to make it visible
document.body.appendChild(this.paletteContainer);
}
If you see an error at this step:
Error: TypeError: document.createElement 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: this.generateShapeItems is not a function
- What this means: generateShapeItems method not defined
- Common causes:
- Method not implemented: generateShapeItems() not defined in class
- Method name typo: generateShapeItem vs generateShapeItems
- Method in wrong scope: Defined outside class
- Fix: Implement generateShapeItems() method, check method name spelling, ensure method is in class body
Error: Palette HTML is empty (no shape items)
- What this means: generateShapeItems() returns empty string or undefined
- Common causes:
- Method returns empty: generateShapeItems() returns '' or undefined
- Method throws error: generateShapeItems() fails, returns nothing
- Template literal issue: ${} not evaluated correctly
- Fix: Check generateShapeItems() returns string, verify method doesn't throw errors, ensure template literal syntax is correct (backticks, not quotes)
Error: TypeError: document.body is null
- What this means: document.body doesn't exist (DOM not ready)
- Common causes:
- Script runs too early: Runs before DOM is loaded
- No body element: HTML has no tag (unlikely but possible)
- Document not ready: DOMContentLoaded not fired yet
- Fix: Wait for DOM ready:
if (document.body) { ... } else { window.addEventListener('DOMContentLoaded', () => { ... }); }, ensure script runs after DOM loads, check HTML has body tag
Error: Palette appears but styles are wrong (not floating, wrong position)
- What this means: CSS not loaded or class names wrong
- Common causes:
- CSS not included: Stylesheet not loaded in HTML
- Class name mismatch: CSS uses different class name
- CSS specificity: Other styles overriding palette styles
- Fix: Check CSS file is loaded, verify class names match CSS, check CSS specificity (may need !important or more specific selectors)
Shape Items
Generate palette items for each shape type:
generateShapeItems() {
const shapes = [
{ type: 'circle', name: 'Circle', icon: this.createCircleIcon() },
{ type: 'rectangle', name: 'Rectangle', icon: this.createRectangleIcon() },
{ type: 'triangle', name: 'Triangle', icon: this.createTriangleIcon() },
// ... more shapes
];
return shapes.map(shape => `
<div class="palette-item" data-shape-type="${shape.type}">
<div class="shape-icon">${shape.icon}</div>
<div class="shape-name">${shape.name}</div>
</div>
`).join('');
}
Icons: Use SVG icons. They scale nicely and look good.
Drag Start
When user clicks a palette item, we initialize the drag operation. We track the offset between the mouse and the palette item so the drag preview maintains the same relative position.
Why track offset? The drag preview should follow the mouse, but maintain the same offset from the mouse cursor as the original item. This makes the drag feel natural and consistent.
startDrag(e, paletteItem) {
// Prevent default browser drag behavior
e.preventDefault();
// Get shape type from data attribute
const shapeType = paletteItem.dataset.shapeType;
const rect = paletteItem.getBoundingClientRect();
// Set drag state
this.isDragging = true;
this.draggedShape = shapeType;
// Calculate offset between mouse and palette item (for natural drag feel)
this.dragOffset = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
// Create visual drag preview
this.createDragPreview(e, paletteItem);
// Visual feedback (add dragging class, change cursor)
paletteItem.classList.add('dragging');
document.body.style.cursor = 'grabbing';
}
Drag Preview
Create a visual preview that follows the mouse. The preview is a clone of the palette item that moves with the cursor.
Why fixed position? Fixed position is relative to viewport, not parent. This makes it easy to position relative to mouse. We also set pointer-events: none so the preview doesn't interfere with drop detection, and a high z-index so it appears above everything.
createDragPreview(e, paletteItem) {
// Clone the palette item for the drag preview
this.dragPreview = paletteItem.cloneNode(true);
this.dragPreview.className = 'drag-preview';
// Position relative to viewport (fixed) so it follows mouse correctly
this.dragPreview.style.position = 'fixed';
// Don't interfere with mouse events (drop detection, etc.)
this.dragPreview.style.pointerEvents = 'none';
// High z-index to appear above everything
this.dragPreview.style.zIndex = '10000';
// Position using mouse coordinates with offset (maintains relative position)
this.dragPreview.style.left = (e.clientX - this.dragOffset.x) + 'px';
this.dragPreview.style.top = (e.clientY - this.dragOffset.y) + 'px';
// Add to document so it's visible
document.body.appendChild(this.dragPreview);
}
Drag Handling
Update preview position as mouse moves. Check if the mouse is over the canvas and provide visual feedback by adding a class.
Visual feedback: Add a class when over canvas so you can style it differently (e.g., highlight). This gives users clear feedback about where they can drop.
handleDrag(e) {
// Only update if drag preview exists
if (!this.dragPreview) return;
// Update preview position to follow mouse (using offset for natural feel)
this.dragPreview.style.left = (e.clientX - this.dragOffset.x) + 'px';
this.dragPreview.style.top = (e.clientY - this.dragOffset.y) + 'px';
// Check if mouse is over canvas (for drop detection)
const canvasRect = this.canvas.getBoundingClientRect();
const isOverCanvas = (
e.clientX >= canvasRect.left &&
e.clientX <= canvasRect.right &&
e.clientY >= canvasRect.top &&
e.clientY <= canvasRect.bottom
);
// Add/remove class for visual feedback (can style differently when over canvas)
this.dragPreview.classList.toggle('over-canvas', isOverCanvas);
}
Drop Handling
When user releases mouse over canvas, we create the shape at the drop position. We need to convert coordinates from screen space to world space.
Coordinate conversion: Screen coordinates (pixels) → canvas coordinates (pixels) → world coordinates (mm). Use renderer's coordinate system to do the conversion.
handleDrop(e) {
// Check if mouse is over canvas
const canvasRect = this.canvas.getBoundingClientRect();
const isOverCanvas = (
e.clientX >= canvasRect.left &&
e.clientX <= canvasRect.right &&
e.clientY >= canvasRect.top &&
e.clientY <= canvasRect.bottom
);
// Only create shape if dropped over canvas and we have a shape type
if (isOverCanvas && this.draggedShape) {
// Convert screen coordinates to canvas coordinates (relative to canvas)
const canvasX = e.clientX - canvasRect.left;
const canvasY = e.clientY - canvasRect.top;
// Convert canvas coordinates to world coordinates (mm)
const worldPos = this.renderer.coordinateSystem.screenToWorld(canvasX, canvasY);
// Create shape at the drop position
this.createShapeAtPosition(this.draggedShape, worldPos.x, worldPos.y);
}
// Clean up drag state and visual elements
this.cleanupDrag();
}
Shape Creation
Generate code and insert it into the editor, then run the code to create the shape. Each shape type has sensible defaults (circle gets radius 50, rectangle gets 80x60, etc.).
createShapeAtPosition(shapeType, x, y) {
// Generate unique shape name (e.g., "circle1", "circle2")
const shapeName = `${shapeType}${this.shapeCounter++}`;
// Generate code for the shape
const shapeCode = this.generateShapeCode(shapeType, shapeName, x, y);
// Insert code into editor
this.insertCodeIntoEditor(shapeCode);
// Run code to create shape (trigger interpreter and rendering)
if (window.runCode) {
window.runCode();
}
}
generateShapeCode(type, name, x, y) {
// Get default parameters for this shape type
const defaultParams = this.getDefaultParams(type);
// Round position to nearest integer for cleaner code
const position = `[${Math.round(x)}, ${Math.round(y)}]`;
// Start with position parameter
let params = [`position: ${position}`];
// Add type-specific parameters (format strings with quotes, numbers without)
Object.entries(defaultParams).forEach(([key, value]) => {
if (typeof value === 'string') {
params.push(`${key}: "${value}"`);
} else {
params.push(`${key}: ${value}`);
}
});
// Generate shape code with proper formatting
return `\nshape ${type} ${name} {\n ${params.join('\n ')}\n fill: false\n}\n`;
}
Code Insertion
Add code to editor:
insertCodeIntoEditor(code) {
const currentCode = this.editor.getValue();
const newCode = currentCode + code;
this.editor.setValue(newCode);
}
Why append? Adding to the end is simplest. User can move it if needed.
Cleanup
Remove drag state:
cleanupDrag() {
// Remove drag preview
if (this.dragPreview) {
this.dragPreview.remove();
this.dragPreview = null;
}
// Remove dragging state
document.querySelectorAll('.palette-item.dragging').forEach(item => {
item.classList.remove('dragging');
});
// Reset state
this.isDragging = false;
this.draggedShape = null;
document.body.style.cursor = '';
}
Common Gotchas
Gotcha 1: Coordinate conversion Screen → canvas → world. Don't forget any step.
Gotcha 2: Event propagation Prevent default on drag start, or browser tries to drag the image.
Gotcha 3: Z-index Drag preview needs high z-index to appear above everything.
Gotcha 4: Pointer events
Drag preview should have pointer-events: none so it doesn't interfere with drop detection.
Gotcha 5: Shape naming
Use a counter to ensure unique names. circle1, circle2, etc.
Building the Parameter Manager UI From Scratch
Users need sliders to adjust shape parameters. This connects the UI to the shape manager.
The Problem
You need:
- A UI panel that shows shape parameters
- Sliders for numeric parameters
- Text inputs for direct value entry
- Real-time updates as user drags
- Sync with shape changes from canvas
The Parameter Manager
Main class that manages the UI:
export class ParameterManager {
constructor(canvas, interpreter, editor, runCode) {
this.canvas = canvas;
this.interpreter = interpreter;
this.editor = editor;
this.runCode = runCode;
this.paramsList = null;
this.menuVisible = false;
this.currentShape = null;
}
}
Creating Parameter Controls
For each parameter, create a slider + input:
createParameterControl(shapeName, paramName, value, min, max, step) {
const container = document.createElement('div');
container.className = 'parameter-item';
const label = document.createElement('label');
label.className = 'parameter-label';
label.textContent = `${paramName}:`;
container.appendChild(label);
const sliderContainer = document.createElement('div');
sliderContainer.className = 'parameter-slider-container';
// Create slider
const slider = document.createElement('input');
slider.type = 'range';
slider.className = 'parameter-slider';
slider.min = min;
slider.max = max;
slider.step = step;
slider.value = value;
// Store data for ShapeManager
slider.dataset.originalValue = value;
slider.dataset.shapeName = shapeName;
slider.dataset.paramName = paramName;
// Real-time updates
slider.addEventListener('input', (e) => {
const newValue = parseFloat(e.target.value);
input.value = newValue;
// IMMEDIATE update through ShapeManager
shapeManager.onSliderChange(shapeName, paramName, newValue, true);
});
// Final update when released
slider.addEventListener('change', (e) => {
const newValue = parseFloat(e.target.value);
slider.dataset.originalValue = newValue;
// Final update through ShapeManager
shapeManager.onSliderChange(shapeName, paramName, newValue, false);
});
sliderContainer.appendChild(slider);
// Create number input
const input = document.createElement('input');
input.type = 'number';
input.className = 'parameter-value';
input.value = value;
input.min = min;
input.max = max;
input.step = step;
input.dataset.shapeName = shapeName;
input.dataset.paramName = paramName;
// Real-time updates for number input
input.addEventListener('input', (e) => {
const newValue = parseFloat(e.target.value);
slider.value = newValue;
shapeManager.onSliderChange(shapeName, paramName, newValue, true);
});
sliderContainer.appendChild(input);
container.appendChild(sliderContainer);
return container;
}
Why two controls? Slider for quick adjustment, input for precise values.
Updating with Interpreter
When code runs, update the UI:
updateWithLatestInterpreter() {
if (!this.interpreter?.env?.shapes) return;
// Clear existing controls
if (this.paramsList) {
this.paramsList.innerHTML = '';
}
// Get selected shape
const selectedShape = this.getSelectedShape();
if (!selectedShape) {
this.paramsList.innerHTML = '<div class="no-shapes-message">No shape selected</div>';
return;
}
// Create controls for each parameter
const shape = selectedShape.shape;
const shapeName = selectedShape.name;
// Transform parameters
if (shape.transform) {
if (shape.transform.position) {
this.createParameterControl(shapeName, 'position_x', shape.transform.position[0], -1000, 1000, 1);
this.createParameterControl(shapeName, 'position_y', shape.transform.position[1], -1000, 1000, 1);
}
if (shape.transform.rotation !== undefined) {
this.createParameterControl(shapeName, 'rotation', shape.transform.rotation, 0, 360, 1);
}
if (shape.transform.scale) {
this.createParameterControl(shapeName, 'scale_x', shape.transform.scale[0], 0.1, 10, 0.1);
this.createParameterControl(shapeName, 'scale_y', shape.transform.scale[1], 0.1, 10, 0.1);
}
}
// Shape-specific parameters
if (shape.params) {
Object.entries(shape.params).forEach(([key, value]) => {
if (typeof value === 'number') {
// Determine min/max/step based on parameter
const { min, max, step } = this.getParameterLimits(key, value);
this.createParameterControl(shapeName, key, value, min, max, step);
}
});
}
}
Parameter limits: Different parameters need different ranges. Radius might be 0-500, rotation 0-360, scale 0.1-10.
Code Update
When slider changes, update code:
updateCodeInEditor(shapeName, paramName, value) {
if (!this.editor) return;
const code = this.editor.getValue();
// Find the shape definition
const shapeRegex = new RegExp(`shape\\s+\\w+\\s+${shapeName}\\s*\\{[^}]*\\}`, 's');
const match = code.match(shapeRegex);
if (!match) return;
// Update the parameter in the code
let shapeCode = match[0];
const paramRegex = new RegExp(`(${paramName}\\s*:\\s*)[^\\n,}]+`, 'g');
if (paramRegex.test(shapeCode)) {
// Parameter exists - replace it
shapeCode = shapeCode.replace(paramRegex, `$1${value}`);
} else {
// Parameter doesn't exist - add it
shapeCode = shapeCode.replace(/\}$/, ` ${paramName}: ${value}\n}`);
}
// Replace in full code
const newCode = code.replace(shapeRegex, shapeCode);
this.editor.setValue(newCode);
}
Regex matching: Find shape definition, find parameter, replace or add it.
Common Gotchas
Gotcha 1: Real-time vs final updates
input event fires while dragging, change fires on release. Use both for smooth experience.
Gotcha 2: Value synchronization Slider and input must stay in sync. Update both when either changes.
Gotcha 3: Parameter limits Wrong min/max makes slider useless. Use sensible defaults based on parameter type.
Gotcha 4: Code update timing
Update code too often = slow. Debounce or only update on change event.
Gotcha 5: Shape selection UI only shows parameters for selected shape. Make sure selection system works.
Building the Interaction Handler From Scratch
The interaction handler processes mouse events for shape manipulation. It's the bridge between user input and shape updates.
The Problem
You need to handle:
- Shape selection (click on shape)
- Shape dragging (drag selected shape)
- Handle interaction (drag handles to resize/rotate)
- Panning (drag background)
- Zooming (mouse wheel)
- Hover feedback (highlight shape under mouse)
The Handler
Main class that processes events:
class SimpleInteractionHandler {
constructor(renderer) {
this.renderer = renderer;
this.canvas = renderer.canvas;
this.coordinateSystem = renderer.coordinateSystem;
this.selectedShape = null;
this.hoveredShape = null;
this.dragging = false;
this.scaling = false;
this.rotating = false;
this.panning = false;
this.lastMousePos = { x: 0, y: 0 };
this.activeHandle = null;
this.hoveredHandle = null;
this.setupEventListeners();
}
}
Event Listeners
Set up mouse and wheel events:
setupEventListeners() {
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.addEventListener('wheel', this.handleWheel.bind(this));
this.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this));
}
Mouse Down
Handle clicks. We check for hits in priority order: handles first (if shape selected), then shapes, then background (pan).
Priority order: Handles first (if shape selected), then shapes, then background (pan). This ensures handles are selectable even when overlapping shapes.
handleMouseDown(event) {
// Get mouse position relative to canvas
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Convert to world coordinates for shape hit testing
const worldPos = this.coordinateSystem.screenToWorld(x, y);
// Check for handle hit first (if a shape is selected)
if (this.selectedShape) {
const handleHit = this.renderer.handleSystem.getHandleAtPoint(x, y, this.selectedShape);
if (handleHit) {
this.activeHandle = handleHit;
// Set interaction state based on handle type
if (handleHit.type === 'scale') {
this.scaling = true;
} else if (handleHit.type === 'rotate') {
this.rotating = true;
}
return; // Handles take priority, exit early
}
}
// Check for shape hit (convert to world coordinates for hit testing)
const hitShape = this.findShapeAtPoint(worldPos.x, worldPos.y);
if (hitShape) {
// Select shape and start dragging
this.selectedShape = hitShape;
this.dragging = true;
this.renderer.setSelectedShape(hitShape);
} else {
// Background click - start panning (deselect shape)
this.panning = true;
this.selectedShape = null;
this.renderer.setSelectedShape(null);
}
// Store mouse position for delta calculations
this.lastMousePos = { x, y };
// Redraw to show selection/handle changes
this.renderer.redraw();
}
Mouse Move
Handle dragging and hovering:
handleMouseMove(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const dx = x - this.lastMousePos.x;
const dy = y - this.lastMousePos.y;
if (this.panning) {
// Pan the canvas
this.coordinateSystem.pan(dx, dy);
this.renderer.redraw();
} else if (this.scaling && this.selectedShape) {
// Scale shape via handle
this.handleParameterScaling(dx, dy);
} else if (this.rotating && this.selectedShape) {
// Rotate shape via handle
this.handleRotation(x, y);
} else if (this.dragging && this.selectedShape) {
// Drag shape
this.handleDragging(dx, dy);
} else {
// Just hovering - update cursor and hover state
this.updateCursor(x, y);
this.updateHoverState(x, y);
}
this.lastMousePos = { x, y };
}
State machine: Different states (panning, scaling, rotating, dragging) do different things.
Shape Dragging
Move shape when dragging. We convert the screen movement delta to world coordinates and update the shape position through the shape manager.
Coordinate conversion: Screen delta (pixels) → world delta (mm). Y is flipped (negative) because canvas Y-axis is flipped compared to world Y-axis.
handleDragging(dx, dy) {
if (!this.selectedShape) return;
// Convert screen delta (pixels) to world delta (mm)
// Divide by scale and zoom to convert from screen to world space
// Negate dy because canvas Y is flipped (positive down) vs world Y (positive up)
const worldDx = dx / (this.coordinateSystem.scale * this.coordinateSystem.zoomLevel);
const worldDy = -dy / (this.coordinateSystem.scale * this.coordinateSystem.zoomLevel);
// Get shape name for updating
const shapeName = this.renderer.findShapeName(this.selectedShape);
if (!shapeName) return;
// Calculate new position by adding delta to current position
const currentPos = this.selectedShape.transform?.position || [0, 0];
const newPos = [
currentPos[0] + worldDx,
currentPos[1] + worldDy
];
// Update shape position through shape manager (ensures proper sync)
shapeManager.onCanvasPositionChange(shapeName, newPos);
}
Handle Scaling
Resize shape via handle:
handleParameterScaling(dx, dy) {
if (!this.selectedShape || !this.activeHandle) return;
const shapeName = this.renderer.findShapeName(this.selectedShape);
if (!shapeName) return;
// Calculate scale factor based on handle movement
const distance = Math.sqrt(dx * dx + dy * dy);
const scaleFactor = 1 + (distance / 100); // Adjust sensitivity
// Delegate to transform manager
this.renderer.transformManager.handleParameterScaling(
this.selectedShape,
this.activeHandle,
dx,
dy,
scaleFactor,
shapeName,
shapeManager
);
}
Why delegate? Transform manager knows how to scale different shape types. Some scale uniformly, some scale width/height separately.
Handle Rotation
Rotate shape via handle. We calculate the angle from the shape center to the mouse position and update the rotation.
Angle calculation: Use atan2 to get angle from center to mouse. atan2 handles all quadrants correctly and gives us the angle in radians, which we convert to degrees.
handleRotation(x, y) {
if (!this.selectedShape) return;
const shapeName = this.renderer.findShapeName(this.selectedShape);
if (!shapeName) return;
// Get shape center in world coordinates
const center = this.renderer.handleSystem.getShapeCenter(this.selectedShape);
// Convert center to screen coordinates for angle calculation
const centerScreen = this.coordinateSystem.worldToScreen(center.x, center.y);
// Calculate angle from center to mouse using atan2
// atan2(dy, dx) gives angle in radians, convert to degrees
const angle = Math.atan2(y - centerScreen.y, x - centerScreen.x) * 180 / Math.PI;
// Update rotation through shape manager
shapeManager.onCanvasRotationChange(shapeName, angle);
}
Hover State
Update hover feedback:
updateHoverState(x, y) {
const worldPos = this.coordinateSystem.screenToWorld(x, y);
const hitShape = this.findShapeAtPoint(worldPos.x, worldPos.y);
if (hitShape !== this.hoveredShape) {
this.hoveredShape = hitShape;
this.renderer.setHoveredShape(hitShape ? this.renderer.findShapeName(hitShape) : null);
this.renderer.redraw();
}
}
Why check change? Only redraw if hover state actually changed. Otherwise you redraw constantly.
Mouse Up
End interactions:
handleMouseUp(event) {
this.dragging = false;
this.scaling = false;
this.rotating = false;
this.panning = false;
this.activeHandle = null;
this.renderer.redraw();
}
Clear all states: When mouse is released, clear all interaction states.
Wheel Zoom
Handle mouse wheel:
handleWheel(event) {
event.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const delta = event.deltaY > 0 ? 0.9 : 1.1;
this.coordinateSystem.zoomAtPoint(x, y, delta);
this.renderer.redraw();
}
Zoom at point: Zoom centered on mouse position, not canvas center.
Common Gotchas
Gotcha 1: Coordinate conversion Always convert screen → world for shape operations. Screen coordinates are pixels, world coordinates are mm.
Gotcha 2: Y-axis flip Canvas Y is flipped (positive down). World Y is normal (positive up). Negate Y when converting.
Gotcha 3: State management Clear all states on mouse up. Otherwise states leak between interactions.
Gotcha 4: Hit testing Hit test in world coordinates, not screen. Shapes are in world space.
Gotcha 5: Redraw frequency Don't redraw on every mouse move. Use requestAnimationFrame or throttle.
Implementation Adjustments and Notes
This section documents adjustments that were needed after the initial implementation, based on practical experience.
Button Placement and Functionality
Issue: Button functionality and placement needed adjustment for better UX.
Solution: Implemented specific button layout:
- Top-left canvas button: Grid toggle
- Top-right canvas button: Shape palette (drag-and-drop) - NOT grid toggle
- Blocks button (left panel): Toggles between text editor and Blockly editor
Implementation:
<!-- Top-left: Grid toggle -->
<button class="canvas-control-btn top-left" id="grid-toggle" title="Toggle grid">
<!-- Grid icon SVG -->
</button>
<!-- Top-right: Shape palette -->
<button class="canvas-control-btn top-right" id="palette-toggle" title="Shape Palette">
<!-- Palette icon SVG -->
</button>
// Palette toggle button (top-right canvas button)
const paletteToggleBtn = document.getElementById('palette-toggle');
if (paletteToggleBtn) {
paletteToggleBtn.addEventListener('click', () => {
if (dragDropSystem) {
dragDropSystem.toggle();
}
});
}
Why: This layout separates concerns - grid control is visual, palette is functionality. The top-right position is intuitive for tool panels in design applications.
Blocks Button Should Toggle Editors
Issue: Blocks button should switch between text and Blockly editors, not just show/hide Blockly.
Solution: Implement toggle functionality that swaps editor containers:
// Blocks toggle button - switches between text editor and Blockly
const blocksToggleBtn = document.getElementById('blocks-toggle-button');
if (blocksToggleBtn) {
blocksToggleBtn.addEventListener('click', () => {
if (blocklyIntegration) {
const isBlocklyVisible = blocklyIntegration.toggleVisibility();
blocksToggleBtn.textContent = isBlocklyVisible ? 'Code' : 'Blocks';
}
});
}
Why: Users expect a toggle button to switch between modes. The button label should reflect the current mode and indicate what clicking will do.
Drag-and-Drop Integration
Issue: The drag-and-drop system initialization had several timing issues:
- It was trying to initialize before editor was ready
- Button handlers couldn't access it because it was initialized asynchronously
- No fallback if initialization failed
- The
addToggleButton()method was looking for a button that didn't exist in the expected location
What Was Added: Proper initialization timing, global storage, and removal of the automatic button hookup (since button is handled separately).
Specific Implementation Changes:
1. Initialization with Proper Timing:
// Initialize drag and drop system (depends on editor being ready)
setTimeout(() => {
// Check that both dependencies are ready
if (editor && renderer) {
// Create drag and drop system
dragDropSystem = new DragDropSystem(renderer, editor, shapeMgr);
// CRITICAL: Store globally for button handlers
// Button handlers may execute before this initialization completes
window.dragDropSystem = dragDropSystem;
console.log('Drag and drop system initialized');
} else {
console.warn('Drag and drop system not initialized: editor or renderer not ready');
// Could retry here if needed
}
}, 100); // Delay ensures editor (CodeMirror) is fully initialized
2. Updated DragDropSystem Constructor:
The addToggleButton() method was removed/updated because the button is now handled in app.js:
// BEFORE: Tried to find and hook up button automatically
addToggleButton() {
const blocksButton = document.querySelector('.blocks-button');
if (blocksButton) {
blocksButton.addEventListener('click', () => {
this.toggle();
});
}
}
// AFTER: Button hookup removed - handled in app.js instead
addToggleButton() {
// Palette toggle is now handled by the top-right canvas button
// This method is kept for compatibility but the button is set up in app.js
}
3. Button Handler in app.js (separate from DragDropSystem):
// Palette toggle button handler (top-right canvas button)
const paletteToggleBtn = document.getElementById('palette-toggle');
if (paletteToggleBtn) {
paletteToggleBtn.addEventListener('click', () => {
// Multiple fallback checks for async initialization
if (window.dragDropSystem) {
// Global reference (set during async initialization)
window.dragDropSystem.toggle();
} else if (dragDropSystem) {
// Local variable (if in same scope when handler executes)
dragDropSystem.toggle();
} else {
// System not ready yet
alert('Drag and drop system is loading...');
// Could also try to initialize here if needed
}
});
}
4. Global Storage Pattern:
This pattern was used for multiple async components:
// Store components globally for cross-component access
window.dragDropSystem = dragDropSystem;
window.blocklyIntegration = blocklyIntegration;
window.debugVisualizer = debugVisualizer;
window.renderer = renderer;
window.interpreter = interpreter;
window.runCode = runCode;
Why These Changes Were Necessary:
- Initialization Order: Editor (CodeMirror) takes time to initialize. DragDropSystem needs the editor instance, so it must wait.
- Button Handler Timing: Button event listeners are set up immediately on DOMContentLoaded, but DragDropSystem initializes later. Global storage bridges this timing gap.
- Multiple Access Points: Button handlers, export functions, and other code need access. Global storage provides a reliable access pattern.
- Separation of Concerns: Button handling is UI orchestration (app.js responsibility), not DragDropSystem's responsibility. This separation makes the code more maintainable.
What Happened Without These Changes:
- Button clicks would fail silently because
dragDropSystemwas undefined when handlers were set up - Console errors about "Cannot read property 'toggle' of undefined"
- No way for other code to access the drag-and-drop system
- Tight coupling between DragDropSystem and button HTML structure
How to Build UI Systems - Complete Step-by-Step Guide
This section provides a complete guide for building the drag-and-drop system and parameter manager UI from scratch.
Part 1: Building the Drag and Drop System
Step 1.1: Create the DragDropSystem Class
File: src/dragDropSystem.mjs
export class DragDropSystem {
constructor(renderer, editor, shapeManager) {
this.renderer = renderer;
this.editor = editor;
this.shapeManager = shapeManager;
this.canvas = renderer.canvas;
this.isVisible = false;
this.isDragging = false;
this.draggedShape = null;
this.dragOffset = { x: 0, y: 0 };
this.shapeCounter = 1;
this.initializePalette();
this.setupEventListeners();
this.addToggleButton();
}
}
Step 1.2: Initialize the Palette UI
initializePalette() {
this.paletteContainer = document.createElement('div');
this.paletteContainer.className = 'shape-palette-container';
this.paletteContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
padding: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
display: none;
`;
this.paletteContainer.innerHTML = `
<div class="palette-header">
<h3>Shapes</h3>
</div>
<div class="palette-items"></div>
`;
document.body.appendChild(this.paletteContainer);
this.paletteItems = this.paletteContainer.querySelector('.palette-items');
this.generateShapeItems();
}
generateShapeItems() {
const shapes = [
{ type: 'circle', name: 'Circle', icon: '○' },
{ type: 'rectangle', name: 'Rectangle', icon: '▭' },
{ type: 'triangle', name: 'Triangle', icon: '△' },
{ type: 'polygon', name: 'Polygon', icon: '⬟' },
{ type: 'star', name: 'Star', icon: '★' }
];
shapes.forEach(shape => {
const item = document.createElement('div');
item.className = 'palette-item';
item.draggable = true;
item.dataset.shapeType = shape.type;
item.style.cssText = `
padding: 10px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: grab;
user-select: none;
`;
item.textContent = `${shape.icon} ${shape.name}`;
item.addEventListener('dragstart', (e) => {
this.startDrag(e, shape.type);
});
this.paletteItems.appendChild(item);
});
}
Step 1.3: Implement Drag Start
startDrag(e, shapeType) {
this.isDragging = true;
this.draggedShape = shapeType;
// Store mouse offset
const rect = this.canvas.getBoundingClientRect();
this.dragOffset = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
// Set drag data
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('text/plain', shapeType);
// Create drag preview
this.createDragPreview(e);
}
Step 1.4: Implement Drag Handling
setupEventListeners() {
// Canvas drag events
this.canvas.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
this.handleDrag(e);
});
this.canvas.addEventListener('drop', (e) => {
e.preventDefault();
this.handleDrop(e);
});
this.canvas.addEventListener('dragleave', () => {
this.cleanupDrag();
});
}
handleDrag(e) {
if (!this.isDragging) return;
// Update drag preview position
if (this.dragPreview) {
const rect = this.canvas.getBoundingClientRect();
this.dragPreview.style.left = (e.clientX - this.dragOffset.x) + 'px';
this.dragPreview.style.top = (e.clientY - this.dragOffset.y) + 'px';
}
}
Step 1.5: Implement Drop Handling
handleDrop(e) {
if (!this.isDragging || !this.draggedShape) return;
const rect = this.canvas.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
// Convert screen coordinates to world coordinates
const worldPos = this.renderer.coordinateSystem.screenToWorld(canvasX, canvasY);
// Create shape at position
this.createShapeAtPosition(this.draggedShape, worldPos.x, worldPos.y);
this.cleanupDrag();
}
createShapeAtPosition(shapeType, x, y) {
// Generate unique shape name
const shapeName = `${shapeType}${this.shapeCounter++}`;
// Generate code for the shape
const code = this.generateShapeCode(shapeType, shapeName, x, y);
// Insert code into editor
this.insertCodeIntoEditor(code);
// Run code to create shape
if (window.runCode) {
window.runCode();
}
}
generateShapeCode(type, name, x, y) {
const templates = {
circle: `shape circle ${name} {
radius: 50
x: ${x}
y: ${y}
}`,
rectangle: `shape rectangle ${name} {
width: 100
height: 100
x: ${x}
y: ${y}
}`,
triangle: `shape triangle ${name} {
radius: 50
x: ${x}
y: ${y}
}`,
polygon: `shape polygon ${name} {
radius: 50
sides: 5
x: ${x}
y: ${y}
}`,
star: `shape star ${name} {
outerRadius: 50
innerRadius: 25
points: 5
x: ${x}
y: ${y}
}`
};
return templates[type] || '';
}
insertCodeIntoEditor(code) {
if (!this.editor) return;
const currentCode = this.editor.getValue();
const newCode = currentCode + '\n\n' + code;
this.editor.setValue(newCode);
}
Step 1.6: Add Toggle Button
addToggleButton() {
const button = document.createElement('button');
button.textContent = 'Shapes';
button.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
z-index: 1001;
`;
button.addEventListener('click', () => {
this.togglePalette();
});
document.body.appendChild(button);
this.toggleButton = button;
}
togglePalette() {
this.isVisible = !this.isVisible;
this.paletteContainer.style.display = this.isVisible ? 'block' : 'none';
}
Part 2: Building the Parameter Manager UI
Step 2.1: Create the ParameterManager Class
File: src/2Dparameters.mjs
export class ParameterManager {
constructor(editor, shapeManager) {
this.editor = editor;
this.shapeManager = shapeManager;
this.menuVisible = false;
this.paramsList = null;
this.createUI();
}
createUI() {
this.container = document.createElement('div');
this.container.className = 'parameter-manager-container';
this.container.style.cssText = `
position: fixed;
top: 20px;
left: 20px;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
display: none;
max-width: 300px;
max-height: 80vh;
overflow-y: auto;
`;
this.container.innerHTML = `
<div class="params-header">
<h3>Parameters</h3>
<button class="close-btn">×</button>
</div>
<div class="params-list"></div>
`;
document.body.appendChild(this.container);
this.paramsList = this.container.querySelector('.params-list');
// Close button
const closeBtn = this.container.querySelector('.close-btn');
closeBtn.addEventListener('click', () => {
this.hide();
});
}
}
Step 2.2: Update Parameters from Interpreter
updateWithLatestInterpreter() {
if (!window.interpreter || !window.interpreter.env) return;
// Clear existing
this.paramsList.innerHTML = '';
// Get all shapes
const shapes = window.interpreter.env.shapes;
shapes.forEach((shape, shapeName) => {
if (shape._consumedByBoolean) return;
// Create shape section
const shapeSection = document.createElement('div');
shapeSection.className = 'shape-section';
shapeSection.innerHTML = `<h4>${shapeName}</h4>`;
// Add parameter controls
const params = shape.params || {};
Object.keys(params).forEach(paramName => {
const control = this.createParameterControl(
shapeName,
paramName,
params[paramName],
this.getParamLimits(shape.type, paramName)
);
shapeSection.appendChild(control);
});
this.paramsList.appendChild(shapeSection);
});
}
getParamLimits(shapeType, paramName) {
// Define min/max/step for each parameter
const limits = {
circle: {
radius: { min: 1, max: 500, step: 1 }
},
rectangle: {
width: { min: 1, max: 500, step: 1 },
height: { min: 1, max: 500, step: 1 }
},
// ... add more
};
return limits[shapeType]?.[paramName] || { min: 0, max: 1000, step: 1 };
}
Step 2.3: Create Parameter Controls
createParameterControl(shapeName, paramName, value, limits) {
const control = document.createElement('div');
control.className = 'param-control';
control.style.cssText = 'margin: 10px 0;';
const label = document.createElement('label');
label.textContent = `${paramName}: `;
label.style.cssText = 'display: block; margin-bottom: 5px;';
const slider = document.createElement('input');
slider.type = 'range';
slider.min = limits.min;
slider.max = limits.max;
slider.step = limits.step;
slider.value = value;
slider.dataset.shapeName = shapeName;
slider.dataset.paramName = paramName;
slider.style.cssText = 'width: 100%;';
const numberInput = document.createElement('input');
numberInput.type = 'number';
numberInput.min = limits.min;
numberInput.max = limits.max;
numberInput.step = limits.step;
numberInput.value = value;
numberInput.dataset.shapeName = shapeName;
numberInput.dataset.paramName = paramName;
numberInput.style.cssText = 'width: 80px; margin-left: 10px;';
// Sync slider and number input
slider.addEventListener('input', (e) => {
const newValue = parseFloat(e.target.value);
numberInput.value = newValue;
this.onParameterChange(shapeName, paramName, newValue);
});
numberInput.addEventListener('input', (e) => {
const newValue = parseFloat(e.target.value);
slider.value = newValue;
this.onParameterChange(shapeName, paramName, newValue);
});
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display: flex; align-items: center;';
wrapper.appendChild(slider);
wrapper.appendChild(numberInput);
control.appendChild(label);
control.appendChild(wrapper);
return control;
}
onParameterChange(shapeName, paramName, value) {
if (this.shapeManager) {
this.shapeManager.updateShapeParameter(shapeName, paramName, value, 'slider');
}
}
Step 2.4: Add Show/Hide Methods
show() {
this.menuVisible = true;
this.container.style.display = 'block';
this.updateWithLatestInterpreter();
}
hide() {
this.menuVisible = false;
this.container.style.display = 'none';
}
toggle() {
if (this.menuVisible) {
this.hide();
} else {
this.show();
}
}
- [ ] Parameter changes update code
- [ ] Parameter changes update renderer
Common Issues and Fixes
Issue: Drag doesn't work
- Check
draggable="true"is set - Verify event listeners are attached
- Check
preventDefault()is called
Issue: Drop position wrong
- Check coordinate conversion (screen → world)
- Verify canvas offset is accounted for
- Check coordinate system is initialized
Issue: Code not inserted
- Check editor is registered
- Verify
insertCodeIntoEditor()is called - Check code generation template is correct
Issue: Parameters don't update
- Check shape manager is registered
- Verify
updateShapeParameter()is called - Check parameter names match shape params
Issue: UI doesn't show
- Check CSS display property
- Verify elements are appended to DOM
- Check z-index for layering