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:
- Division by zero or invalid scale in coordinate transformations
- Arithmetic operations on undefined/null values
- Corrupted data in Flash Persistence
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:
- Coordinate transformation (
screenToCanvas) - Drag event handlers
- State updates
- Flash Persistence
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
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; }Before Updating State
updateBlockPosition(blockId: string, position: BlockPosition): void { if (!isValidPosition(position)) { console.error('Invalid position rejected'); return; // Don't update } // ... proceed with update }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'); } }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:
- Check browser console for position validation errors
- Verify
transformsignal values (x, y, scale) - Inspect Flash Persistence data in IndexedDB (DevTools → Application → IndexedDB)
- Look for
NaNin position values - Check if
blockPositionsMap is synchronized with signal - Verify coordinate calculations in
screenToCanvas - Test with different zoom levels
- Check for corrupted data from previous sessions
Testing
Manual Test Cases
Basic Drag: Drag a block to a new position
- Expected: Block moves smoothly, remains visible
Multi-Block Drag: Drag multiple blocks sequentially
- Expected: All blocks maintain positions
Extreme Positions: Drag block to far corners/edges
- Expected: Block remains visible and functional
Zoom + Drag: Change zoom level, then drag
- Expected: Dragging works correctly at all zoom levels
Refresh After Drag: Drag block, refresh page
- Expected: Block position persists correctly
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
libs/singularity-lib/src/lib/services/singularity.service.tslibs/singularity-lib/src/lib/components/logic-builder/logic-builder.component.ts
Validation Points
screenToCanvas()- Coordinate transformationupdateBlockPosition()- State updatesgetBlockPosition()- Position retrievalreconcilePersistedState()- Auto-recoveryonGlobalMouseMove()- Drag handlingonGlobalMouseUp()- Drag completionconstructor effect()- State synchronization
References
Last Updated: October 23, 2025
Status: Fixed and Validated