A Layer Panel Is Not a File Browser. It Is a Live Mirror of the Scene Graph.
TLDR
Layer panels built as separate, secondary data models (storing their own arrays of layer objects, folders, and z-index ordering) inevitably desynchronize from the actual render tree. The DOM itself is the authoritative scene graph. Treating the layer panel as a pure, zero-state projection generated directly from SVG DOM node order eliminates all layer sync bugs, simplifies undo/redo, and ensures z-index parity.| Architectural Pattern | Data Storage Model | Undo / Redo Handling | Desynchronization Risk |
|---|---|---|---|
| Secondary Panel Model | Array of layer state objects | Complex custom state snapshotting | High (Fails on import/undo) |
| DOM-Derived Pure View | DOM attributes (data-layer-name) | Automatic (Rebuilds on DOM change) | Zero (Guaranteed parity) |
Problem statement: the flaws of dual-model architecture
In vector design applications, developers often treat the layer panel like a file explorer UI, maintaining an array of layer objects ({ id, name, zIndex, children }) separate from the SVG canvas elements.
This dual-model architecture requires intercepting every canvas mutation path (add, delete, move, group, paste, import, undo/redo) to keep the secondary layer model in sync with the DOM. Miss a single event handler, and the layer panel displays stale or orphaned entries.
Technical failure mode: Z-order & mutation desynchronization
- Z-Order Inversion: SVG rendering order is determined strictly by DOM child node sequence. When a secondary panel model tracks z-index independently, reordering entries in the layer panel can easily diverge from true DOM painting sequence.
- Undo/Redo Breakdown: Reverting a canvas state via DOM snapshot restoration updates the canvas tree immediately, but leaves the secondary layer panel model holding pre-undo state.
The fix & architecture: deriving panels directly from the DOM
Treat the DOM as the single source of truth. Store layer metadata directly on SVG elements as data-* attributes and rebuild the layer UI by walking DOM child nodes:
// REFACTORED: Zero-state layer panel builder
function buildLayerPanel(contentRoot, panelContainer) {
panelContainer.innerHTML = ''; // Fast reset (<1ms for typical diagrams)
// Walk SVG children in DOM order — DOM order IS Z-order! Array.from(contentRoot.children) .filter(el => !el.dataset.system) // Filter out system UI elements .forEach(el => { const row = createLayerRow(el); panelContainer.appendChild(row); }); }
function createLayerRow(el) { const row = document.createElement('div'); row.className = 'layer-row'; row.textContent = el.getAttribute('data-layer-name') || el.id;
// Toggle visibility directly on DOM node const hideBtn = document.createElement('button'); hideBtn.onclick = () => { const isHidden = el.dataset.hidden === 'true'; el.dataset.hidden = String(!isHidden); el.style.display = isHidden ? '' : 'none'; buildLayerPanel(contentRoot, panelContainer); // Rebuild reads DOM state! };
return row; }
Reordering DOM nodes directly
Moving a layer in the panel executes native DOM node reordering (insertBefore), eliminating the need for a secondary z-index array:
function moveElementBefore(movedEl, targetEl, contentRoot) {
contentRoot.insertBefore(movedEl, targetEl);
buildLayerPanel(contentRoot, panelContainer); // Rebuild reflects DOM order
}
Rule of thumb: Never maintain a secondary array for layer panel state. Store visibility and layer names as data-* attributes directly on DOM nodes, and generate layer UI rows by walking the DOM tree.