Building a Live Measure Tool on an SVG Canvas
TLDR
The GINEXYS Schema Editor implements a live tape measure tool using an ephemeral SVG<g> overlay group (#measure-overlay) positioned above all canvas elements. The overlay clears and redraws on mousemove frames during active measurements. Point placement uses geometry-aware snapping, clicking wires invokes native SVGGeometryElement.getTotalLength() for exact path distance, and 3-point angle measurements use dot and cross products to classify angles as acute, right, obtuse, or straight.
| Feature Capability | Underlying API / Math | Implementation Pattern |
|---|---|---|
| Ephemeral Overlay | SVG <g> element + innerHTML = '' | Full redraw per mousemove frame |
| Snap-Aware Points | smartSnap(worldPt) | Anchors to exact component pin coordinates |
| Wire Auto-Measure | wireEl.getTotalLength() | Native SVG path length calculation |
| Angle Classification | atan2( | cross |
Problem statement: high-precision measurements in vector editors
Engineers using schematic editors need to verify distances between pins, validate wire run lengths, and inspect geometric angles between components.
Building an interactive measurement tool requires:
- Drawing measurement lines and text labels in real-time as the cursor moves.
- Snapping measurement endpoints to exact component pins rather than approximate mouse pixels.
- Measuring multi-segment bent wire paths automatically without manual vertex clicking.
Technical failure mode: granular element property mutating vs overlay redraws
Updating individual DOM attributes across dozens of child elements (lines, text labels, ticks, backdrop rects) during a fast mousemove drag requires complex DOM diffing logic.
For ephemeral overlays with under 10 elements, clearing the group (overlay.innerHTML = '') and appending fresh SVG elements per frame is significantly faster and less bug-prone than mutating individual attributes.
The fix & architecture: ephemeral overlay & native path inspection
1. Ephemeral overlay & snap placement
Create a dedicated<g id="measure-overlay"> group at the root of the SVG canvas and clear/redraw it on mousemove:
function _updateMeasureOverlay(fromPt, toPt) {
overlay.innerHTML = ''; // Fast reset for small DOM node counts (<10 nodes)
const dx = toPt.x - fromPt.x;
const dy = toPt.y - fromPt.y;
const dist = Math.hypot(dx, dy);
// Offset distance label perpendicular to measurement line const mid = { x: (fromPt.x + toPt.x) / 2, y: (fromPt.y + toPt.y) / 2 }; const norm = { x: -dy / dist, y: dx / dist }; const labelPt = { x: mid.x + norm.x 14, y: mid.y + norm.y 14 };
_appendLine(overlay, fromPt.x, fromPt.y, toPt.x, toPt.y, 'measure-line'); _appendText(overlay, ${dist.toFixed(1)} u, labelPt, 'measure-label'); }
2. Auto-Measuring wire paths
Clicking a wire invokes nativegetTotalLength() and getPointAtLength() on the SVGPathElement:
function _handleWireClick(wireEl) {
const length = wireEl.getTotalLength(); // Native browser SVG path calculation
const labelPt = wireEl.getPointAtLength(length / 2); // Midpoint along curve
_renderLengthLabel(labelPt, length, 'path');
}
3. Vector angle classification
When three points are selected (vertex between two vectors), calculate and classify the angle using dot and cross products:function _classifyAngle(a, vertex, b) {
const va = { x: a.x - vertex.x, y: a.y - vertex.y };
const vb = { x: b.x - vertex.x, y: b.y - vertex.y };
const dot = va.x vb.x + va.y vb.y;
const cross = va.x vb.y - va.y vb.x;
const angleDeg = Math.atan2(Math.abs(cross), dot) * (180 / Math.PI);
if (Math.abs(angleDeg - 90) < 1) return 'right'; if (angleDeg < 90) return 'acute'; if (angleDeg < 180) return 'obtuse'; return 'straight'; }
Rule of thumb: Use native browser APIs likeSVGGeometryElement.getTotalLength()for path length measurements, and clear ephemeral overlay groups withinnerHTML = ''on 60fps interaction frames when node counts are small (<10 elements).