586 lines
20 KiB
JavaScript
586 lines
20 KiB
JavaScript
import { layoutAndRoute, netAnchorPoint } from "./layout.js";
|
|
|
|
const GRID = 20;
|
|
|
|
const NET_COLORS = {
|
|
power: "#b54708",
|
|
ground: "#344054",
|
|
signal: "#1d4ed8",
|
|
analog: "#0f766e",
|
|
differential: "#c11574",
|
|
clock: "#b93815",
|
|
bus: "#155eef"
|
|
};
|
|
|
|
function esc(text) {
|
|
return String(text)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|
|
|
|
function normalizeRotation(value) {
|
|
const n = Number(value ?? 0);
|
|
if (!Number.isFinite(n)) {
|
|
return 0;
|
|
}
|
|
const snapped = Math.round(n / 90) * 90;
|
|
let rot = snapped % 360;
|
|
if (rot < 0) {
|
|
rot += 360;
|
|
}
|
|
return rot;
|
|
}
|
|
|
|
function rotatePoint(point, center, rotation) {
|
|
if (!rotation) {
|
|
return point;
|
|
}
|
|
const rad = (rotation * Math.PI) / 180;
|
|
const cos = Math.round(Math.cos(rad));
|
|
const sin = Math.round(Math.sin(rad));
|
|
const dx = point.x - center.x;
|
|
const dy = point.y - center.y;
|
|
return {
|
|
x: Math.round(center.x + dx * cos - dy * sin),
|
|
y: Math.round(center.y + dx * sin + dy * cos)
|
|
};
|
|
}
|
|
|
|
function rotateSide(side, rotation) {
|
|
const steps = normalizeRotation(rotation) / 90;
|
|
const order = ["top", "right", "bottom", "left"];
|
|
const idx = order.indexOf(side);
|
|
if (idx < 0) {
|
|
return side;
|
|
}
|
|
return order[(idx + steps) % 4];
|
|
}
|
|
|
|
function truncate(text, max) {
|
|
const s = String(text ?? "");
|
|
if (s.length <= max) {
|
|
return s;
|
|
}
|
|
return `${s.slice(0, Math.max(1, max - 1))}...`;
|
|
}
|
|
|
|
function netColor(netClass) {
|
|
return NET_COLORS[netClass] ?? NET_COLORS.signal;
|
|
}
|
|
|
|
function symbolTemplateKind(sym) {
|
|
const t = String(sym?.template_name ?? "").toLowerCase();
|
|
if (["resistor", "capacitor", "inductor", "diode", "led", "connector"].includes(t)) {
|
|
return t;
|
|
}
|
|
const c = String(sym?.category ?? "").toLowerCase();
|
|
if (c.includes("resistor")) return "resistor";
|
|
if (c.includes("capacitor")) return "capacitor";
|
|
if (c.includes("inductor")) return "inductor";
|
|
if (c.includes("diode")) return "diode";
|
|
if (c.includes("led")) return "led";
|
|
if (c.includes("connector")) return "connector";
|
|
return null;
|
|
}
|
|
|
|
function renderSymbolBody(sym, x, y, width, height) {
|
|
const kind = symbolTemplateKind(sym);
|
|
if (!kind) {
|
|
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="8" fill="#ffffff" stroke="#1f2937" stroke-width="2" />`;
|
|
}
|
|
|
|
const midX = x + width / 2;
|
|
const midY = y + height / 2;
|
|
const left = x + 16;
|
|
const right = x + width - 16;
|
|
const top = y + 14;
|
|
const bottom = y + height - 14;
|
|
const body = [];
|
|
body.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="8" fill="#fffdfa" stroke="#1f2937" stroke-width="2" />`);
|
|
|
|
if (kind === "resistor") {
|
|
const y0 = midY;
|
|
const pts = [
|
|
[left, y0],
|
|
[left + 16, y0 - 10],
|
|
[left + 28, y0 + 10],
|
|
[left + 40, y0 - 10],
|
|
[left + 52, y0 + 10],
|
|
[left + 64, y0 - 10],
|
|
[right, y0]
|
|
];
|
|
body.push(`<polyline points="${pts.map((p) => `${p[0]},${p[1]}`).join(" ")}" fill="none" stroke="#344054" stroke-width="2" />`);
|
|
} else if (kind === "capacitor") {
|
|
body.push(`<line x1="${midX - 10}" y1="${top}" x2="${midX - 10}" y2="${bottom}" stroke="#344054" stroke-width="2" />`);
|
|
body.push(`<line x1="${midX + 10}" y1="${top}" x2="${midX + 10}" y2="${bottom}" stroke="#344054" stroke-width="2" />`);
|
|
body.push(`<line x1="${left}" y1="${midY}" x2="${midX - 10}" y2="${midY}" stroke="#344054" stroke-width="2" />`);
|
|
body.push(`<line x1="${midX + 10}" y1="${midY}" x2="${right}" y2="${midY}" stroke="#344054" stroke-width="2" />`);
|
|
} else if (kind === "inductor") {
|
|
body.push(`<line x1="${left}" y1="${midY}" x2="${left + 10}" y2="${midY}" stroke="#344054" stroke-width="2" />`);
|
|
for (let i = 0; i < 4; i += 1) {
|
|
const cx = left + 18 + i * 16;
|
|
body.push(`<path d="M ${cx - 8} ${midY} A 8 8 0 0 1 ${cx + 8} ${midY}" fill="none" stroke="#344054" stroke-width="2" />`);
|
|
}
|
|
body.push(`<line x1="${right - 10}" y1="${midY}" x2="${right}" y2="${midY}" stroke="#344054" stroke-width="2" />`);
|
|
} else if (kind === "diode" || kind === "led") {
|
|
const triLeft = left + 12;
|
|
const triRight = midX + 6;
|
|
body.push(`<line x1="${left}" y1="${midY}" x2="${triLeft}" y2="${midY}" stroke="#344054" stroke-width="2" />`);
|
|
body.push(`<path d="M ${triLeft} ${midY - 14} L ${triLeft} ${midY + 14} L ${triRight} ${midY} Z" fill="none" stroke="#344054" stroke-width="2" />`);
|
|
body.push(`<line x1="${triRight + 4}" y1="${midY - 16}" x2="${triRight + 4}" y2="${midY + 16}" stroke="#344054" stroke-width="2" />`);
|
|
body.push(`<line x1="${triRight + 4}" y1="${midY}" x2="${right}" y2="${midY}" stroke="#344054" stroke-width="2" />`);
|
|
if (kind === "led") {
|
|
body.push(`<line x1="${triRight - 2}" y1="${midY - 20}" x2="${triRight + 8}" y2="${midY - 30}" stroke="#b42318" stroke-width="1.6" />`);
|
|
body.push(`<line x1="${triRight + 10}" y1="${midY - 18}" x2="${triRight + 20}" y2="${midY - 28}" stroke="#b42318" stroke-width="1.6" />`);
|
|
}
|
|
} else if (kind === "connector") {
|
|
body.push(`<line x1="${midX}" y1="${top + 6}" x2="${midX}" y2="${bottom - 6}" stroke="#98a2b3" stroke-width="1.6" />`);
|
|
body.push(`<circle cx="${midX}" cy="${midY - 16}" r="5" fill="#fff" stroke="#344054" stroke-width="1.6" />`);
|
|
body.push(`<circle cx="${midX}" cy="${midY + 16}" r="5" fill="#fff" stroke="#344054" stroke-width="1.6" />`);
|
|
}
|
|
|
|
return body.join("");
|
|
}
|
|
|
|
function pinNetMap(model) {
|
|
const map = new Map();
|
|
for (const net of model.nets) {
|
|
for (const node of net.nodes) {
|
|
const key = `${node.ref}.${node.pin}`;
|
|
const list = map.get(key) ?? [];
|
|
list.push(net.name);
|
|
map.set(key, list);
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function renderWirePath(pathD, netName, netClass) {
|
|
const color = netColor(netClass);
|
|
return [
|
|
`<path d="${pathD}" fill="none" stroke="#f8fafc" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" data-net="${esc(netName)}" data-net-class="${esc(netClass)}" />`,
|
|
`<path d="${pathD}" fill="none" stroke="${color}" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" data-net="${esc(netName)}" data-net-class="${esc(netClass)}" />`
|
|
].join("");
|
|
}
|
|
|
|
function renderNetLabel(x, y, netName, netClass, bold = false) {
|
|
const color = netColor(netClass);
|
|
const weight = bold ? "700" : "600";
|
|
return `<text x="${x}" y="${y}" font-size="10" font-weight="${weight}" fill="${color}" stroke="#f8fafc" stroke-width="3" paint-order="stroke fill" data-net-label="${esc(netName)}">${esc(netName)}</text>`;
|
|
}
|
|
|
|
function isGroundLikeNet(net) {
|
|
const cls = String(net?.class ?? "").trim().toLowerCase();
|
|
if (cls === "ground") {
|
|
return true;
|
|
}
|
|
const name = String(net?.name ?? "").trim().toLowerCase();
|
|
return name === "gnd" || name === "ground" || name.endsWith("_gnd");
|
|
}
|
|
|
|
function tieLabelPoint(point, netClass) {
|
|
if (netClass === "power") {
|
|
return { x: point.x + 8, y: point.y - 10 };
|
|
}
|
|
return { x: point.x + 8, y: point.y - 8 };
|
|
}
|
|
|
|
function distance(a, b) {
|
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
}
|
|
|
|
function pickLabelPoints(points, maxCount, used, minSpacing, avoidPoints = []) {
|
|
const accepted = [];
|
|
|
|
for (const p of points) {
|
|
if (accepted.length >= maxCount) {
|
|
break;
|
|
}
|
|
|
|
let blocked = false;
|
|
for (const prev of used) {
|
|
if (distance(p, prev) < minSpacing) {
|
|
blocked = true;
|
|
break;
|
|
}
|
|
}
|
|
if (blocked) {
|
|
continue;
|
|
}
|
|
|
|
for (const pin of avoidPoints) {
|
|
if (distance(p, pin) < minSpacing * 0.9) {
|
|
blocked = true;
|
|
break;
|
|
}
|
|
}
|
|
if (blocked) {
|
|
continue;
|
|
}
|
|
|
|
accepted.push(p);
|
|
used.push(p);
|
|
}
|
|
|
|
return accepted;
|
|
}
|
|
|
|
function renderGroundSymbol(x, y, netName) {
|
|
const y0 = y + 3;
|
|
const y1 = y + 7;
|
|
const y2 = y + 10;
|
|
const y3 = y + 13;
|
|
return `
|
|
<g data-net-tie="${esc(netName)}">
|
|
<line x1="${x}" y1="${y}" x2="${x}" y2="${y0}" stroke="#344054" stroke-width="1.4" />
|
|
<line x1="${x - 6}" y1="${y1}" x2="${x + 6}" y2="${y1}" stroke="#344054" stroke-width="1.4" />
|
|
<line x1="${x - 4}" y1="${y2}" x2="${x + 4}" y2="${y2}" stroke="#344054" stroke-width="1.4" />
|
|
<line x1="${x - 2}" y1="${y3}" x2="${x + 2}" y2="${y3}" stroke="#344054" stroke-width="1.4" />
|
|
</g>`;
|
|
}
|
|
|
|
function renderPowerSymbol(x, y, netName) {
|
|
return `
|
|
<g data-net-tie="${esc(netName)}">
|
|
<line x1="${x}" y1="${y}" x2="${x}" y2="${y - 4}" stroke="#b54708" stroke-width="1.6" />
|
|
<path d="M ${x - 5} ${y + 2} L ${x} ${y - 8} L ${x + 5} ${y + 2} Z" fill="#b54708" stroke="#f8fafc" stroke-width="1" />
|
|
</g>`;
|
|
}
|
|
|
|
function renderGenericTie(x, y, netName, netClass) {
|
|
const color = netColor(netClass);
|
|
return `<circle cx="${x}" cy="${y}" r="3" fill="#ffffff" stroke="${color}" stroke-width="1.3" data-net-tie="${esc(netName)}" />`;
|
|
}
|
|
|
|
function renderTieSymbol(x, y, netName, netClass) {
|
|
if (netClass === "ground") {
|
|
return renderGroundSymbol(x, y, netName);
|
|
}
|
|
if (netClass === "power") {
|
|
return renderPowerSymbol(x, y, netName);
|
|
}
|
|
return renderGenericTie(x, y, netName, netClass);
|
|
}
|
|
|
|
function representativePoint(routeInfo, netAnchor) {
|
|
if (routeInfo.labelPoints?.length) {
|
|
return routeInfo.labelPoints[0];
|
|
}
|
|
if (routeInfo.tiePoints?.length) {
|
|
return routeInfo.tiePoints[0];
|
|
}
|
|
if (routeInfo.routes?.length && routeInfo.routes[0].length) {
|
|
const seg = routeInfo.routes[0][0];
|
|
return {
|
|
x: (seg.a.x + seg.b.x) / 2,
|
|
y: (seg.a.y + seg.b.y) / 2
|
|
};
|
|
}
|
|
return netAnchor;
|
|
}
|
|
|
|
function renderLegend() {
|
|
const entries = [
|
|
["power", "Power"],
|
|
["ground", "Ground"],
|
|
["clock", "Clock"],
|
|
["signal", "Signal"],
|
|
["analog", "Analog"]
|
|
];
|
|
|
|
const rows = entries
|
|
.map(
|
|
([cls, label], idx) =>
|
|
`<g transform="translate(0 ${idx * 14})"><line x1="0" y1="6" x2="16" y2="6" stroke="${netColor(cls)}" stroke-width="2" /><text x="22" y="9" fill="#475467" font-size="10">${label}</text></g>`
|
|
)
|
|
.join("");
|
|
|
|
return `
|
|
<g data-layer="legend" transform="translate(14 14)">
|
|
<rect x="-8" y="-8" width="120" height="82" rx="6" fill="#ffffffd8" stroke="#d0d5dd" />
|
|
${rows}
|
|
</g>`;
|
|
}
|
|
|
|
export function renderSvgFromLayout(model, layout, options = {}) {
|
|
const showLabels = options.show_labels !== false;
|
|
const pinNets = pinNetMap(model);
|
|
const netClassByName = new Map((model.nets ?? []).map((n) => [n.name, n.class]));
|
|
const allPinPoints = [];
|
|
|
|
const components = layout.placed
|
|
.map((inst) => {
|
|
const sym = model.symbols[inst.symbol];
|
|
const x = inst.placement.x;
|
|
const y = inst.placement.y;
|
|
const rotation = normalizeRotation(inst.placement.rotation ?? 0);
|
|
const cx = x + sym.body.width / 2;
|
|
const cy = y + sym.body.height / 2;
|
|
const templateKind = symbolTemplateKind(sym);
|
|
const compactLabel = templateKind || sym.body.width <= 140 || sym.body.height <= 90;
|
|
const legacyShowInstanceNetLabels = Boolean(inst.properties?.show_net_labels);
|
|
const pinUi =
|
|
inst.properties?.pin_ui && typeof inst.properties.pin_ui === "object" && !Array.isArray(inst.properties.pin_ui)
|
|
? inst.properties.pin_ui
|
|
: {};
|
|
|
|
const pinCircles = [];
|
|
const pinLabels = [];
|
|
const instanceNetLabels = [];
|
|
for (const pin of sym.pins) {
|
|
let px = x;
|
|
let py = y;
|
|
|
|
if (pin.side === "left") {
|
|
px = x;
|
|
py = y + pin.offset;
|
|
} else if (pin.side === "right") {
|
|
px = x + sym.body.width;
|
|
py = y + pin.offset;
|
|
} else if (pin.side === "top") {
|
|
px = x + pin.offset;
|
|
py = y;
|
|
} else {
|
|
px = x + pin.offset;
|
|
py = y + sym.body.height;
|
|
}
|
|
|
|
pinCircles.push(
|
|
`<circle cx="${px}" cy="${py}" r="3.2" fill="#111827" data-pin-ref="${esc(inst.ref)}" data-pin-name="${esc(pin.name)}" data-pin-nets="${esc((pinNets.get(`${inst.ref}.${pin.name}`) ?? []).join(","))}" />`
|
|
);
|
|
|
|
const rotated = rotatePoint({ x: px, y: py }, { x: cx, y: cy }, rotation);
|
|
const rx = rotated.x;
|
|
const ry = rotated.y;
|
|
const rotatedSide = rotateSide(pin.side, rotation);
|
|
|
|
allPinPoints.push({ x: rx, y: ry });
|
|
let labelX = rx + 6;
|
|
let labelY = ry - 4;
|
|
let textAnchor = "start";
|
|
|
|
if (rotatedSide === "right") {
|
|
labelX = rx - 6;
|
|
labelY = ry - 4;
|
|
textAnchor = "end";
|
|
} else if (rotatedSide === "top") {
|
|
labelX = rx + 4;
|
|
labelY = ry + 12;
|
|
textAnchor = "start";
|
|
} else if (rotatedSide === "bottom") {
|
|
labelX = rx + 4;
|
|
labelY = ry - 8;
|
|
textAnchor = "start";
|
|
}
|
|
|
|
const showPinLabel = !templateKind || !/^\d+$/.test(pin.name);
|
|
if (showPinLabel) {
|
|
pinLabels.push(
|
|
`<text x="${labelX}" y="${labelY}" text-anchor="${textAnchor}" font-size="10" fill="#475467" data-pin-label="${esc(inst.ref)}.${esc(pin.name)}">${esc(pin.name)}</text>`
|
|
);
|
|
}
|
|
|
|
const pinUiEntry = pinUi[pin.name];
|
|
const showPinNetLabel =
|
|
pinUiEntry && typeof pinUiEntry === "object" && Object.prototype.hasOwnProperty.call(pinUiEntry, "show_net_label")
|
|
? Boolean(pinUiEntry.show_net_label)
|
|
: legacyShowInstanceNetLabels;
|
|
if (showPinNetLabel && showLabels) {
|
|
const nets = pinNets.get(`${inst.ref}.${pin.name}`) ?? [];
|
|
const displayNet = nets.find((n) => !isGroundLikeNet({ name: n, class: "" })) ?? nets[0];
|
|
if (displayNet) {
|
|
let netX = labelX;
|
|
let netY = labelY;
|
|
let netAnchor = textAnchor;
|
|
|
|
if (rotatedSide === "left") {
|
|
netX = rx - 12;
|
|
netY = ry - 10;
|
|
netAnchor = "end";
|
|
} else if (rotatedSide === "right") {
|
|
netX = rx + 12;
|
|
netY = ry - 10;
|
|
netAnchor = "start";
|
|
} else if (rotatedSide === "top") {
|
|
netX = rx + 8;
|
|
netY = ry - 10;
|
|
netAnchor = "start";
|
|
} else {
|
|
netX = rx + 8;
|
|
netY = ry + 14;
|
|
netAnchor = "start";
|
|
}
|
|
|
|
instanceNetLabels.push(
|
|
`<text x="${netX}" y="${netY}" text-anchor="${netAnchor}" font-size="10" font-weight="700" fill="${netColor(netClassByName.get(displayNet))}" stroke="#f8fafc" stroke-width="2.6" paint-order="stroke fill" data-net-label="${esc(displayNet)}" data-ref-net-label="${esc(inst.ref)}">${esc(displayNet)}</text>`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const pinLabelsSvg = [...pinLabels, ...instanceNetLabels].join("");
|
|
const pinCoreSvg = pinCircles.join("");
|
|
|
|
const rotationTransform = rotation ? ` transform="rotate(${rotation} ${cx} ${cy})"` : "";
|
|
|
|
const refLabel = truncate(inst.ref, compactLabel ? 6 : 10);
|
|
const valueLabel = truncate(inst.properties?.value ?? inst.symbol, compactLabel ? 18 : 28);
|
|
const refY = compactLabel ? y - 6 : y + 18;
|
|
const valueY = compactLabel ? y + sym.body.height + 14 : y + 34;
|
|
|
|
return `
|
|
<g data-ref="${esc(inst.ref)}" data-symbol="${esc(inst.symbol)}">
|
|
<g${rotationTransform}>
|
|
${renderSymbolBody(sym, x, y, sym.body.width, sym.body.height)}
|
|
${pinCoreSvg}
|
|
</g>
|
|
${pinLabelsSvg}
|
|
<text x="${x + 8}" y="${refY}" font-size="${compactLabel ? 11 : 12}" font-weight="700" fill="#111827" stroke="#f8fafc" stroke-width="2.2" paint-order="stroke fill" data-ref-label="${esc(inst.ref)}">${esc(refLabel)}</text>
|
|
<text x="${x + 8}" y="${valueY}" font-size="${compactLabel ? 9 : 10}" fill="#667085" stroke="#f8fafc" stroke-width="2" paint-order="stroke fill" data-value-label="${esc(inst.ref)}">${esc(valueLabel)}</text>
|
|
</g>`;
|
|
})
|
|
.join("\n");
|
|
|
|
const wires = layout.routed
|
|
.flatMap((rn) =>
|
|
rn.routes.map((route) => {
|
|
const path = route
|
|
.map((seg, idx) => `${idx === 0 ? "M" : "L"} ${seg.a.x} ${seg.a.y} L ${seg.b.x} ${seg.b.y}`)
|
|
.join(" ");
|
|
return renderWirePath(path, rn.net.name, rn.net.class);
|
|
})
|
|
)
|
|
.join("\n");
|
|
|
|
const junctions = layout.routed
|
|
.flatMap((rn) =>
|
|
(rn.junctionPoints ?? []).map((p) => {
|
|
const color = netColor(rn.net.class);
|
|
return `<circle cx="${p.x}" cy="${p.y}" r="3" fill="${color}" stroke="#f8fafc" stroke-width="1.3" data-net-junction="${esc(rn.net.name)}" />`;
|
|
})
|
|
)
|
|
.join("\n");
|
|
|
|
const tiePoints = layout.routed
|
|
.flatMap((rn) =>
|
|
(rn.tiePoints ?? []).map((p) => renderTieSymbol(p.x, p.y, rn.net.name, rn.net.class))
|
|
)
|
|
.join("\n");
|
|
|
|
const routedByName = new Map(layout.routed.map((r) => [r.net.name, r]));
|
|
const usedLabelPoints = [];
|
|
const labels = [];
|
|
const tieLabels = [];
|
|
|
|
for (const net of model.nets) {
|
|
if (isGroundLikeNet(net)) {
|
|
continue;
|
|
}
|
|
|
|
const routeInfo = routedByName.get(net.name);
|
|
if (routeInfo?.isBusMember && routeInfo.mode === "label_tie") {
|
|
continue;
|
|
}
|
|
|
|
const netAnchor = netAnchorPoint(net, model, layout.placed);
|
|
const candidates = [];
|
|
|
|
if (routeInfo?.mode === "label_tie") {
|
|
candidates.push(...(routeInfo?.labelPoints ?? []));
|
|
const selected = pickLabelPoints(candidates, 1, usedLabelPoints, GRID * 2.4, allPinPoints);
|
|
for (const p of selected) {
|
|
labels.push(renderNetLabel(p.x, p.y, net.name, net.class, true));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (routeInfo?.labelPoints?.length) {
|
|
candidates.push(...routeInfo.labelPoints);
|
|
}
|
|
if (netAnchor) {
|
|
candidates.push({ x: netAnchor.x + 8, y: netAnchor.y - 8 });
|
|
}
|
|
|
|
const selected = pickLabelPoints(candidates, 1, usedLabelPoints, GRID * 2.4, allPinPoints);
|
|
for (const p of selected) {
|
|
labels.push(renderNetLabel(p.x, p.y, net.name, net.class));
|
|
}
|
|
}
|
|
|
|
if (showLabels) {
|
|
const usedTieLabels = [];
|
|
for (const rn of layout.routed) {
|
|
if (rn.mode !== "label_tie" || isGroundLikeNet(rn.net)) {
|
|
continue;
|
|
}
|
|
|
|
const candidates = (rn.tiePoints ?? []).map((p) => tieLabelPoint(p, rn.net.class));
|
|
if (!candidates.length) {
|
|
continue;
|
|
}
|
|
|
|
const maxPerNet = rn.net.class === "power" ? Math.min(6, candidates.length) : Math.min(2, candidates.length);
|
|
const selected = pickLabelPoints(candidates, maxPerNet, usedTieLabels, GRID * 1.5, allPinPoints);
|
|
for (const p of selected) {
|
|
tieLabels.push(renderNetLabel(p.x, p.y, rn.net.name, rn.net.class, true));
|
|
}
|
|
}
|
|
}
|
|
|
|
const busLabels = (layout.bus_groups ?? [])
|
|
.map((group) => {
|
|
const reps = group.nets
|
|
.map((netName) => {
|
|
const net = model.nets.find((n) => n.name === netName);
|
|
if (!net) {
|
|
return null;
|
|
}
|
|
const anchor = netAnchorPoint(net, model, layout.placed);
|
|
return representativePoint(routedByName.get(netName), anchor);
|
|
})
|
|
.filter(Boolean);
|
|
|
|
if (!reps.length) {
|
|
return "";
|
|
}
|
|
|
|
const x = reps.reduce((sum, p) => sum + p.x, 0) / reps.length;
|
|
const y = reps.reduce((sum, p) => sum + p.y, 0) / reps.length;
|
|
|
|
return `<text x="${x + 12}" y="${y - 12}" font-size="11" font-weight="700" fill="#155eef" stroke="#f8fafc" stroke-width="3" paint-order="stroke fill" data-bus-group="${esc(group.name)}">${esc(group.name)} bus</text>`;
|
|
})
|
|
.join("\n");
|
|
|
|
const annotations = (model.annotations ?? [])
|
|
.map((a, idx) => {
|
|
const x = a.x ?? 16;
|
|
const y = a.y ?? 24 + idx * 16;
|
|
return `<text x="${x}" y="${y}" font-size="11" fill="#6b7280">${esc(a.text)}</text>`;
|
|
})
|
|
.join("\n");
|
|
|
|
const labelLayer = showLabels ? [...labels, ...tieLabels].join("\n") : "";
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${layout.width} ${layout.height}" width="${layout.width}" height="${layout.height}" data-engine="schemeta-v2">
|
|
<g data-layer="background">
|
|
<rect x="0" y="0" width="${layout.width}" height="${layout.height}" fill="#f8fafc" />
|
|
</g>
|
|
<g data-layer="components">${components}</g>
|
|
<g data-layer="wires">${wires}</g>
|
|
<g data-layer="junctions">${junctions}</g>
|
|
<g data-layer="ties">${tiePoints}</g>
|
|
<g data-layer="net-labels">${labelLayer}</g>
|
|
<g data-layer="bus-groups">${busLabels}</g>
|
|
<g data-layer="annotations">${annotations}</g>
|
|
${renderLegend()}
|
|
</svg>`;
|
|
}
|
|
|
|
export function renderSvg(model, options = {}) {
|
|
const layout = layoutAndRoute(model, options);
|
|
return renderSvgFromLayout(model, layout, options);
|
|
}
|