app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
master
John Lauer
1.2.0: chip thumbnail baked into every IC symbol by default (real outline or generic chip), chipOutline/chipSize prefs, AI check-and-revise hints, STEP ladder in hints
9a15e91
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
/*!
* symgen.js : JavaScript port of adom-symbol's `src/sym_gen.rs` (generate_ic_sym).
* Used by the wiki widget when WebAssembly is blocked by the frame CSP. Kept
* byte-for-byte equal to the Rust output; `widget/parity.js` diffs the two on
* every build. Do not "improve" this file: change sym_gen.rs, then mirror it.
*/
(function (root) {
const PIN_LENGTH = 2.54, PIN_PITCH = 2.54, GROUP_GAP = 3.81, LABEL_OFFSET = 2.0;
const BODY_TOP_MARGIN = 2.54, BODY_BOTTOM_MARGIN = 2.54, MIN_BODY_WIDTH = 20.0, BODY_PADDING = 4.0;
const FS_PIN_NAME = 1.27, FS_GROUP_LABEL = 0.762;
const NAME_CHAR_W = 1.5, NAME_EXT_OFFSET = 1.016 + 0.6, LABEL_CHAR_W = 0.9, LABEL_INSET = 1.02;
const DESIGNATOR_INSET = 2.54, MIN_ICON_MM = 15.0, ICON_MARGIN = 1.0;
// Chip outline (mirrors sym_gen.rs: OUTLINE_BODY_FRAC, chip_size_frac, GENERIC_OUTLINE*, icon_box, bake_chip_outline)
const OUTLINE_BODY_FRAC = 0.62;
function chipSizeFrac(size) { return size === 'small' ? OUTLINE_BODY_FRAC * 0.58 : size === 'medium' ? OUTLINE_BODY_FRAC * 0.78 : OUTLINE_BODY_FRAC; }
const GENERIC_OUTLINE_VIEWBOX = [0.0, 0.0, 320.0, 320.0];
const GENERIC_OUTLINE_NAME_AT = [160.3, 141.3];
const GENERIC_OUTLINE_NAME_W = 297.5;
const GENERIC_OUTLINE = [
[[123.1, 55.4], [309.1, 162.8], [197.5, 227.2], [11.6, 119.8], [123.1, 55.4]],
[[11.6, 119.8], [11.6, 156.3], [197.5, 263.7], [197.5, 227.2]],
[[197.5, 263.7], [309.1, 199.3], [309.1, 162.8]],
[[19.3, 160.8], [10.0, 166.1], [21.2, 172.6], [30.5, 167.2], [21.2, 172.6], [21.2, 164.0], [10.0, 157.5], [10.0, 166.1]],
[[45.9, 176.1], [36.6, 181.5], [47.7, 187.9], [57.0, 182.5], [47.7, 187.9], [47.7, 179.3], [36.6, 172.9], [36.6, 181.5]],
[[72.4, 191.4], [63.1, 196.8], [74.3, 203.2], [83.6, 197.9], [74.3, 203.2], [74.3, 194.7], [63.1, 188.2], [63.1, 196.8]],
[[99.0, 206.8], [89.7, 212.1], [100.8, 218.6], [110.1, 213.2], [100.8, 218.6], [100.8, 210.0], [89.7, 203.6], [89.7, 212.1]],
[[125.5, 222.1], [116.2, 227.5], [127.4, 233.9], [136.7, 228.5], [127.4, 233.9], [127.4, 225.3], [116.2, 218.9], [116.2, 227.5]],
[[152.1, 237.4], [142.8, 242.8], [154.0, 249.2], [163.3, 243.9], [154.0, 249.2], [154.0, 240.7], [142.8, 234.2], [142.8, 242.8]],
[[178.7, 252.8], [169.4, 258.1], [180.5, 264.6], [189.8, 259.2], [180.5, 264.6], [180.5, 256.0], [169.4, 249.6], [169.4, 258.1]],
[[300.7, 204.1], [310.0, 209.5], [298.8, 215.9], [289.5, 210.5], [298.8, 215.9], [298.8, 207.3], [310.0, 200.9], [310.0, 209.5]],
[[272.8, 220.2], [282.1, 225.6], [271.0, 232.0], [261.7, 226.6], [271.0, 232.0], [271.0, 223.4], [282.1, 217.0], [282.1, 225.6]],
[[244.9, 236.3], [254.2, 241.7], [243.1, 248.1], [233.8, 242.7], [243.1, 248.1], [243.1, 239.5], [254.2, 233.1], [254.2, 241.7]],
[[217.0, 252.4], [226.3, 257.8], [215.2, 264.2], [205.9, 258.8], [215.2, 264.2], [215.2, 255.6], [226.3, 249.2], [226.3, 257.8]],
[[59.0, 123.6], [52.5, 125.1], [46.0, 123.6], [43.3, 119.8], [46.0, 116.1], [52.5, 114.5], [59.0, 116.1], [61.7, 119.8], [59.0, 123.6]],
];
function genericOutline(size) {
return { polylines: GENERIC_OUTLINE.map(pl => pl.map(p => [p[0], p[1]])), viewbox: GENERIC_OUTLINE_VIEWBOX.slice(), name_at: GENERIC_OUTLINE_NAME_AT.slice(), name_width: GENERIC_OUTLINE_NAME_W, size: size || 'large', source: 'generic-iso' };
}
function fx3(v) {
const scaled = v * 1000;
const rounded = scaled >= 0 ? Math.floor(scaled + 0.5) : Math.ceil(scaled - 0.5);
let s = (rounded / 1000).toFixed(3);
return s === '-0.000' ? '0.000' : s;
}
function iconBox(body, free0, designators, frac) {
const bw = body[2] - body[0], bh = body[3] - body[1];
const free = free0.slice();
for (const [x, y] of designators) {
if (x > body[0] && x < body[2] && y > body[1] && y < body[3]) {
const cy = (free[1] + free[3]) / 2;
if (y >= cy) free[3] = Math.min(free[3], y - 1.3); else free[1] = Math.max(free[1], y + 1.3);
}
}
const fw = Math.max(free[2] - free[0] - 2 * ICON_MARGIN, 1.0), fh = Math.max(free[3] - free[1] - 2 * ICON_MARGIN, 1.0);
const w = Math.min(bw * frac, fw), h = Math.min(bh * frac, fh);
const bcx = (body[0] + body[2]) / 2;
const cx = (bcx - w / 2 >= free[0] + ICON_MARGIN && bcx + w / 2 <= free[2] - ICON_MARGIN) ? bcx : (free[0] + free[2]) / 2;
const cy = (free[1] + free[3]) / 2;
return [cx, cy, w, h];
}
function bakeChipOutline(outline, name, body, free, designators) {
const frac = chipSizeFrac(outline.size);
const [cx, cy, w, h] = iconBox(body, free, designators, frac);
const [vx0, vy0, vw, vh] = outline.viewbox;
const scale = Math.min(w / vw, h / vh);
const iconMm = scale * Math.max(vw, vh);
if (!(isFinite(scale) && scale > 0) || iconMm < MIN_ICON_MM * 0.6) return null;
const scx = vx0 + vw / 2, scy = vy0 + vh / 2;
const map = (px, py) => [cx + (px - scx) * scale, cy - (py - scy) * scale];
const stroke = fx3(Math.max(scale * vw / 120, 0.12));
const lines = [];
for (const pl of outline.polylines) {
if (pl.length < 2) continue;
const pts = pl.map(([px, py]) => { const [x, y] = map(px, py); return `(xy ${fx3(x)} ${fx3(y)})`; });
lines.push(` (polyline (pts ${pts.join(' ')}) (stroke (width ${stroke}) (type default)) (fill (type none)))`);
}
let lasered = false;
if (outline.name_at) {
const [tx, ty] = map(outline.name_at[0], outline.name_at[1]);
const n = Math.max(chars(name), 1);
const avail = outline.name_width * scale;
const size = Math.max(Math.min(avail * 0.7 / (n * 0.8), 1.27), 0.5);
lines.push(` (text "${name}" (at ${fx(tx)} ${fx(ty)} 0)`, ` (effects (font (size ${fx(size)} ${fx(size)})))`, ' )');
lasered = true;
}
return [lines, lasered];
}
function fx(v) {
const scaled = v * 100;
const rounded = scaled >= 0 ? Math.floor(scaled + 0.5) : Math.ceil(scaled - 0.5);
let s = (rounded / 100).toFixed(2);
return s === '-0.00' ? '0.00' : s;
}
const chars = s => Array.from(s).length;
const strip = s => chars(s.replace(/~\{/g, '').replace(/\}/g, ''));
function isGroundName(name) {
const n = name.toUpperCase();
return n.startsWith('GND') || n.startsWith('VSS') || n.startsWith('AGND') || n.startsWith('DGND')
|| n.startsWith('PGND') || n.startsWith('OGND') || n === 'EP' || n.startsWith('EP_') || n === 'PAD' || n === 'THERMAL';
}
const eqi = (a, b) => a.toLowerCase() === b.toLowerCase();
const clone = p => ({ name: p.name, number: p.number, ptype: p.ptype, group: p.group == null ? null : p.group, side: p.side == null ? null : p.side });
function byGroup(pins) {
const out = [];
for (const p of pins) {
const g = p.group == null ? '_ungrouped' : p.group;
const e = out.find(x => x[0] === g);
if (e) e[1].push(clone(p)); else out.push([g, [clone(p)]]);
}
return out;
}
const isPowerType = p => p.ptype === 'power_in' || p.ptype === 'power_out';
function inferSquareSides(pins, grouped) {
grouped = grouped && pins.some(p => p.group != null);
if (!grouped) {
const n = pins.length, per = Math.max(1, Math.floor((n + 3) / 4));
const left = [], bottom = [], right = [], top = [];
pins.forEach((p, i) => {
const q = clone(p); q.group = null;
switch (Math.min(3, Math.floor(i / per))) {
case 0: q.side = 'left'; left.push(q); break;
case 1: q.side = 'bottom'; bottom.push(q); break;
case 2: q.side = 'right'; right.push(q); break;
default: q.side = 'top'; top.push(q);
}
});
right.reverse(); top.reverse();
return left.concat(bottom, right, top);
}
const bg = byGroup(pins);
const order = bg.map(x => x[0]);
const pk = order.find(g => eqi(g, 'power'));
if (pk != null) {
const idx = bg.findIndex(x => x[0] === pk);
const gnd = bg[idx][1].filter(p => isGroundName(p.name)), pwr = bg[idx][1].filter(p => !isGroundName(p.name));
if (gnd.length && pwr.length && !order.some(g => eqi(g, 'ground'))) {
bg[idx][1] = pwr;
bg.push(['Ground', gnd.map(p => { const q = clone(p); q.group = 'Ground'; return q; })]);
order.push('Ground');
}
}
const sides = ['left', 'bottom', 'right', 'top'];
const load = [0, 0, 0, 0];
const sideOf = bg.map(() => null);
bg.forEach(([g, ps], i) => {
const allGround = ps.every(p => isGroundName(p.name));
const allPower = ps.every(isPowerType);
if (eqi(g, 'ground') || (allGround && g !== '_ungrouped')) { sideOf[i] = 'bottom'; load[1] += ps.length; }
else if (eqi(g, 'power') || (allPower && !allGround && g !== '_ungrouped')) { sideOf[i] = 'top'; load[3] += ps.length; }
});
const rest = bg.map((_, i) => i).filter(i => sideOf[i] == null);
rest.sort((a, b) => bg[b][1].length - bg[a][1].length); // stable, like Rust sort_by
for (const i of rest) {
let k = 0; for (let j = 1; j < 4; j++) if (load[j] < load[k]) k = j;
sideOf[i] = sides[k]; load[k] += bg[i][1].length;
}
const out = [];
for (const side of sides) bg.forEach(([, ps], i) => { if (sideOf[i] === side) for (const p of ps) { const q = clone(p); q.side = side; out.push(q); } });
return out;
}
function inferLrGroupedSides(pins) {
const bg = byGroup(pins);
const n = bg.length;
const isPower = (g, ps) => eqi(g, 'power') || eqi(g, 'ground') || (g !== '_ungrouped' && ps.every(isPowerType));
const side = bg.map(() => null);
let ln = 0, rn = 0;
bg.forEach(([g, ps], i) => { if (isPower(g, ps)) { side[i] = 'right'; rn += ps.length; } });
const rest = bg.map((_, i) => i).filter(i => side[i] == null);
rest.sort((a, b) => bg[b][1].length - bg[a][1].length);
for (const i of rest) {
const ps = bg[i][1];
const outputsOnly = ps.every(p => p.ptype === 'output' || p.ptype === 'tri_state' || p.ptype === 'open_collector');
const toRight = ln === rn ? outputsOnly : rn < ln;
if (toRight) { side[i] = 'right'; rn += ps.length; } else { side[i] = 'left'; ln += ps.length; }
}
const out = [];
bg.forEach(([, ps], i) => { if (side[i] === 'left') for (const p of ps) { const q = clone(p); q.side = 'left'; out.push(q); } });
const ro = bg.map((_, i) => i).filter(i => side[i] === 'right');
ro.sort((a, b) => ((isPower(bg[a][0], bg[a][1]) ? 0 : 1) - (isPower(bg[b][0], bg[b][1]) ? 0 : 1)) || (a - b));
for (const i of ro) for (const p of bg[i][1]) { const q = clone(p); q.side = 'right'; out.push(q); }
return out;
}
function inferDipSides(pins) {
const half = Math.floor((pins.length + 1) / 2);
const left = pins.slice(0, half).map(p => { const q = clone(p); q.side = 'left'; return q; });
const right = pins.slice(half).map(p => { const q = clone(p); q.side = 'right'; return q; });
right.reverse();
return left.concat(right);
}
function layoutVertical(sideGroups) {
const pins = [], labels = []; let y = 0;
sideGroups.forEach((sg, i) => {
if (i > 0) y -= GROUP_GAP;
const labelY = y; y -= LABEL_OFFSET;
for (const pin of sg.pins) { pins.push({ name: pin.name, number: pin.number, ptype: pin.ptype, x: 0, y }); y -= PIN_PITCH; }
if (sg.name !== '_ungrouped') labels.push({ name: sg.name, x: 0, y: labelY });
});
return [pins, labels, Math.abs(y)];
}
function layoutHorizontal(sideGroups, startX) {
const pins = [], labels = []; let x = startX;
sideGroups.forEach((sg, i) => {
if (i > 0) x += GROUP_GAP;
const labelX = x;
if (sg.name !== '_ungrouped') x += LABEL_OFFSET;
for (const pin of sg.pins) { pins.push({ name: pin.name, number: pin.number, ptype: pin.ptype, x, y: 0 }); x += PIN_PITCH; }
if (sg.name !== '_ungrouped') labels.push({ name: sg.name, x: labelX, y: 0 });
});
return [pins, labels, x - startX];
}
function designatorAt(pos, bl, br, bt, bb, clr, free, halfW) {
const cy = (bt + bb) / 2;
const bcx = (bl + br) / 2;
const fcx = (bcx - halfW >= free[0] && bcx + halfW <= free[2]) ? bcx : (free[0] + free[2]) / 2;
switch (pos) {
case 'center': return [0, cy, ''];
case 'free-top': return [fcx, free[3] - DESIGNATOR_INSET, ''];
case 'free-bot': case 'free-bottom': return [fcx, free[1] + DESIGNATOR_INSET, ''];
case 'corner-top': return [bl + 1.0, clr.top, ' (justify left)'];
case 'corner-bot': case 'corner-bottom': return [bl + 1.0, clr.bot, ' (justify left)'];
case 'in-top': return [0, bt - 2.2, ''];
case 'out-bot': case 'out-bottom': return [0, clr.bot, ''];
case 'in-bot': case 'in-bottom': return [0, bb + 2.2, ''];
case 'out-left': return [clr.left, cy, ' (justify right)'];
case 'in-left': return [bl + 2.0, cy, ' (justify left)'];
case 'out-right': return [clr.right, cy, ' (justify left)'];
case 'in-right': return [br - 2.0, cy, ' (justify right)'];
case 'top-left': return [bl + 2.0, bt - 1.8, ' (justify left)'];
case 'bottom-left': return [bl + 2.0, bb + 1.8, ' (justify left)'];
default: return [0, clr.top, ''];
}
}
function generateIcSym(input) {
const pins0 = input.pins.map(p => ({ name: p.name, number: p.number, ptype: p.ptype != null ? p.ptype : p.type, group: p.group == null ? null : p.group, side: p.side == null ? null : p.side }));
let pinLayout = input.pin_layout || 'left-right';
const hasExplicitSide = pins0.some(p => p.side != null);
let squarePins = null;
if (pinLayout === 'square' && !hasExplicitSide) squarePins = inferSquareSides(pins0, false);
else if ((pinLayout === 'square-grouped' || pinLayout === 'all-sides') && !hasExplicitSide) squarePins = inferSquareSides(pins0, true);
const anyGroup = pins0.some(p => p.group != null);
let dipPins = null;
if (squarePins == null && pinLayout === 'left-right' && !hasExplicitSide && pins0.length > 1)
dipPins = anyGroup ? inferLrGroupedSides(pins0) : inferDipSides(pins0);
const inputPins = squarePins || dipPins || pins0;
if (squarePins) pinLayout = 'all-sides';
const groups = byGroup(inputPins);
const left = [], right = [], top = [], bottom = [];
for (const [groupName, groupPins] of groups) {
for (const side of ['left', 'right', 'top', 'bottom']) {
const ps = groupPins.filter(p => p.side === side);
if (!ps.length) continue;
const sg = { name: groupName, pins: ps };
({ left, right, top, bottom })[side].push(sg);
}
const rest = groupPins.filter(p => p.side == null);
if (rest.length) {
const sg = { name: groupName, pins: rest };
const hasInputs = rest.some(p => p.ptype === 'input' || p.ptype === 'bidirectional');
const hasPower = rest.some(isPowerType);
const hasOutputs = rest.some(p => p.ptype === 'output' || p.ptype === 'tri_state');
const isGround = rest.every(p => p.ptype === 'power_in' && isGroundName(p.name));
if (pinLayout === 'all-sides') {
if (isGround) bottom.push(sg);
else if (hasPower && !hasInputs && !hasOutputs) top.push(sg);
else if (hasOutputs && !hasInputs) right.push(sg);
else left.push(sg);
} else {
if (isGround) left.push(sg);
else if (hasOutputs && !hasInputs) right.push(sg);
else if (hasPower && !hasInputs) right.push(sg);
else left.push(sg);
}
}
}
const [leftPins, leftLabels] = layoutVertical(left);
const [rightPins, rightLabels] = layoutVertical(right);
const longestLeft = leftPins.reduce((m, p) => Math.max(m, chars(p.name)), 0);
const longestRight = rightPins.reduce((m, p) => Math.max(m, chars(p.name)), 0);
let nameWidth = (longestLeft + longestRight) * 1.0 + BODY_PADDING * 2;
if (input.chip_name_centered) {
const nameLen = chars(input.symbol_name);
nameWidth = Math.max(nameWidth, (longestLeft + longestRight + nameLen) * 1.0 + BODY_PADDING * 2 + 2.0);
}
const topPinCount = top.reduce((s, g) => s + g.pins.length, 0);
const bottomPinCount = bottom.reduce((s, g) => s + g.pins.length, 0);
const topGroupGaps = Math.max(0, top.length - 1) * GROUP_GAP;
const bottomGroupGaps = Math.max(0, bottom.length - 1) * GROUP_GAP;
const topLabelOffsets = top.filter(g => g.name !== '_ungrouped').length * LABEL_OFFSET;
const bottomLabelOffsets = bottom.filter(g => g.name !== '_ungrouped').length * LABEL_OFFSET;
const topWidth = topPinCount * PIN_PITCH + topGroupGaps + topLabelOffsets + BODY_PADDING * 2;
const bottomWidth = bottomPinCount * PIN_PITCH + bottomGroupGaps + bottomLabelOffsets + BODY_PADDING * 2;
if (top.length || bottom.length) {
const valueLen = Math.max(chars(input.symbol_name), chars(input.reference_prefix) + 1);
nameWidth = Math.max(nameWidth, (longestLeft + longestRight + valueLen) * NAME_CHAR_W + 2 * NAME_EXT_OFFSET + 2.0);
}
nameWidth = Math.max(nameWidth, (longestLeft + longestRight) * NAME_CHAR_W + 2 * NAME_EXT_OFFSET + MIN_ICON_MM + 2 * ICON_MARGIN);
const bodyWidth = Math.max(nameWidth, MIN_BODY_WIDTH, topWidth, bottomWidth);
const halfWidth = bodyWidth / 2;
const bodyLeft = -Math.ceil(halfWidth / 2.54) * 2.54;
const bodyRight = Math.ceil(halfWidth / 2.54) * 2.54;
const topTotal = layoutHorizontal(top, 0)[2];
const bottomTotal = layoutHorizontal(bottom, 0)[2];
const [topPins, topLabels] = layoutHorizontal(top, -topTotal / 2);
const [bottomPins, bottomLabels] = layoutHorizontal(bottom, -bottomTotal / 2);
const labelYs = leftLabels.map(l => l.y).concat(rightLabels.map(l => l.y));
const topLabelY = labelYs.length ? Math.max(...labelYs) : 0;
let topPinClearance = 0, bottomPinClearance = 0;
if (topPins.length) {
const ln = topPins.reduce((m, p) => Math.max(m, chars(p.name)), 0);
const ll = topLabels.reduce((m, l) => Math.max(m, chars(l.name)), 0);
topPinClearance = Math.max(ln * FS_PIN_NAME * 0.7, ll * FS_GROUP_LABEL * 0.7) + 2.0;
}
if (bottomPins.length) {
const ln = bottomPins.reduce((m, p) => Math.max(m, chars(p.name)), 0);
const ll = bottomLabels.reduce((m, l) => Math.max(m, chars(l.name)), 0);
bottomPinClearance = Math.max(ln * FS_PIN_NAME * 0.7, ll * FS_GROUP_LABEL * 0.7) + 2.5;
}
let bodyTop = topLabelY + BODY_TOP_MARGIN + topPinClearance;
const pinYs = leftPins.map(p => p.y).concat(rightPins.map(p => p.y));
const lowestPinY = pinYs.length ? Math.min(...pinYs) : -10;
let bodyBottom = lowestPinY - BODY_BOTTOM_MARGIN - bottomPinClearance;
const extName = n => n * NAME_CHAR_W + NAME_EXT_OFFSET;
const extLabel = n => n * LABEL_CHAR_W + LABEL_INSET;
if (topPins.length || bottomPins.length) {
const topExt = Math.max(0, ...topPins.map(p => extName(strip(p.name))), ...topLabels.map(l => extLabel(chars(l.name))));
const botExt = Math.max(0, ...bottomPins.map(p => extName(strip(p.name))), ...bottomLabels.map(l => extLabel(chars(l.name))));
const freeH = (bodyTop - topExt) - (bodyBottom + botExt);
const needH = 2 * DESIGNATOR_INSET + 2 * FS_PIN_NAME + MIN_ICON_MM + 2 * ICON_MARGIN;
if (freeH < needH) {
const grow = Math.ceil((needH - freeH) / 2 / 2.54) * 2.54;
bodyTop += grow; bodyBottom -= grow;
}
}
if (input.chip_outline && !topPins.length && !bottomPins.length) {
const needH = MIN_ICON_MM * 0.6 / OUTLINE_BODY_FRAC + 2 * ICON_MARGIN + 0.5;
const freeH = bodyTop - bodyBottom;
if (freeH < needH) { const grow = Math.ceil((needH - freeH) / 2 / 1.27) * 1.27; bodyTop += grow; bodyBottom -= grow; }
}
let fl = bodyLeft, fr = bodyRight, ft = bodyTop, fb = bodyBottom;
for (const p of leftPins) fl = Math.max(fl, bodyLeft + extName(strip(p.name)));
for (const l of leftLabels) fl = Math.max(fl, bodyLeft + extLabel(chars(l.name)));
for (const p of rightPins) fr = Math.min(fr, bodyRight - extName(strip(p.name)));
for (const l of rightLabels) fr = Math.min(fr, bodyRight - extLabel(chars(l.name)));
for (const p of topPins) ft = Math.min(ft, bodyTop - extName(strip(p.name)));
for (const l of topLabels) ft = Math.min(ft, bodyTop - extLabel(chars(l.name)));
for (const p of bottomPins) fb = Math.max(fb, bodyBottom + extName(strip(p.name)));
for (const l of bottomLabels) fb = Math.max(fb, bodyBottom + extLabel(chars(l.name)));
const free = (fr - fl < 2 || ft - fb < 2) ? [bodyLeft, bodyBottom, bodyRight, bodyTop] : [fl, fb, fr, ft];
const vertical = topPins.length > 0 || bottomPins.length > 0;
const hasVerticalPins = vertical && (free[3] - free[1]) >= 2 * DESIGNATOR_INSET + 2 * FS_PIN_NAME;
const cornerFallback = vertical && !hasVerticalPins;
const L = [];
L.push('(kicad_symbol_lib', ' (version 20231120)', ' (generator "adom")', ` (symbol "${input.symbol_name}"`,
' (pin_names (offset 1.016))', ' (exclude_from_sim no)', ' (in_bom yes)', ' (on_board yes)');
const longestLeftNum = leftPins.reduce((m, p) => Math.max(m, chars(p.number)), 0);
const longestRightNum = rightPins.reduce((m, p) => Math.max(m, chars(p.number)), 0);
const clr = {
top: topPins.length ? bodyTop + PIN_LENGTH + 6.0 : bodyTop + 2.54,
bot: bottomPins.length ? bodyBottom - PIN_LENGTH - 6.0 : bodyBottom - 2.54,
left: bodyLeft - PIN_LENGTH - longestLeftNum * 0.8 - 2.5,
right: bodyRight + PIN_LENGTH + longestRightNum * 0.8 + 2.5,
};
const designators = [];
L.push(` (property "Reference" "${input.reference_prefix}"`);
{
const rp = input.ref_pos || '';
let pos;
if ((rp === '' || rp === 'top' || rp === 'out-top') && hasVerticalPins) pos = 'free-top';
else if ((rp === '' || rp === 'top' || rp === 'out-top') && cornerFallback) pos = 'corner-top';
else if (rp === '') pos = 'out-top';
else pos = rp;
const halfW = chars(input.reference_prefix) * NAME_CHAR_W / 2 + NAME_CHAR_W;
const [rx, ry, just] = designatorAt(pos, bodyLeft, bodyRight, bodyTop, bodyBottom, clr, free, halfW);
designators.push([rx, ry]);
L.push(` (at ${fx(rx)} ${fx(ry)} 0)`, ` (effects (font (size 1.27 1.27))${just})`);
}
L.push(' )');
L.push(` (property "Value" "${input.symbol_name}"`);
if (input.value_pos === 'none') {
const [vx, vy] = designatorAt('out-bot', bodyLeft, bodyRight, bodyTop, bodyBottom, clr, free, 0);
L.push(` (at ${fx(vx)} ${fx(vy)} 0)`, ' (effects (font (size 1.27 1.27)) hide)');
} else {
const vp = input.value_pos || '';
let pos;
if ((vp === '' || vp === 'out-bot' || vp === 'out-bottom') && hasVerticalPins) pos = 'free-bot';
else if ((vp === '' || vp === 'out-bot' || vp === 'out-bottom') && cornerFallback) pos = 'corner-bot';
else if (vp === '') pos = 'out-bot';
else pos = vp;
const halfW = chars(input.symbol_name) * NAME_CHAR_W / 2;
const [vx, vy, just] = designatorAt(pos, bodyLeft, bodyRight, bodyTop, bodyBottom, clr, free, halfW);
designators.push([vx, vy]);
L.push(` (at ${fx(vx)} ${fx(vy)} 0)`, ` (effects (font (size 1.27 1.27))${just})`);
}
L.push(' )');
const baked = input.chip_outline ? bakeChipOutline(input.chip_outline, input.symbol_name, [bodyLeft, bodyBottom, bodyRight, bodyTop], free, designators) : null;
for (const [prop, val] of [['Footprint', input.package], ['Datasheet', input.datasheet_url], ['Description', input.description], ['Manufacturer', input.manufacturer]]) {
L.push(` (property "${prop}" "${val || ''}"`, ' (at 0 0 0)', ' (effects (font (size 1.27 1.27)) (hide yes))', ' )');
}
for (const [dist, pn] of (input.distributor_pn || [])) {
const label = dist ? dist[0].toUpperCase() + dist.slice(1) : '';
L.push(` (property "${label}" "${pn}"`, ' (at 0 0 0)', ' (effects (font (size 1.27 1.27)) (hide yes))', ' )');
}
if (input.chip_outline && baked) {
L.push(` (property "Adom_Chip_Outline" "${input.chip_outline.source}@${input.chip_outline.size}"`, ' (at 0 0 0)', ' (effects (font (size 1.27 1.27)) (hide yes))', ' )');
}
L.push(` (symbol "${input.symbol_name}_0_1"`);
L.push(` (rectangle (start ${fx(bodyLeft)} ${fx(bodyTop)}) (end ${fx(bodyRight)} ${fx(bodyBottom)})`,
' (stroke (width 0.254) (type default))', ' (fill (type background))', ' )');
for (const l of leftLabels) L.push(` (text "${l.name}" (at ${fx(bodyLeft + 1.02)} ${fx(l.y)} 0)`, ' (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify left))', ' )');
for (const l of rightLabels) L.push(` (text "${l.name}" (at ${fx(bodyRight - 1.02)} ${fx(l.y)} 0)`, ' (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify right))', ' )');
for (const l of topLabels) L.push(` (text "${l.name}" (at ${fx(l.x)} ${fx(bodyTop - 1.02)} 900)`, ' (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify right))', ' )');
for (const l of bottomLabels) L.push(` (text "${l.name}" (at ${fx(l.x)} ${fx(bodyBottom + 1.02)} 900)`, ' (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify left))', ' )');
let lasered = false;
if (baked) { L.push(...baked[0]); lasered = baked[1]; }
if (input.chip_name_centered && !lasered) {
L.push(` (text "${input.symbol_name}" (at 0 ${fx((bodyTop + bodyBottom) / 2)} 0)`, ' (effects (font (size 1.27 1.27) (color 100 100 100 1)))', ' )');
}
L.push(' )');
L.push(` (symbol "${input.symbol_name}_1_1"`);
for (const p of leftPins) L.push(` (pin ${p.ptype} line (at ${fx(bodyLeft - PIN_LENGTH)} ${fx(p.y)} 0) (length 2.54) (name "${p.name}") (number "${p.number}"))`);
for (const p of rightPins) L.push(` (pin ${p.ptype} line (at ${fx(bodyRight + PIN_LENGTH)} ${fx(p.y)} 180) (length 2.54) (name "${p.name}") (number "${p.number}"))`);
if (topPins.length) { const e = bodyTop - 0.1; for (const p of topPins) L.push(` (pin ${p.ptype} line (at ${fx(p.x)} ${fx(e + PIN_LENGTH)} 270) (length 2.54) (name "${p.name}") (number "${p.number}"))`); }
if (bottomPins.length) { const e = bodyBottom + 0.1; for (const p of bottomPins) L.push(` (pin ${p.ptype} line (at ${fx(p.x)} ${fx(e - PIN_LENGTH)} 90) (length 2.54) (name "${p.name}") (number "${p.number}"))`); }
L.push(' )', ' )', ')', '');
return L.join('\n');
}
// Mirrors main.rs layout_note / compose_description (the CLI `create` path).
function layoutNote(pinLayout, grouped, explicitSides, centred) {
let shape;
if (pinLayout === 'square') shape = '4-side square symbol, pins in package order';
else if (pinLayout === 'square-grouped') shape = '4-side square symbol';
else if (pinLayout === 'all-sides') shape = explicitSides ? '4-side symbol' : '4-side square symbol';
else shape = '2-column symbol';
const parts = [shape];
if (grouped && pinLayout !== 'square') parts.push('pins grouped by function');
if (centred) parts.push('chip name centred');
return parts.join(', ');
}
function composeDescription(desc, note) {
const d = desc.trim().replace(/\.+$/, '').trim();
if (!note) return d;
if (d.includes(note)) return desc.trim();
return d === '' ? note + '.' : d + '. ' + note + '.';
}
/** Same JSON body as the CLI `create` / the wasm export. */
function generateFromCreateJson(i) {
const prefs = i.preferences || {};
const pins = i.pins || [];
const note = layoutNote(prefs.pinLayout || 'left-right', pins.some(p => p.group != null), pins.some(p => p.side != null), !!prefs.chipNameCentered);
return generateIcSym({
symbol_name: i.symbolName, manufacturer: i.manufacturer || '', package: i.package || '', description: composeDescription(i.description || '', note),
datasheet_url: i.datasheetUrl || '', reference_prefix: i.referencePrefix || 'U',
pins: (i.pins || []).map(p => ({ name: p.name, number: p.number, ptype: p.type, group: p.group == null ? null : p.group, side: p.side == null ? null : p.side })),
pin_layout: prefs.pinLayout || 'left-right', chip_name_centered: !!prefs.chipNameCentered,
ref_pos: prefs.refPos || '', value_pos: prefs.valuePos || '', distributor_pn: [],
// Default ON (the CLI's default too): the generic isometric chip. The CLI
// additionally discovers a real <mpn>-3d-outline-*-named.svg; the browser can't.
chip_outline: (prefs.chipOutline == null ? true : !!prefs.chipOutline) ? genericOutline(prefs.chipSize || 'large') : null,
});
}
const api = { generateIcSym, generateFromCreateJson, fx, fx3, genericOutline, bakeChipOutline, iconBox, chipSizeFrac };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
root.AdomSymGen = api;
})(typeof globalThis !== 'undefined' ? globalThis : this);