Building the Debug Visualizer From Scratch - Complete Beginner's Guide

What is a Debug Visualizer? - Much More Detailed Explanation

A debug visualizer is a development tool that overlays debug information on your canvas. Let's break this down completely:

Simple Definition: A debug visualizer shows you information about what's happening inside your program that you normally can't see. It draws extra information on top of your canvas so you can debug problems.

What "Overlays" Means:

  • Overlay = Drawing on top of something
  • Like putting a transparent sheet over a drawing
  • You see both the drawing AND the overlay information

Think of it like X-ray vision - Much More Detailed:

Normal Vision (What You Usually See):

  • You see shapes on the canvas (circles, rectangles, etc.)
  • You see what users see
  • Like looking at a finished building - you see the exterior

X-ray Vision (What Debug Visualizer Shows):

  • You see shapes PLUS extra debug information
  • You see what's happening inside the system
  • Like X-ray vision showing the building's internal structure (pipes, wires, supports)

What Debug Information Looks Like:

Normal View:

Canvas shows:
  ┌─────┐
  │  ○  │  ← Just a circle
  └─────┘

Debug View (with overlay):

Canvas shows:
  ┌─────┐  ← Green box = bounding box (shape bounds)
  │  ○  │  ← Circle shape
  │  ●  │  ← Blue dot = anchor point
  └─────┘
  ───────   ← Red line = edge
  FPS: 60   ← Performance info

Why You Need This:

Without Debug Visualizer:

  • Shape appears wrong → Why? You don't know!
  • Performance is slow → Why? Can't tell!
  • Constraint not working → Which one? Where? No idea!

With Debug Visualizer:

  • Shape appears wrong → See bounding box, see if it's in wrong position
  • Performance is slow → See FPS counter, see how many shapes/edges
  • Constraint not working → See anchor points, see constraint lines, see what's connected

Real-World Analogy - Much More Detailed:

Think of it like a car's diagnostic tool:

Normal View (What Driver Sees):

  • Car looks fine from outside
  • Engine runs (or doesn't)
  • Like: Car appears normal

Diagnostic Tool (What Mechanic Sees):

  • Engine temperature
  • Oil pressure
  • Error codes
  • Performance metrics
  • Like: See inside the engine, see what's wrong

Debug Visualizer = Diagnostic Tool for Code:

  • Shows internal state
  • Shows what's happening
  • Shows where problems are
  • Helps you fix issues

The Key Insight:

Debug visualizer is like having X-ray vision for your code - it shows you the internal workings that are normally invisible, helping you understand and fix problems!

Real-World Analogy:

Think of it like a car's diagnostic tool:

  • Normal view = What you see driving (shapes on canvas)
  • Debug view = What mechanics see (engine internals, diagnostic codes, performance metrics)
  • Debug visualizer = The diagnostic tool for your code

Why a Debug Visualizer? - Explained Simply

The Problem:

When building complex systems, you need to see what's happening internally:

  • Are shapes positioned correctly? - Are they where you think they are?
  • Are edges calculated properly? - Are the edges in the right places?
  • Are anchors in the right places? - Can constraints connect properly?
  • Are constraints solving correctly? - Are relationships maintained?
  • What's the performance like? - Is it running fast or slow?

The Solution:

The debug visualizer provides this visibility by drawing overlays that show internal state. It's like turning on debug mode in a game - you see hitboxes, paths, and other internal information.

Real-World Example:

Without debug visualizer:

  • Shape appears wrong - Why? You have no idea!
  • Performance is slow - Why? No way to tell!
  • Constraint not working - Which constraint? Where?

With debug visualizer:

  • Shape bounds shown - See exact position and size
  • FPS counter shown - See performance in real-time
  • Anchors and constraints shown - See why constraint fails

What to Visualize - Explained Simply

What Information Can the Debug Visualizer Show?

The debug visualizer can show various types of internal information:

  1. Shape Bounds (Bounding Boxes):

    • Rectangles showing the bounds of each shape
    • Used for collision detection
    • Shows exact position and size
    • Example: Green rectangle around each shape
  2. Edge Data:

    • Individual edges that make up shapes
    • Shows the actual line segments/arcs
    • Useful for debugging edge calculations
    • Example: Red lines showing all edges
  3. Anchor Points:

    • Points that constraints connect to
    • Shows where anchors are positioned
    • Useful for debugging constraints
    • Example: Blue dots showing all anchors
  4. Constraint Relationships:

    • Lines showing constraint connections
    • Shows which anchors are connected by constraints
    • Useful for debugging constraint solving
    • Example: Yellow lines connecting constrained anchors
  5. Performance Metrics:

    • FPS (frames per second)
    • Render times
    • Shape counts, edge counts
    • Useful for performance optimization
    • Example: FPS: 60, Shapes: 25, Edges: 100
  6. Coordinate System:

    • Grid lines
    • Origin marker
    • Axes (X and Y)
    • Useful for understanding coordinate transformations
    • Example: Grid overlay, red crosshair at origin

Visual Example:

Normal view:
  ┌─────────┐
  │  Circle │
  └─────────┘

Debug view:
  ┌─────────┐  ← Bounding box (green)
  │  Circle │
  │    ●    │  ← Anchor point (blue)
  └─────────┘
  ───────────  ← Edge (red)
  FPS: 60      ← Performance info (black box)

When to Use It - Explained Simply

When Should You Use the Debug Visualizer?

The debug visualizer is primarily a development tool (not for end users):

  1. During Development:

    • See what's happening as you build
    • Verify things work correctly
    • Catch bugs early
  2. When Debugging:

    • Find why something isn't working
    • See internal state that might be wrong
    • Understand what the code is actually doing
  3. When Optimizing:

    • Identify performance bottlenecks
    • See if too many shapes/edges
    • Check if rendering is efficient
  4. When Teaching:

    • Show how the system works internally
    • Visualize concepts (anchors, constraints, etc.)
    • Help others understand the codebase

Important: Usually hidden from end users, only enabled during development!

The Problem

You need to visualize:

  1. Shape bounds
  2. Edge data
  3. Anchor points
  4. Constraint relationships
  5. Performance metrics
  6. Coordinate system info

The Visualizer - Explained Simply

What is the Visualizer Class?

The DebugVisualizer class manages all debug visualization. It draws overlays on the canvas to show internal state.

The DebugVisualizer Class:

The constructor takes the canvas context and sets up all the properties needed for debugging.

Constructor Parameter:

  1. ctx - The canvas 2D context (needed to draw debug overlays, same context used for normal rendering)

State Properties:

  1. enabled - Whether debug visualization is active (false = off by default, true = on, can be toggled)
  2. frameCount - Counter for FPS calculation (increments each frame, resets every second)
  3. fps - Current frames per second (updated every second, shows rendering performance, 60 FPS is ideal)
  4. lastFpsUpdate - Timestamp of last FPS update (used to know when to update, updates every 1000ms)
  5. shapeCount - Number of shapes currently rendered (updated when drawing, useful for performance monitoring)
  6. edgeCount - Number of edges currently rendered (updated when drawing, useful for performance monitoring)

The setEnabled() Method:

Toggles debug visualization on or off. When enabled is true, debug overlays are drawn. When false, nothing is drawn. You don't want debug visualization running all the time (it's slow and cluttered), so this method lets you turn it on/off easily.

export class DebugVisualizer {
  constructor(ctx) {
    // Store canvas context for drawing
    this.ctx = ctx;

    // Enable/disable debug visualization
    this.enabled = false;

    // Performance tracking (FPS calculation)
    this.frameCount = 0;          // Counter that increments each frame
    this.fps = 0;                 // Current frames per second
    this.lastFpsUpdate = 0;       // Timestamp of last FPS update

    // Render statistics
    this.shapeCount = 0;          // Number of shapes currently rendered
    this.edgeCount = 0;           // Number of edges currently rendered
  }

  setEnabled(enabled) {
    // Toggle debug visualization on/off
    this.enabled = enabled;
  }
}

If you see an error at this step:

Error: TypeError: Cannot read property 'fillStyle' of null

  • What this means: ctx is null or not a valid canvas context
  • Common causes:
    1. ctx not passed: new DebugVisualizer(undefined)
    2. ctx is null: Canvas context is null
    3. Wrong context type: Passed wrong object instead of canvas context
  • Fix: Check ctx exists: if (!ctx || typeof ctx.fillStyle === 'undefined') throw new Error('Valid canvas context required');, verify ctx is canvas 2D context, ensure canvas.getContext('2d') was called successfully

Error: TypeError: this.setEnabled is not a function

  • What this means: setEnabled method not defined
  • Common causes:
    1. Method not implemented: setEnabled() not defined in class
    2. Method name typo: setEnable vs setEnabled
    3. Method in wrong scope: Defined outside class
  • Fix: Implement setEnabled() method, check method name spelling, ensure method is in class body

Error: Debug visualization doesn't appear (nothing drawn)

  • What this means: enabled is false or draw methods not called
  • Common causes:
    1. enabled is false: setEnabled(true) not called
    2. Draw methods not called: drawOverlay() not called from renderer
    3. Methods return early: drawOverlay() returns early when !enabled
  • Fix: Call setEnabled(true) to enable, ensure drawOverlay() is called from renderer, check drawOverlay() doesn't return early incorrectly

Error: FPS always 0 or NaN

  • What this means: Performance tracking not working
  • Common causes:
    1. frameCount not incremented: updatePerformanceMetrics() not called
    2. Performance.now() not available: Browser doesn't support it (very old browser)
    3. Time calculation wrong: lastFpsUpdate logic incorrect
  • Fix: Ensure updatePerformanceMetrics() is called each frame, check performance.now() is available, verify time calculation logic is correct

Drawing Overlay - Explained Simply

What is drawOverlay()?

drawOverlay() is the main entry point for drawing all debug information. It's called from the renderer each frame to draw debug overlays on top of the normal rendering.

The drawOverlay() Method:

This method coordinates all debug drawing. It calls different drawing methods to render different types of debug information.

Parameters:

  1. shapes - Map of all shapes (needed to draw shape bounds, edges, anchors, usually renderer.shapes)
  2. coordinateSystem - The coordinate system object (needed to draw grid, origin, axes, usually renderer.coordinateSystem)

Step-by-Step:

  1. Check if enabled: If debug visualization is off, do nothing. Early return to skip all drawing (performance optimization).
  2. Save canvas state: Saves current canvas drawing state (colors, line styles, transforms, etc.) so we can modify it for debug drawing.
  3. Draw shape bounds: Draws bounding boxes around shapes, showing exact position and size.
  4. Draw coordinate system: Draws grid, origin marker, axes, showing coordinate system information.
  5. Draw performance info: Draws FPS, shape count, edge count, showing performance metrics.
  6. Restore canvas state: Restores canvas state to what it was before, ensuring our debug drawing doesn't affect normal rendering.

Why Save/Restore Canvas State?

Canvas state includes fill/stroke styles, line widths, transformations, line dash patterns, etc. We modify these for debug drawing. Without save/restore, our changes would affect normal rendering. With save/restore, we can modify state safely and restore it afterward.

drawOverlay(shapes, coordinateSystem) {
  // Early return if debug visualization is disabled (performance optimization)
  if (!this.enabled) return;

  // Save canvas state so we can modify it for debug drawing
  this.ctx.save();

  // Draw different types of debug information
  this.drawShapeBounds(shapes);              // Bounding boxes around shapes
  this.drawCoordinateSystem(coordinateSystem); // Grid, origin, axes
  this.drawPerformanceInfo();                // FPS, shape count, edge count

  // Restore canvas state (critical - prevents debug drawing from affecting normal rendering)
  this.ctx.restore();
}

If you see an error at this step:

Error: TypeError: Cannot read property 'forEach' of undefined

  • What this means: shapes is undefined or not a Map/iterable
  • Common causes:
    1. shapes not passed: Called drawOverlay(undefined, coordinateSystem)
    2. shapes is null: Shapes parameter is null
    3. shapes is not Map: Shapes is object/array instead of Map
  • Fix: Check shapes exists: if (!shapes) return;, verify shapes is Map or iterable, ensure shapes passed correctly

Error: TypeError: Cannot read property 'worldToScreen' of undefined

  • What this means: coordinateSystem is undefined or doesn't have worldToScreen method
  • Common causes:
    1. coordinateSystem not passed: Called drawOverlay(shapes, undefined)
    2. coordinateSystem is null: Parameter is null
    3. Wrong object: Passed wrong object instead of coordinateSystem
  • Fix: Check coordinateSystem exists: if (!coordinateSystem) return;, verify coordinateSystem has required methods, ensure correct object passed

Error: Debug drawing affects normal rendering (colors/styles wrong)

  • What this means: Canvas state not restored (missing restore())
  • Common causes:
    1. restore() not called: Missing this.ctx.restore();
    2. restore() in wrong place: Called too early or not at end
    3. Multiple saves: More saves than restores (unbalanced)
  • Fix: Always call restore() after save(), ensure restore() is at end of method, match saves with restores (one restore per save)

Error: TypeError: this.drawShapeBounds is not a function

  • What this means: drawShapeBounds method not defined
  • Common causes:
    1. Method not implemented: drawShapeBounds() not defined in class
    2. Method name typo: drawShapeBound vs drawShapeBounds
    3. Method in wrong scope: Defined outside class
  • Fix: Implement drawShapeBounds() method, check method name spelling, ensure method is in class body

Shape Bounds - Explained Simply

What are Shape Bounds?

Shape bounds are the bounding boxes (rectangles) that contain each shape. Think of it like a box around an object - the smallest rectangle that completely contains the shape.

Why Draw Bounds?

Bounding boxes are useful for:

  • Collision detection - Check if shapes overlap
  • Debugging - See exact position and size
  • Understanding layout - Visualize where shapes actually are

The drawShapeBounds() Method:

This method draws a green dashed rectangle around each shape, plus the shape's name. It iterates through all shapes, calculates bounds, and draws them with appropriate styling.

Step-by-Step:

  1. Loop through shapes: Iterate through all shapes (shape object and name like "c1", "r1").
  2. Skip shapes without transform: Some shapes might not have transform (invalid shapes), skip them to avoid errors.
  3. Calculate bounds: Calculate the bounding box for the shape, returns { x, y, width, height }.
  4. Set drawing style: Set color to green, line width to 1 pixel, dashed line pattern (5px dash, 5px gap).
  5. Draw the rectangle: Draw the bounding box rectangle using strokeRect() (just the outline, not filled).
  6. Reset line dash: Reset line dash to solid (empty array = solid line) so other drawing isn't affected.
  7. Draw shape name: Draw shape name above the bounds using green fill and monospace font.
drawShapeBounds(shapes) {
  // Loop through all shapes
  shapes.forEach((shape, name) => {
    // Skip shapes without transform (invalid shapes)
    if (!shape.transform) return;

    // Calculate bounding box for the shape
    const bounds = this.calculateBounds(shape);

    // Set drawing style for bounds (green dashed line)
    this.ctx.strokeStyle = '#00FF00';    // Green color
    this.ctx.lineWidth = 1;              // 1 pixel wide
    this.ctx.setLineDash([5, 5]);        // Dashed line (5px dash, 5px gap)

    // Draw the bounding box rectangle (just outline, not filled)
    this.ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);

    // Reset line dash to solid (empty array = solid line)
    this.ctx.setLineDash([]);

    // Draw shape name above bounds
    this.ctx.fillStyle = '#00FF00';      // Green color (same as bounds)
    this.ctx.font = '10px monospace';    // Fixed-width font for readability
    this.ctx.fillText(name, bounds.x, bounds.y - 5);  // 5 pixels above bounds
  });
}

Visual Example:

Shape: Circle at (100, 100) with radius 50

Bounds calculated:
  x: 50 (centerX - radius)
  y: 50 (centerY - radius)
  width: 100 (radius * 2)
  height: 100 (radius * 2)

Drawn:
  ┌─────────────────┐ ← Dashed green rectangle
  │                 │
  │      Circle     │
  │        ●        │ ← Actual shape (not drawn here, just shown)
  │                 │
  └─────────────────┘
  c1                ← Green text showing shape name

If you see an error at this step:

Error: TypeError: shapes.forEach is not a function

  • What this means: shapes is not a Map or doesn't have forEach
  • Common causes:
    1. shapes is object: Passed plain object {} instead of Map
    2. shapes is array: Passed array instead of Map
    3. shapes is null/undefined: Shapes parameter is invalid
  • Fix: Check shapes is Map: if (!(shapes instanceof Map)) throw new Error('shapes must be Map');, convert to Map if needed, ensure shapes is Map type

Error: TypeError: this.calculateBounds is not a function

  • What this means: calculateBounds method not defined
  • Common causes:
    1. Method not implemented: calculateBounds() not defined in class
    2. Method name typo: calculateBound vs calculateBounds
    3. Method in wrong scope: Defined outside class
  • Fix: Implement calculateBounds() method, check method name spelling, ensure method is in class body

Error: TypeError: Cannot read property 'x' of undefined

  • What this means: calculateBounds() returned undefined or bounds object invalid
  • Common causes:
    1. calculateBounds() returns undefined: Method doesn't return value
    2. calculateBounds() returns wrong format: Returns object without x/y/width/height
    3. Shape has no points: Shape is invalid, bounds can't be calculated
  • Fix: Check calculateBounds() returns object: { x, y, width, height }, verify method returns value, handle invalid shapes (return null/undefined and check)

Error: Bounds drawn in wrong position (not around shape)

  • What this means: Bounds calculation or coordinate conversion is wrong
  • Common causes:
    1. Coordinate system: Bounds in world coords but drawing in screen coords (need conversion)
    2. Calculation wrong: calculateBounds() uses wrong formula
    3. Transform not applied: Shape transform not considered in bounds
  • Fix: Convert bounds to screen coordinates: coordinateSystem.worldToScreen(bounds.x, bounds.y), verify bounds calculation formula, ensure transform is considered

Error: Text not appearing (shape names not drawn)

  • What this means: fillText() not drawing or text is off-screen
  • Common causes:
    1. fillStyle not set: fillStyle is transparent or not set before fillText()
    2. Text off-screen: Coordinates put text outside canvas bounds
    3. Font not set: Font is invalid or not loaded
  • Fix: Ensure fillStyle is set before fillText(), check text coordinates are on-screen, verify font is valid: '10px monospace'

Coordinate System

Draw grid and axes. We convert the origin from world coordinates to screen coordinates and draw a crosshair there. If the grid is enabled, we also draw the grid.

drawCoordinateSystem(coordinateSystem) {
  // Convert origin (0, 0) from world coordinates to screen coordinates
  const origin = coordinateSystem.worldToScreen(0, 0);

  // Draw red crosshair at origin (20px size)
  this.drawCrossHairs(origin.x, origin.y, 20, '#FF0000');

  // Draw grid if enabled
  if (coordinateSystem.isGridEnabled) {
    this.drawGrid(coordinateSystem);
  }
}

Performance Info - Explained Simply

What is Performance Info?

Performance info shows metrics about how well the system is running - FPS (frames per second), number of shapes, number of edges, etc. It's like a speedometer for your code.

Why Show Performance Info?

Performance info helps you:

  • Monitor performance - See if rendering is fast or slow
  • Identify bottlenecks - Find what's causing slowdowns
  • Optimize - Know what to optimize (too many shapes? too many edges?)

The drawPerformanceInfo() Method:

This method draws a semi-transparent black box in the top-left corner with performance statistics (FPS, shape count, edge count).

Step-by-Step:

  1. Update metrics: Updates FPS, shape count, edge count (called each frame to keep metrics current).
  2. Draw background box: Draws semi-transparent black rectangle at (10, 10) with width 200, height 100 (creates readable background for text).
  3. Set text style: Sets text color to white, font to 12px monospace, alignment to left.
  4. Draw FPS: Draws FPS text at x=20, y=30, formats to 1 decimal place using .toFixed(1).
  5. Draw shape count: Moves y down 20 pixels, draws shape count text.
  6. Draw edge count: Moves y down 20 more pixels, draws edge count text.

The updatePerformanceMetrics() Method:

Calculates FPS by counting frames over time. Increments frame counter each frame, checks if 1000ms has passed, then updates FPS to the frame count and resets the counter.

drawPerformanceInfo() {
  // Update performance metrics first (FPS calculation, etc.)
  this.updatePerformanceMetrics();

  // Draw semi-transparent black background box (makes text readable)
  this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
  this.ctx.fillRect(10, 10, 200, 100);

  // Set text style (white, monospace font, left-aligned)
  this.ctx.fillStyle = '#FFFFFF';
  this.ctx.font = '12px monospace';
  this.ctx.textAlign = 'left';

  // Draw performance metrics, one line at a time (20px spacing)
  let y = 30;
  this.ctx.fillText(`FPS: ${this.fps.toFixed(1)}`, 20, y);  // Format to 1 decimal place
  y += 20;
  this.ctx.fillText(`Shapes: ${this.shapeCount}`, 20, y);
  y += 20;
  this.ctx.fillText(`Edges: ${this.edgeCount}`, 20, y);
}

updatePerformanceMetrics() {
  // Increment frame counter (called once per frame)
  this.frameCount++;

  // Get current time using high-resolution timer
  const now = performance.now();

  // Update FPS every second (1000ms)
  if (now - this.lastFpsUpdate > 1000) {
    // Set FPS to number of frames counted in this second
    this.fps = this.frameCount;

    // Reset counter and update timestamp for next second
    this.frameCount = 0;
    this.lastFpsUpdate = now;
  }
}

How FPS Calculation Works:

Frame 1: frameCount = 1, time = 0ms
Frame 2: frameCount = 2, time = 16ms (16ms since start)
Frame 3: frameCount = 3, time = 32ms
...
Frame 60: frameCount = 60, time = 1000ms
  → 1000ms has passed! Update FPS:
  → fps = 60 (60 frames in 1 second)
  → frameCount = 0 (reset)
  → lastFpsUpdate = 1000ms

Frame 61: frameCount = 1, time = 1016ms
Frame 62: frameCount = 2, time = 1032ms
...

If you see an error at this step:

Error: TypeError: performance.now is not a function

  • What this means: performance.now() not available (very old browser)
  • Common causes:
    1. Old browser: Browser doesn't support performance API
    2. Wrong object: performance object doesn't exist or is different
    3. Context issue: Running in wrong context (worker without performance)
  • Fix: Check performance exists: if (typeof performance === 'undefined' || typeof performance.now !== 'function') { use Date.now() instead; }, use polyfill if needed, ensure browser supports performance API

Error: FPS always shows 0

  • What this means: updatePerformanceMetrics() not being called or frameCount not incrementing
  • Common causes:
    1. Method not called: updatePerformanceMetrics() not called in drawPerformanceInfo()
    2. Time check wrong: Condition now - this.lastFpsUpdate > 1000 never true
    3. frameCount not incrementing: frameCount++ not executing
  • Fix: Ensure updatePerformanceMetrics() is called, check time calculation, verify frameCount increments

Error: FPS shows NaN

  • What this means: frameCount or fps has invalid value
  • Common causes:
    1. frameCount is NaN: Something set frameCount to NaN
    2. fps calculation wrong: Division or calculation resulting in NaN
    3. Initialization wrong: frameCount/fps not initialized to 0
  • Fix: Initialize frameCount and fps to 0 in constructor, verify calculations don't produce NaN, check frameCount is number

Error: Performance box not visible (black box not drawn)

  • What this means: fillRect() not drawing or coordinates off-screen
  • Common causes:
    1. fillStyle not set: fillStyle is transparent or wrong
    2. Coordinates wrong: fillRect() coordinates put box off-screen
    3. Drawing order: Drawn before or after other things that cover it
  • Fix: Verify fillStyle is set before fillRect(), check coordinates are on-screen (10, 10, 200, 100), ensure drawing order is correct (draw background first, then text)

Error: Text not visible (white text not showing)

  • What this means: fillText() not drawing or text color wrong
  • Common causes:
    1. fillStyle not set: fillStyle not set to white before fillText()
    2. Text coordinates wrong: Text drawn off-screen or outside box
    3. Font not loaded: Font string invalid
  • Fix: Ensure fillStyle is '#FFFFFF' before fillText(), check text coordinates are within canvas bounds, verify font string is valid: '12px monospace'

Debug Points

Draw points with labels. Useful for marking specific coordinates or important locations. The point is drawn as a small filled circle, and if a label is provided, it's drawn next to the point with a semi-transparent background for readability.

drawDebugPoint(x, y, label = '', color = '#FF0000') {
  // Only draw if debug visualization is enabled
  if (!this.enabled) return;

  this.ctx.save();

  // Draw the point as a small filled circle (3px radius)
  this.ctx.fillStyle = color;
  this.ctx.beginPath();
  this.ctx.arc(x, y, 3, 0, Math.PI * 2);
  this.ctx.fill();

  // Draw label if provided
  if (label) {
    // Draw semi-transparent black background for label (makes text readable)
    this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
    this.ctx.fillRect(x + 5, y - 8, label.length * 6 + 4, 16);

    // Draw label text in the same color as the point
    this.ctx.fillStyle = color;
    this.ctx.font = '10px monospace';
    this.ctx.textAlign = 'left';
    this.ctx.fillText(label, x + 7, y);
  }

  this.ctx.restore();
}

Common Gotchas

Gotcha 1: Performance impact Debug drawing can be slow. Only draw when enabled, and keep it simple.

Gotcha 2: Coordinate conversion Debug info might be in world or screen coordinates. Convert properly.

Gotcha 3: Overlay z-order Debug overlays should be on top. Draw them last, or use separate canvas.

Gotcha 4: Text rendering Canvas text can be slow. Limit text rendering, use simple fonts.

Gotcha 5: Update frequency Don't update performance metrics every frame. Throttle to once per second.

How to Build the Debug Visualizer - Complete Step-by-Step Guide

This section provides a complete guide for building the debug visualizer from scratch.

Prerequisites

Before building the debug visualizer, you need:

  • A working renderer system
  • Understanding of canvas drawing
  • Access to shape data and coordinate system

Step 1: Create the Debug Visualizer Class

File: src/renderer/debugVisualizer.mjs

export class DebugVisualizer {
  constructor(ctx) {
    this.ctx = ctx;
    this.enabled = false;

    // Performance tracking
    this.frameCount = 0;
    this.fps = 0;
    this.lastFpsUpdate = performance.now();
    this.lastFrameTime = performance.now();

    // Statistics
    this.shapeCount = 0;
    this.edgeCount = 0;
  }

  setEnabled(enabled) {
    this.enabled = enabled;
  }
}

Step 2: Implement Main Draw Overlay Method

drawOverlay(shapes, coordinateSystem) {
  if (!this.enabled) return;

  this.ctx.save();

  // Update statistics
  this.shapeCount = shapes ? shapes.size : 0;

  // Draw various debug overlays
  this.drawShapeBounds(shapes, coordinateSystem);
  this.drawCoordinateSystemInfo(coordinateSystem);
  this.drawPerformanceInfo();

  this.ctx.restore();
}

Step 3: Implement Shape Bounds Drawing

drawShapeBounds(shapes, coordinateSystem) {
  if (!shapes) return;

  this.ctx.strokeStyle = '#FF0000';
  this.ctx.lineWidth = 1;
  this.ctx.setLineDash([2, 2]);

  shapes.forEach((shape, shapeName) => {
    const bounds = this.getShapeBounds(shape, coordinateSystem);
    if (!bounds) return;

    this.ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);

    // Draw shape name
    this.ctx.fillStyle = '#FF0000';
    this.ctx.font = '10px monospace';
    this.ctx.fillText(shapeName, bounds.x, bounds.y - 5);
  });

  this.ctx.setLineDash([]);
}

getShapeBounds(shape, coordinateSystem) {
  // Get shape bounds in world coordinates
  const transform = shape.transform || {
    position: [shape.params.x || 0, shape.params.y || 0],
    rotation: 0,
    scale: [1, 1]
  };

  // Calculate bounds based on shape type
  let bounds = null;

  if (shape.type === 'circle') {
    const radius = (shape.params.radius || 50) * (transform.scale[0] || 1);
    bounds = {
      x: transform.position[0] - radius,
      y: transform.position[1] - radius,
      width: radius * 2,
      height: radius * 2
    };
  } else if (shape.type === 'rectangle') {
    const width = (shape.params.width || 100) * (transform.scale[0] || 1);
    const height = (shape.params.height || 100) * (transform.scale[1] || 1);
    bounds = {
      x: transform.position[0] - width / 2,
      y: transform.position[1] - height / 2,
      width: width,
      height: height
    };
  }

  // Convert to screen coordinates
  if (bounds) {
    const topLeft = coordinateSystem.worldToScreen(bounds.x, bounds.y);
    const bottomRight = coordinateSystem.worldToScreen(
      bounds.x + bounds.width,
      bounds.y + bounds.height
    );
    return {
      x: topLeft.x,
      y: topLeft.y,
      width: bottomRight.x - topLeft.x,
      height: bottomRight.y - topLeft.y
    };
  }

  return null;
}

Step 4: Implement Coordinate System Info

drawCoordinateSystemInfo(coordinateSystem) {
  const ctx = this.ctx;

  // Draw origin marker
  const origin = coordinateSystem.worldToScreen(0, 0);
  ctx.strokeStyle = '#00FF00';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(origin.x - 10, origin.y);
  ctx.lineTo(origin.x + 10, origin.y);
  ctx.moveTo(origin.x, origin.y - 10);
  ctx.lineTo(origin.x, origin.y + 10);
  ctx.stroke();

  // Draw axis labels
  ctx.fillStyle = '#00FF00';
  ctx.font = '12px monospace';
  ctx.fillText('(0,0)', origin.x + 5, origin.y - 5);

  // Draw grid info
  ctx.fillStyle = '#666666';
  ctx.fillText(
    `Scale: ${coordinateSystem.scale.toFixed(2)}`,
    10,
    20
  );
  ctx.fillText(
    `Pan: (${coordinateSystem.panX.toFixed(1)}, ${coordinateSystem.panY.toFixed(1)})`,
    10,
    35
  );
}

Step 5: Implement Performance Metrics

drawPerformanceInfo() {
  this.updatePerformanceMetrics();

  const ctx = this.ctx;

  // Draw background
  ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
  ctx.fillRect(10, 10, 200, 100);

  // Draw text
  ctx.fillStyle = '#FFFFFF';
  ctx.font = '12px monospace';
  ctx.textAlign = 'left';

  let y = 30;
  ctx.fillText(`FPS: ${this.fps.toFixed(1)}`, 20, y);
  y += 20;
  ctx.fillText(`Shapes: ${this.shapeCount}`, 20, y);
  y += 20;
  ctx.fillText(`Edges: ${this.edgeCount}`, 20, y);
  y += 20;
  ctx.fillText(`Frame: ${this.frameTime.toFixed(2)}ms`, 20, y);
}

updatePerformanceMetrics() {
  this.frameCount++;
  const now = performance.now();

  // Calculate frame time
  this.frameTime = now - this.lastFrameTime;
  this.lastFrameTime = now;

  // Update FPS every second
  if (now - this.lastFpsUpdate > 1000) {
    this.fps = this.frameCount;
    this.frameCount = 0;
    this.lastFpsUpdate = now;
  }
}

Step 6: Add Debug Point Drawing

drawDebugPoint(x, y, label = '', color = '#FF0000') {
  if (!this.enabled) return;

  const screen = this.coordinateSystem.worldToScreen(x, y);

  this.ctx.save();

  // Draw point
  this.ctx.fillStyle = color;
  this.ctx.beginPath();
  this.ctx.arc(screen.x, screen.y, 3, 0, Math.PI * 2);
  this.ctx.fill();

  // Draw label
  if (label) {
    this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
    this.ctx.fillRect(screen.x + 5, screen.y - 8, label.length * 6 + 4, 16);

    this.ctx.fillStyle = color;
    this.ctx.font = '10px monospace';
    this.ctx.textAlign = 'left';
    this.ctx.fillText(label, screen.x + 7, screen.y);
  }

  this.ctx.restore();
}

Step 7: Integrate with Renderer

In renderer.mjs:

import { DebugVisualizer } from './renderer/debugVisualizer.mjs';

export class Renderer {
  constructor(canvas) {
    // ... existing code ...
    this.debugVisualizer = new DebugVisualizer(this.ctx);
  }

  redraw() {
    // ... existing drawing code ...

    // Draw debug overlays last (on top)
    if (this.debugVisualizer.enabled) {
      this.debugVisualizer.drawOverlay(this.shapes, this.coordinateSystem);
    }
  }

  toggleDebug() {
    this.debugVisualizer.setEnabled(!this.debugVisualizer.enabled);
    this.redraw();
  }
}

Step 8: Add Keyboard Shortcut

In app.js:

document.addEventListener('keydown', (e) => {
  if (e.key === 'd' && e.ctrlKey) {
    e.preventDefault();
    renderer.toggleDebug();
  }
});

Common Issues and Fixes

Issue: Debug overlays don't appear

  • Check enabled flag is set
  • Verify drawOverlay() is called in redraw
  • Check overlays are drawn after shapes

Issue: Performance metrics wrong

  • Check frame counting logic
  • Verify timing calculations
  • Check update frequency (should be 1 second)

Issue: Bounds incorrect

  • Check coordinate conversion (world → screen)
  • Verify bounds calculation includes transforms
  • Check shape type handling

Issue: Text overlaps

  • Adjust text positioning
  • Use background rectangles for readability
  • Check font size and spacing

Implementation Adjustments and Notes

This section documents specific adjustments that were needed after the initial implementation, based on practical experience. These changes address real-world issues encountered when integrating the debug visualizer into the actual rendering pipeline.

Passing Debug Visualizer Through the Rendering Pipeline

Issue: The initial implementation had the debug visualizer isolated in the renderer, but shape manager triggers many redraws during parameter updates. Without passing the debug visualizer through, debug overlays wouldn't appear during these updates, making debug mode appear broken.

What Was Added: Debug visualizer parameter passing through all redraw methods and storage in shape manager.

Specific Implementation Changes:

1. Added Debug Visualizer Storage in Shape Manager:

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

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

2. Updated immediateVisualUpdate Method Signature:

// BEFORE: No debug visualizer parameter
immediateVisualUpdate() {
    if (!this.renderer) return;

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

// AFTER: Accepts optional debug visualizer parameter
immediateVisualUpdate(debugVisualizer = null) {
    if (!this.renderer) return;

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

3. Updated updateShapeParameter to Pass Debug Visualizer:

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

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

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

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

4. Updated Renderer Methods to Accept Debug Visualizer:

// In renderer.mjs - All methods updated to accept debug visualizer

// Updated redraw method
redraw(debugVisualizer = null) {
    // ... clear canvas, draw background, draw grid ...

    // Draw shapes
    if (this.shapes) {
        let shapeCount = 0;
        let edgeCount = 0;

        for (const [name, shape] of this.shapes) {
            // ... draw shape ...
            shapeCount++;
            // ... count edges ...
        }

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

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

// Updated setHoveredShape method
setHoveredShape(shapeName, debugVisualizer = null) {
    this.hoveredShape = shapeName;
    // Pass debug visualizer to redraw
    this.redraw(debugVisualizer || window.debugVisualizer);
}

// Updated setSelectedShape method
setSelectedShape(shapeName, debugVisualizer = null) {
    this.selectedShape = shapeName;
    // Pass debug visualizer to redraw
    this.redraw(debugVisualizer || window.debugVisualizer);
}

// Updated clear method
clear(debugVisualizer = null) {
    this.redraw(debugVisualizer || window.debugVisualizer);
}

5. Registration in app.js:

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

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

Why These Changes Were Necessary:

  1. Parameter Updates: When users adjust sliders or drag shapes, shape manager triggers updateShapeParameter(), which calls immediateVisualUpdate(). Without passing the debug visualizer, these updates wouldn't show debug overlays.
  2. Multiple Entry Points: Redraws are triggered from many places (shape updates, hover changes, selection changes). Each needs to pass the debug visualizer.
  3. Global Fallback: Methods also check window.debugVisualizer as a fallback, ensuring debug visualizer is available even if not explicitly passed.
  4. State Updates: Debug visualizer needs to update its statistics (shape count, edge count) during redraws. Passing it through ensures it has access to current rendering state.

What Happened Without These Changes:

  • Debug overlays would only appear on initial render
  • When shapes were updated via sliders, debug overlays would disappear
  • Performance metrics (FPS, counts) wouldn't update correctly
  • Debug mode would appear "broken" because overlays weren't consistent

Global Storage for Cross-Component Access

Issue: Components like InteractionHandler need to trigger redraws with debug visualization, but they don't have direct access to the debug visualizer instance. Passing it through constructors would require threading it through many layers.

What Was Added: Global storage pattern using window.debugVisualizer with fallback checks in all methods that need it.

Specific Implementation:

1. Global Storage in app.js:

// Initialize debug visualizer early (after renderer is created)
if (renderer.coordinateSystem) {
    debugVisualizer = new DebugVisualizer(canvas.getContext('2d'), renderer.coordinateSystem);
    debugVisualizer.setEnabled(debugEnabled);

    // NEW: Store globally for cross-component access
    window.debugVisualizer = debugVisualizer;

    // Also register with shape manager
    shapeMgr.setDebugVisualizer(debugVisualizer);
}

2. Usage in InteractionHandler:

// In interactionHandler.mjs - updateHoverState method
updateHoverState(x, y) {
    const hitShape = this.findShapeAtPoint(x, y);

    // Get debug visualizer from global (since InteractionHandler doesn't have direct reference)
    const debugViz = window.debugVisualizer || null;

    if (hitShape && hitShape.name !== this.hoveredShape) {
        this.hoveredShape = hitShape.name;
        if (this.renderer.setHoveredShape) {
            // Pass debug visualizer to renderer method
            this.renderer.setHoveredShape(this.hoveredShape, debugViz);
        }
    } else if (!hitShape && this.hoveredShape) {
        this.hoveredShape = null;
        if (this.renderer.setHoveredShape) {
            // Pass debug visualizer even when clearing hover
            this.renderer.setHoveredShape(null, debugViz);
        }
    }
}

3. Fallback Pattern in Renderer Methods:

// In renderer.mjs - All methods check global as fallback
setHoveredShape(shapeName, debugVisualizer = null) {
    this.hoveredShape = shapeName;
    // Use parameter if provided, otherwise try global, otherwise null
    const dv = debugVisualizer || window.debugVisualizer || null;
    this.redraw(dv);
}

setSelectedShape(shapeName, debugVisualizer = null) {
    this.selectedShape = shapeName;
    const dv = debugVisualizer || window.debugVisualizer || null;
    this.redraw(dv);
}

clear(debugVisualizer = null) {
    const dv = debugVisualizer || window.debugVisualizer || null;
    this.redraw(dv);
}

4. Usage in Grid Toggle:

// Grid toggle button handler
const gridToggle = document.getElementById('grid-toggle');
if (gridToggle && renderer) {
    gridToggle.addEventListener('click', () => {
        if (renderer.coordinateSystem) {
            renderer.coordinateSystem.isGridEnabled = !renderer.coordinateSystem.isGridEnabled;
            // Pass debug visualizer to redraw
            renderer.redraw(debugVisualizer || window.debugVisualizer);
        }
    });
}

5. Usage in Debug Toggle Keyboard Shortcut:

// Debug visualizer toggle (Ctrl+D)
document.addEventListener('keydown', (e) => {
    if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
        e.preventDefault();
        debugEnabled = !debugEnabled;

        // Use global reference (debugVisualizer may not be in scope)
        const dv = debugVisualizer || window.debugVisualizer;
        if (dv) {
            dv.setEnabled(debugEnabled);
            if (renderer) {
                // Pass to redraw
                renderer.redraw(dv);
            }
        }
    }
});

Why This Pattern Was Necessary:

  1. Cross-Module Access: InteractionHandler is a separate module that doesn't have direct access to debugVisualizer created in app.js. Global storage bridges this gap.
  2. Optional Parameter: Methods accept debugVisualizer as optional parameter, but need fallback when not provided. Global storage provides that fallback.
  3. Event Handlers: Event handlers (like keyboard shortcuts, button clicks) may execute in contexts where local variables aren't available. Global storage ensures access.
  4. Flexibility: Allows passing debug visualizer explicitly when available, but falls back to global when not, providing flexibility without breaking functionality.

What Happened Without Global Storage:

  • InteractionHandler couldn't pass debug visualizer to renderer methods
  • Debug overlays wouldn't appear during hover/selection changes
  • Keyboard shortcut toggle wouldn't work
  • Methods would have to be called without debug visualizer, breaking debug mode functionality

Shape Count and Edge Count Updates

Issue: Debug visualizer needs accurate shape and edge counts, but shapes may not support getPoints().

Solution: Add defensive checks when counting edges:

// In renderer.mjs redraw()
for (const [name, shape] of this.shapes) {
    if (shape._consumedByBoolean) continue;

    shapeCount++;

    // Count edges safely
    if (shape.getPoints && typeof shape.getPoints === 'function') {
        try {
            const points = shape.getPoints();
            if (Array.isArray(points)) {
                edgeCount += points.length;
            }
        } catch (e) {
            // Shape doesn't support getPoints, skip
        }
    }
}

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

Why: Not all shapes support getPoints(). Defensive programming prevents crashes and allows graceful handling of different shape types.

Coordinate System Reference

Issue: Debug visualizer needs to convert between world coordinates (where shapes exist) and screen coordinates (where pixels are) to draw overlays. The initial implementation didn't store the coordinate system reference, so coordinate conversions weren't possible.

What Was Added: Coordinate system reference storage in constructor and usage throughout overlay drawing methods.

Specific Implementation:

1. Constructor Update:

// In renderer/debugVisualizer.mjs
export class DebugVisualizer {
    constructor(ctx, coordinateSystem) {
        this.ctx = ctx;

        // NEW: Store coordinate system reference for coordinate conversions
        // This is needed for converting shape positions from world to screen coordinates
        this.coord = coordinateSystem;

        this.enabled = false;

        // Performance tracking
        this.frameCount = 0;
        this.fps = 0;
        this.lastFpsUpdate = Date.now();

        // Render statistics
        this.shapeCount = 0;
        this.edgeCount = 0;
    }
}

2. Usage in Coordinate Conversions:

// In getShapeBounds() method
getShapeBounds(shape, coordinateSystem) {
    // Get shape bounds in world coordinates
    const transform = shape.transform || { position: [0, 0], rotation: 0, scale: [1, 1] };
    const [px, py] = transform.position || [0, 0];

    // Calculate world bounds based on shape type
    let size = 50;
    if (shape.params) {
        if (shape.params.radius) {
            size = shape.params.radius;
        } else if (shape.params.width && shape.params.height) {
            size = Math.max(shape.params.width || 0, shape.params.height || 0) / 2;
        }
    }

    // Convert world coordinates to screen coordinates using coordinate system
    // This is where stored coordinateSystem reference is used
    const center = coordinateSystem.worldToScreen(px, py);
    const screenSize = size * coordinateSystem.scale;

    return {
        minX: center.x - screenSize,
        minY: center.y - screenSize,
        maxX: center.x + screenSize,
        maxY: center.y + screenSize
    };
}

// In drawCoordinateSystemInfo() method
drawCoordinateSystemInfo(coordinateSystem) {
    // Convert origin (0, 0) from world to screen coordinates
    // coordinateSystem reference is passed as parameter, but could use this.coord
    const origin = coordinateSystem.worldToScreen(0, 0);

    // Draw origin marker at screen position
    this.ctx.fillStyle = '#0066ff';
    this.ctx.font = '10px monospace';
    this.ctx.fillText('Origin', origin.x + 10, origin.y - 10);

    // Display scale information (from coordinate system)
    this.ctx.fillStyle = '#666';
    this.ctx.fillText(`Scale: ${coordinateSystem.scale.toFixed(2)}x`, 10, 40);
}

3. Initialization in app.js:

// Initialize debug visualizer with coordinate system reference
if (renderer.coordinateSystem) {
    // Pass coordinate system so debug visualizer can do coordinate conversions
    debugVisualizer = new DebugVisualizer(
        canvas.getContext('2d'), 
        renderer.coordinateSystem  // Coordinate system reference
    );
    debugVisualizer.setEnabled(debugEnabled);
    window.debugVisualizer = debugVisualizer;
}

Why This Was Necessary:

  1. Coordinate Conversion: Debug overlays (bounds, origin marker) need to be drawn at correct screen positions. Shapes exist in world coordinates (mm), but canvas drawing uses screen coordinates (pixels). The coordinate system handles this conversion.

  2. Scale Information: Debug visualizer displays scale information from the coordinate system. Without the reference, it couldn't access this information.

  3. Consistent Reference: By storing the reference, all overlay drawing methods can use the same coordinate system instance, ensuring consistent conversions.

What Happened Without This:

  • Debug overlays would be drawn at wrong positions (not matching shapes)
  • Origin marker would appear at wrong location
  • Bounds rectangles wouldn't align with actual shapes
  • Scale information couldn't be displayed
  • Coordinate conversions would fail or use wrong values

results matching ""

    No results matching ""