app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
// viewer_core.js — the ONE symbol-viewer engine, compiled into BOTH the live
// adom-symbol app (SymView) AND the standalone embed export. Neither file should
// reimplement pan/zoom or pin-hover; they call into here so they can never drift.
window.AdomSymViewer = (function(){
var ZOOM_K = 0.0022; // wheel sensitivity (lower = gentler). The feel.
var MIN_SCALE = 0.05, MAX_SCALE = 60;
function clampScale(s){ return Math.max(MIN_SCALE, Math.min(s, MAX_SCALE)); }
// Device-independent wheel zoom: normalize deltaMode + cap one notch, then hand
// the pointer-anchored factor to the host's zoomAt(cx, cy, factor). This is the
// exact math the app has always used — the embed now shares it verbatim.
function attachWheelZoom(el, zoomAt){
el.addEventListener('wheel', function(e){
e.preventDefault();
var d = e.deltaY;
if(e.deltaMode === 1) d *= 16; // lines -> px
else if(e.deltaMode === 2) d *= el.clientHeight; // pages -> px
d = Math.max(-50, Math.min(50, d)); // cap one event's contribution
var r = el.getBoundingClientRect();
zoomAt(e.clientX - r.left, e.clientY - r.top, Math.exp(-d * ZOOM_K));
}, { passive: false });
}
// ---- shared pin-hover engine (glyph pairing + hit-box geometry) ----
function norm(s){ return String(s || '').replace(/[~{}]/g, '').trim(); }
// Pair each invisible <text opacity="0"> with the visible <g class="stroked-text">
// glyph that follows it in document order (handles rotated top/bottom pins).
function glyphLabels(svg){
var out = [], pending = null;
if(!svg) return out;
svg.querySelectorAll('text, g.stroked-text').forEach(function(n){
if(n.classList && n.classList.contains('stroked-text')){
if(pending){ var k = norm(pending.textContent); if(k) out.push({ k: k, group: n }); pending = null; }
} else if(n.getAttribute && n.getAttribute('opacity') === '0'){ pending = n; }
});
return out;
}
// Build one transparent hit-rect per pin label (number + name), matched to each
// pin's glyph by proximity to the pin tip (so duplicate names resolve to the
// right pin). Rects are appended to `svg` in user space so they pan/zoom with it.
// pins: [{number,name,x,y,angle,length,...}]
// offset: {x,y} symbol->svg-unit origin (state.offset)
// NS: the svg namespace
// cb: { enter(pin,ev), move(ev), leave(), click(pin,ev)? }
// Returns { textMap } so the host can highlight a pin's glyphs on demand.
function buildPinHits(svg, pins, offset, NS, cb){
var textMap = new Map();
if(!svg || !offset || !pins || !pins.length) return { textMap: textMap };
cb = cb || {};
var gl = glyphLabels(svg);
gl.forEach(function(o){ if(!textMap.has(o.k)) textMap.set(o.k, []); textMap.get(o.k).push(o.group); });
var labels = [];
gl.forEach(function(o){ var b; try { b = o.group.getBBox(); } catch(e){ return; }
if(!b || !b.width) return;
labels.push({ k: o.k, box: b, cx: b.x + b.width/2, cy: b.y + b.height/2, used: false }); });
function pick(k, ax, ay){ k = norm(k); var best = null, bd = 1e9;
labels.forEach(function(l){ if(l.used || l.k !== k) return; var d = Math.hypot(l.cx - ax, l.cy - ay); if(d < bd){ bd = d; best = l; } });
if(best) best.used = true; return best; }
function addBox(b, p){
var pad = 0.45, r = document.createElementNS(NS, 'rect');
r.setAttribute('class', 'hit');
r.setAttribute('x', b.x - pad); r.setAttribute('y', b.y - pad);
r.setAttribute('width', b.width + pad*2); r.setAttribute('height', b.height + pad*2); r.setAttribute('rx', .3);
r.addEventListener('mouseenter', function(ev){ if(cb.enter) cb.enter(p, ev); });
r.addEventListener('mousemove', function(ev){ if(cb.move) cb.move(ev); });
r.addEventListener('mouseleave', function(){ if(cb.leave) cb.leave(); });
if(cb.click) r.addEventListener('click', function(ev){ cb.click(p, ev); });
svg.appendChild(r);
}
pins.forEach(function(p){
var tipX = offset.x + p.x, tipY = offset.y - p.y, L = p.length || 2.54, dx = 0, dy = 0;
if(p.angle === 0) dx = 1; else if(p.angle === 180) dx = -1; else if(p.angle === 90) dy = -1; else if(p.angle === 270) dy = 1;
var nl = pick(p.number, tipX + dx*L*0.5, tipY + dy*L*0.5 - (dx !== 0 ? 0.9 : 0)); // number: over the wire
var ml = pick(p.name, tipX + dx*(L+2), tipY + dy*(L+2)); // name: inside the body
if(nl) addBox(nl.box, p);
if(ml) addBox(ml.box, p);
if(!nl && !ml){ // discrete parts: no glyphs
var ex = tipX + dx*L, ey = tipY + dy*L, HT = 1.0;
addBox({ x: Math.min(tipX, ex) - (dx === 0 ? HT : 0.3), y: Math.min(tipY, ey) - (dy === 0 ? HT : 0.3),
width: Math.abs(ex - tipX) + (dx === 0 ? HT*2 : 0.6), height: Math.abs(ey - tipY) + (dy === 0 ? HT*2 : 0.6) }, p);
}
});
return { textMap: textMap };
}
return {
ZOOM_K: ZOOM_K, clampScale: clampScale, attachWheelZoom: attachWheelZoom,
norm: norm, glyphLabels: glyphLabels, buildPinHits: buildPinHits
};
})();