RESIZE FIX COMPLETE

Comprehensive guidelines for AI-assisted development within the Editor platform

Resize Coordinate System Fix - Complete ✅

🔴 Problem: SVG Sliding Instead of Resizing

Symptoms

When resizing elements (especially from top, left, or corner handles), the SVG shapes would slide/move around the canvas instead of smoothly scaling in place. The shapes appeared to "jump" or "drift" during resize operations.

Root Cause: Coordinate System Confusion

The Universal Visual Compositor uses two coordinate spaces:

  1. Canvas Space (absolute): Where elements are positioned on the canvas
  2. Element Space (relative): Where SVG shapes are defined within their element

The bug was caused by mixing these coordinate spaces in the resize event data.


🎯 Understanding the Coordinate System

Architecture Design

interface VisualElement {
  position: Point;  // Canvas-absolute coordinates (e.g., {x: 100, y: 100})
  bounds: Bounds;   // Element-relative, always starts at {x: 0, y: 0, width: X, height: Y}
  shapes: VectorShape[];  // Coordinates relative to bounds (0-based)
}

Example: Blue Circle at (100, 100)

{
  id: 'circle-1',
  position: { x: 100, y: 100 },  // Canvas position
  bounds: { x: 0, y: 0, width: 100, height: 100 },  // Always 0,0 origin
  shapes: [{
    type: 'circle',
    circle: { cx: 50, cy: 50, r: 40 }  // Relative to bounds
  }]
}

Key Principle: bounds.x and bounds.y are ALWAYS 0 for standalone elements. They only change for grouped/nested elements.


❌ What Was Wrong

The Old Resize Event

@Output() elementResize = new EventEmitter<{
  width: number;
  height: number;
  x: number;  // ← WRONG: Mixed absolute position with size
  y: number;  // ← WRONG: Mixed absolute position with size
}>();

Problem: x and y in the resize event were absolute canvas coordinates, not position deltas. This caused the demo component to incorrectly calculate position changes.

The Old Demo Logic

// Calculate position delta from the resize
const deltaX = data.bounds.x - el.bounds.x;  // ← WRONG: bounds.x is always 0!
const deltaY = data.bounds.y - el.bounds.y;  // ← WRONG: bounds.y is always 0!

// This always resulted in deltaX = data.bounds.x - 0 = absolute X
// Not the actual CHANGE in position!

✅ The Solution: Delta-Based Resize Events

Conceptual Fix

Instead of emitting absolute coordinates, emit position deltas (how much the position changed):

@Output() elementResize = new EventEmitter<{
  width: number;    // New width
  height: number;   // New height
  deltaX: number;   // How much X position changed
  deltaY: number;   // How much Y position changed
}>();

🔧 Implementation Details

Step 1: Resize Handles Component

File: libs/universal-visual-compositor/src/lib/components/resize-handles/resize-handles.component.ts

Changed:

// OLD: Mixed absolute position with size
@Output() elementResize = new EventEmitter<{
  width: number;
  height: number;
  x: number;
  y: number;
}>();

// NEW: Separate size from position delta
@Output() elementResize = new EventEmitter<{
  width: number;
  height: number;
  deltaX: number;
  deltaY: number;
}>();

Calculation:

// Calculate position delta (how much the position changed)
const deltaX = newBounds.x - this.startBounds.x;
const deltaY = newBounds.y - this.startBounds.y;

this.elementResize.emit({
  width: Math.round(newBounds.width),
  height: Math.round(newBounds.height),
  deltaX: Math.round(deltaX),  // Position change
  deltaY: Math.round(deltaY)   // Position change
});

Why This Works:


Step 2: Visual Element Component

File: libs/universal-visual-compositor/src/lib/components/visual-element/visual-element.component.ts

Changed:

// OLD
@Output() elementResize = new EventEmitter<{
  width: number;
  height: number;
  x: number;
  y: number;
}>();

// NEW
@Output() elementResize = new EventEmitter<{
  width: number;
  height: number;
  deltaX: number;
  deltaY: number;
}>();

Just passes through the event from resize-handles to parent.


Step 3: Compositor Canvas Component

File: libs/universal-visual-compositor/src/lib/components/compositor-canvas/compositor-canvas.component.ts

Changed:

// OLD
@Output() elementResized = new EventEmitter<{
  id: string;
  bounds: { width: number; height: number; x: number; y: number }
}>();

onElementResize(elementId: string, dimensions: { ... }): void {
  this.elementResized.emit({ id: elementId, bounds: dimensions });
}

// NEW
@Output() elementResized = new EventEmitter<{
  id: string;
  dimensions: { width: number; height: number; deltaX: number; deltaY: number }
}>();

onElementResize(elementId: string, dimensions: { ... }): void {
  this.elementResized.emit({ id: elementId, dimensions });
}

Renamed bounds to dimensions to clarify it's not a complete Bounds object (no x/y fields).


Step 4: Demo Component

File: apps/gograb-space/src/app/demos/universal-compositor-demo/universal-compositor-demo.component.ts

Changed:

// OLD (WRONG)
onElementResized(data: { id: string; bounds: { ... } }): void {
  const deltaX = data.bounds.x - el.bounds.x;  // Always 0 - 0 = 0 or wrong value
  const deltaY = data.bounds.y - el.bounds.y;
  
  const newPosition = {
    x: el.position.x + deltaX,  // Incorrect
    y: el.position.y + deltaY
  };
  
  return {
    ...el,
    bounds: {
      x: el.bounds.x,  // Kept old value (always 0)
      y: el.bounds.y,
      width: data.bounds.width,
      height: data.bounds.height
    },
    position: constrainedPosition
  };
}

// NEW (CORRECT)
onElementResized(data: { id: string; dimensions: { ... } }): void {
  // Use the provided delta directly
  const newPosition = {
    x: el.position.x + data.dimensions.deltaX,  // Correct delta
    y: el.position.y + data.dimensions.deltaY
  };
  
  const constrainedPosition = this.constrainToViewport(newPosition, {
    width: data.dimensions.width,
    height: data.dimensions.height
  });
  
  return {
    ...el,
    bounds: {
      x: 0,  // Always 0 (element-relative)
      y: 0,  // Always 0 (element-relative)
      width: data.dimensions.width,
      height: data.dimensions.height
    },
    position: constrainedPosition
  };
}

Key Changes:

  1. ✅ Use data.dimensions.deltaX/deltaY directly (no calculation needed)
  2. ✅ Always set bounds.x = 0, bounds.y = 0 (maintain coordinate system invariant)
  3. ✅ Update position with the delta
  4. ✅ Constrain to viewport with new dimensions

🎯 How It Works Now

Resize from Right Edge (Handle 'e')

User drags right edge +50px
  ↓
ResizeHandles calculates:
  - newWidth = 100 + 50 = 150
  - newHeight = 100 (unchanged)
  - deltaX = 0 (position didn't change)
  - deltaY = 0
  ↓
Demo updates:
  - position: { x: 100, y: 100 } (unchanged)
  - bounds: { x: 0, y: 0, width: 150, height: 100 }
  ↓
Result: Shape grows to the right, position stays fixed ✅

Resize from Left Edge (Handle 'w')

User drags left edge -30px (moves left)
  ↓
ResizeHandles calculates:
  - newWidth = 100 + 30 = 130 (wider)
  - newHeight = 100 (unchanged)
  - deltaX = -30 (moved left)
  - deltaY = 0
  ↓
Demo updates:
  - position: { x: 100 - 30 = 70, y: 100 }
  - bounds: { x: 0, y: 0, width: 130, height: 100 }
  ↓
Result: Shape grows to the left, right edge stays fixed ✅

Resize from Top-Left Corner (Handle 'nw')

User drags corner up-left (-20, -20)
  ↓
ResizeHandles calculates:
  - newWidth = 100 + 20 = 120
  - newHeight = 100 + 20 = 120
  - deltaX = -20
  - deltaY = -20
  ↓
Demo updates:
  - position: { x: 100 - 20 = 80, y: 100 - 20 = 80 }
  - bounds: { x: 0, y: 0, width: 120, height: 120 }
  ↓
Result: Shape grows toward top-left, bottom-right stays fixed ✅

✅ Benefits

1. Correct Coordinate Semantics

2. Predictable Behavior

3. Clean Architecture

4. Scalable


🧪 Testing Checklist

Resize from Edges

Resize from Corners

SVG Rendering

Interaction


📊 Code Quality


🎨 Architectural Principles Demonstrated

1. Coordinate Space Separation

Different coordinate spaces should never be mixed in the same data structure.

2. Delta-Based Updates

For operations that modify position+size, emit deltas rather than absolutes.

3. Invariants

bounds.x and bounds.y being 0 for standalone elements is an invariant that must be preserved.

4. Event Semantics

Event data should be semantic and explicit about what it represents.


🚀 What This Enables

With correct coordinate handling, we can now implement:

Immediate

Near-Term

Long-Term


💡 Key Takeaway

Problem: Mixed coordinate spaces in resize events Solution: Separate size (dimensions) from position change (delta) Result: Clean, predictable, professional-grade resize behavior

This fix demonstrates that the Universal Visual Compositor is built on solid architectural foundations with proper coordinate space management, setting the stage for advanced features.


Status: 🟢 Resize System Complete Next: Continue with canvas basics verification and refinement Confidence: High - follows industry-standard coordinate system patterns