TROUBLESHOOTING CANVAS DRAG

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Troubleshooting: Canvas Drag Issues in Logic Builder

Issue: Blocks Disappear When Dragged

Symptom: When dragging logic blocks on the canvas, the blocks disappear leaving only connection lines visible.

Date Fixed: October 23, 2025


Root Causes

1. Invalid Position Data (NaN)

When block positions contain NaN values, they fail to render correctly.

Common Sources:

2. Desynchronized State

The blockPositions Map can become out of sync with the canonical canvasBlocks signal from Flash Persistence.

3. Missing Position Validation

Without validation, invalid positions can propagate through:


Prevention Strategy

✅ Always Validate Positions

function isValidPosition(pos: any): boolean {
  return pos &&
         typeof pos.x === 'number' &&
         typeof pos.y === 'number' &&
         !isNaN(pos.x) &&
         !isNaN(pos.y);
}

✅ Validate at Every Entry Point

  1. Coordinate Transformations

    private screenToCanvas(screenX: number, screenY: number): BlockPosition {
      const result = { x: ..., y: ... };
      
      if (isNaN(result.x) || isNaN(result.y)) {
        console.error('Invalid transformation result');
        return { x: 0, y: 0 }; // Safe default
      }
      
      return result;
    }
    
  2. Before Updating State

    updateBlockPosition(blockId: string, position: BlockPosition): void {
      if (!isValidPosition(position)) {
        console.error('Invalid position rejected');
        return; // Don't update
      }
      // ... proceed with update
    }
    
  3. During Drag Operations

    private onGlobalMouseMove(event: MouseEvent): void {
      const newPosition = calculatePosition(event);
      
      if (isValidPosition(newPosition)) {
        this.blockPositions.set(blockId, newPosition);
      } else {
        console.error('Invalid position during drag');
      }
    }
    
  4. When Loading Persisted State

    effect(() => {
      const blocks = this.service.blocks();
      
      blocks.forEach(block => {
        if (isValidPosition(block.position)) {
          this.blockPositions.set(block.id, block.position);
        } else {
          console.warn('Skipping invalid position');
        }
      });
    });
    

✅ Use Canonical Source for Positions

Always read from the signal (canonical state), not local maps:

getBlockPosition(blockId: string): BlockPosition {
  // ✅ Primary: Signal (canonical)
  const block = this.canvasBlocks().find(b => b.id === blockId);
  if (block?.position && isValidPosition(block.position)) {
    return block.position;
  }
  
  // ⚠️ Fallback: Local map (for in-flight updates only)
  const mapPos = this.blockPositions.get(blockId);
  if (mapPos && isValidPosition(mapPos)) {
    return mapPos;
  }
  
  // 🛟 Safe default
  return { x: 100, y: 100 };
}

✅ Auto-Recovery

Implement reconciliation that fixes invalid positions automatically:

async reconcilePersistedState(): Promise<void> {
  const blocks = this._blocks();
  
  // Find blocks with invalid positions
  const broken = blocks.filter(b => !isValidPosition(b.position));
  
  if (broken.length > 0) {
    console.warn(`Found ${broken.length} blocks with invalid positions`);
    
    // Apply grid-based defaults
    let gridX = 100;
    let gridY = 100;
    
    this._blocks.update(currentBlocks =>
      currentBlocks.map(block => {
        if (!isValidPosition(block.position)) {
          const newPosition = { x: gridX, y: gridY };
          
          // Move to next grid position
          gridX += 350;
          if (gridX > 1000) {
            gridX = 100;
            gridY += 300;
          }
          
          return { ...block, position: newPosition };
        }
        return block;
      })
    );
  }
}

Diagnostic Checklist

When debugging canvas drag issues:


Testing

Manual Test Cases

  1. Basic Drag: Drag a block to a new position

    • Expected: Block moves smoothly, remains visible
  2. Multi-Block Drag: Drag multiple blocks sequentially

    • Expected: All blocks maintain positions
  3. Extreme Positions: Drag block to far corners/edges

    • Expected: Block remains visible and functional
  4. Zoom + Drag: Change zoom level, then drag

    • Expected: Dragging works correctly at all zoom levels
  5. Refresh After Drag: Drag block, refresh page

    • Expected: Block position persists correctly
  6. Corrupted Data Recovery: Manually corrupt position in IndexedDB

    • Expected: Auto-recovery assigns grid position

Automated Tests (Recommended)

describe('Canvas Position Validation', () => {
  it('should reject NaN positions', () => {
    const invalidPos = { x: NaN, y: 100 };
    service.updateBlockPosition('block-1', invalidPos);
    
    const stored = service.getBlock('block-1').position;
    expect(stored.x).not.toBeNaN();
  });
  
  it('should auto-recover invalid positions on load', async () => {
    // Setup: Create block with invalid position
    await service.createBlock({ position: { x: NaN, y: NaN } });
    
    // Trigger reconciliation
    await service.reconcilePersistedState();
    
    // Verify: Position is now valid
    const blocks = service.blocks();
    blocks.forEach(block => {
      expect(isValidPosition(block.position)).toBe(true);
    });
  });
});

Related Files

Core Files

Validation Points

  1. screenToCanvas() - Coordinate transformation
  2. updateBlockPosition() - State updates
  3. getBlockPosition() - Position retrieval
  4. reconcilePersistedState() - Auto-recovery
  5. onGlobalMouseMove() - Drag handling
  6. onGlobalMouseUp() - Drag completion
  7. constructor effect() - State synchronization

References


Last Updated: October 23, 2025
Status: Fixed and Validated