Engineering Journal
Schema Editor
Schema Editor

Topology Analyzer Reports Phantom Connections After Pressing Escape to Cancel a Wire

2026-06-04

TLDR

Pressing Escape during wire drawing removes the wire visually but leaves a stale entry in the wire registry. The topology analyzer finds this entry, calls getBBox() on the detached element (returns zeros), and reports a phantom connection at the origin. Filter the registry by element.isConnected on every analysis pass.


Symptom

The user starts drawing a wire, changes their mind, and presses Escape. The canvas looks clean. On the next topology analysis, the BOM or connectivity panel reports a connection between two components that were never wired. The phantom connection disappears when the page is refreshed but reappears if the user presses Escape again.

Why It Happens

The wire registry and the SVG DOM are separate stores. Cancellation removes the element from the DOM:

function cancelWire() {
    activeWire?.element?.remove(); // DOM cleaned up
    activeWire = null;             // activeWire cleared
    // wiresArray still contains the cancelled wire object
}

The registry entry (wiresArray or equivalent) is not cleaned up. The wire object persists with a reference to a detached SVG element.

When the topology analyzer iterates the registry, it calls getBBox() on each wire's element. getBBox() on a detached SVG element does not throw; it returns a zero-area bbox with x=0, y=0, width=0, height=0. The analyzer interprets this as a wire at the origin and checks whether any component's bbox contains (0, 0). If one does, a phantom connection is created.

The Fix

Two changes, both needed:

// 1. Cleanup function that updates both DOM and registry
function cleanupWire(wireObj) {
    wireObj?.element?.remove();
    wiresArray = wiresArray.filter(
        w => w !== wireObj && w.element?.isConnected
    );
    if (activeWire === wireObj) activeWire = null;
}

// 2. Defensive filter at the start of every analysis pass function analyzeTopology() { // Purge any registry entries whose elements are no longer in the DOM wiresArray = wiresArray.filter(w => w.element?.isConnected); // ... rest of analysis }

The cleanup function is now the only way to remove a wire. The analysis guard catches anything that slipped through. Together they make the registry self-healing.

Replace every direct element.remove() call on wires with cleanupWire(wireObj). This includes: Escape handler, pointercancel handler, delete key handler, and any undo restore that replaces the canvas.

How to Prevent It

Any time two stores are meant to represent the same set of objects, add a guard at the consumer. For DOM-backed registries, element.isConnected is a zero-cost check that returns false for any element that has been removed from the document. Run it as a filter before any operation that trusts the registry.

For testing: write a test that calls cancelWire() (or equivalent), then runs topology analysis, and asserts that the connection count did not change.

The Generalizable Lesson

When a registry holds references to DOM nodes, detached nodes are invalid entries. node.isConnected is the cheap, correct way to test this. Any operation that reads from the registry should filter out disconnected entries before trusting the list.

Read this post in the full Engineering Journal →