component
CR0402-FX-6803GLF
Public Unreviewedby AravK
Bourns · Thick Film Chip Resistor · General Purpose
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CR0402-FX-6803GLF</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0d1117;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
color: #e6edf3;
}
/* ── Top toolbar ── */
.toolbar {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: #161b22;
border-bottom: 1px solid #21262d;
flex-shrink: 0;
font-size: 13px;
}
.toolbar .title {
font-weight: 600;
color: #00b8b0;
font-size: 15px;
letter-spacing: 0.02em;
}
.toolbar .spacer { flex: 1; }
.toolbar .sep {
width: 1px;
height: 22px;
background: #21262d;
margin: 0 4px;
flex-shrink: 0;
}
.toolbar .btn-group {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.toolbar button {
background: rgba(0, 184, 176, 0.12);
border: 1px solid rgba(0, 184, 176, 0.25);
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
font-size: 12px;
color: #8b949e;
transition: all 0.15s;
}
.toolbar button:hover { background: rgba(0,184,176,0.18); color: #e6edf3; border-color: rgba(0,184,176,0.4); }
.toolbar button:active { background: rgba(0,184,176,0.28); }
.toolbar button.active { background: rgba(0,184,176,0.2); color: #00b8b0; border-color: rgba(0,184,176,0.4); }
.toolbar .zoom-level {
font-size: 11px;
color: #8b949e;
min-width: 44px;
text-align: center;
font-variant-numeric: tabular-nums;
}
/* ── Canvas (dark background) ── */
.canvas-wrap {
flex: 1;
overflow: hidden;
position: relative;
cursor: grab;
background-color: #0d1117;
background-image:
radial-gradient(ellipse at 50% 50%, rgba(0,184,176,0.03) 0%, transparent 70%),
linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px);
background-size: 100% 100%, 20px 20px, 20px 20px;
}
.canvas-wrap.dragging { cursor: grabbing; }
.canvas-wrap.no-grid {
background-image: radial-gradient(ellipse at 50% 50%, rgba(0,184,176,0.03) 0%, transparent 70%);
background-size: 100% 100%;
}
.canvas {
position: absolute;
transform-origin: 0 0;
}
.canvas svg { display: block; }
/* ── Tooltip ── */
.tooltip {
display: none;
position: fixed;
background: #21262d;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
color: #e6edf3;
padding: 10px 14px;
border-radius: 10px;
border: 1px solid rgba(0, 184, 176, 0.25);
font-size: 12px;
line-height: 1.6;
pointer-events: none;
user-select: none;
z-index: 100;
max-width: 300px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
transition: border-color 0.15s;
}
.tooltip.visible { display: block; }
.tooltip.sticky {
pointer-events: auto;
user-select: text;
border-color: #00e6dc;
box-shadow: 0 8px 32px rgba(0,184,176,0.15), 0 8px 24px rgba(0,0,0,0.4);
}
.tooltip .tt-name { font-weight: 700; font-size: 15px; color: #00e6dc; }
.tooltip .tt-pin { color: #8b949e; font-size: 11px; margin-left: 4px; }
.tooltip .tt-type {
display: inline-block;
margin-top: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.tooltip .tt-desc {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid #21262d;
color: #8b949e;
font-size: 11px;
line-height: 1.5;
}
.tooltip .tt-source {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid #21262d;
font-size: 10px;
color: #484f58;
}
.tooltip .tt-source a { color: #00b8b0; text-decoration: none; }
.tooltip .tt-source a:hover { text-decoration: underline; }
.tooltip .tt-source-label { color: #E6B450; font-weight: 600; }
.tooltip .tt-hint {
margin-top: 6px;
font-size: 9px;
color: #484f58;
font-style: italic;
}
.tooltip .tt-group-name { font-weight: 700; font-size: 14px; color: #E6B450; }
.tooltip .tt-group-badge {
display: inline-block;
margin-top: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
background: rgba(230,180,80,0.15);
color: #FFD180;
border: 1px solid rgba(230,180,80,0.3);
}
/* ── Wiki edit button ── */
.edit-btn {
position: absolute; top: 10px; right: 10px; z-index: 100;
display: flex; align-items: center; gap: 6px;
padding: 5px 10px;
background: rgba(22,27,34,0.75);
backdrop-filter: blur(8px);
border: 1px solid rgba(230,180,80,0.2);
border-radius: 6px;
color: #8b949e; font-size: 11px;
cursor: pointer; text-decoration: none;
transition: all 0.2s; opacity: 0.7;
}
.edit-btn:hover {
opacity: 1;
background: rgba(230,180,80,0.12);
border-color: rgba(230,180,80,0.4);
color: #FFD180;
}
.edit-btn svg { width: 12px; height: 12px; }
/* Regen prompt modal */
.regen-overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.6);
z-index: 2000;
align-items: center; justify-content: center;
}
.regen-overlay.visible { display: flex; }
.regen-modal {
background: undefined;
border: 1px solid undefined;
border-radius: 10px;
padding: 20px 24px;
max-width: 520px;
width: 90%;
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
.regen-modal h3 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: undefined;
}
.regen-modal .prompt-text {
background: undefined;
border: 1px solid undefined;
border-radius: 6px;
padding: 12px 14px;
font-size: 13px;
line-height: 1.5;
color: undefined;
white-space: pre-wrap;
word-break: break-word;
user-select: all;
}
.regen-modal .modal-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14px;
}
.regen-modal .copy-status {
font-size: 11px;
color: undefined;
}
.regen-modal .copy-status.copied { color: undefined; }
.regen-modal .modal-btns { display: flex; gap: 8px; }
.regen-modal .modal-btns button {
padding: 6px 14px;
border-radius: 6px;
border: 1px solid undefined;
background: undefined;
color: undefined;
font-size: 12px;
cursor: pointer;
}
.regen-modal .modal-btns button:hover { background: rgba(255,255,255,0.06); }
.regen-modal .modal-btns .copy-btn {
background: rgba(0,230,220,0.12);
border-color: rgba(0,230,220,0.3);
color: undefined;
}
.regen-modal .modal-btns .copy-btn:hover { background: rgba(0,230,220,0.22); }
/* Pin highlight on hover */
.canvas svg .pin-highlight path, .canvas svg path.pin-highlight { stroke: #00e6dc !important; }
.canvas svg .group-highlight path { stroke: #FFD180 !important; }
/* Discrete pin label highlight (injected <text> elements) */
.canvas svg text.pin-label-highlight { fill: #00e6dc !important; }
.canvas svg text.pin-num-highlight { fill: #00e6dc !important; }
/* Discrete pin hit zone outline (visible on hover) */
.canvas svg rect.pin-hit-zone { fill: rgba(0,230,220,0.06); stroke: #00e6dc; stroke-width: 0.12; stroke-dasharray: 0.3,0.2; stroke-opacity: 0.5; pointer-events: all; cursor: pointer; rx: 0.3; }
.canvas svg rect.pin-hit-zone:hover { fill: rgba(0,230,220,0.12); stroke-opacity: 0.8; stroke-width: 0.15; }
/* ── Bottom panel ── */
.bottom-panel {
background: #161b22;
border-top: 1px solid #21262d;
flex-shrink: 0;
}
.info-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 1px 0;
padding: 5px 8px;
font-size: 10px;
}
.info-item {
display: inline-flex;
align-items: baseline;
gap: 5px;
padding: 1px 0;
margin-right: 8px;
}
.info-item::after {
content: '';
display: inline-block;
width: 1px;
height: 10px;
background: #21262d;
margin-left: 8px;
vertical-align: middle;
}
.info-item:last-child::after { display: none; }
.info-item:last-child { margin-right: 0; }
.info-label {
color: #484f58;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 9px;
}
.info-value { color: #8b949e; }
.info-value a { color: #00b8b0; text-decoration: none; }
.info-value a:hover { text-decoration: underline; }
.info-divider { height: 1px; background: #21262d; margin: 0 8px; }
.coord-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
font-size: 10px;
color: #8b949e;
font-variant-numeric: tabular-nums;
}
</style>
</head>
<body>
<div class="toolbar">
<span class="title">CR0402-FX-6803GLF</span>
<div class="spacer"></div>
<div class="btn-group">
<button onclick="zoomOut()">−</button>
<span class="zoom-level" id="zoom-level">100%</span>
<button onclick="zoomIn()">+</button>
</div>
<div class="sep"></div>
<button onclick="zoomFit()">Fit</button>
<button onclick="zoomReset()">1:1</button>
<button onclick="toggleGrid()" id="grid-btn" class="active">Grid</button>
<button onclick="toggleBBox()" id="bbox-btn" class="active">BBox</button>
<button onclick="toggleHits()" id="hits-btn">Hits</button>
<div class="sep"></div>
<button onclick="showRegenPrompt()" title="Show regeneration prompt to paste into Claude Code">Regen</button>
</div>
<div class="regen-overlay" id="regen-overlay" onclick="if(event.target===this)closeRegenPrompt()">
<div class="regen-modal">
<h3>Regeneration Prompt</h3>
<div class="prompt-text" id="regen-prompt-text"></div>
<div class="modal-footer">
<span class="copy-status" id="regen-copy-status">Copied to clipboard</span>
<div class="modal-btns">
<button onclick="closeRegenPrompt()">Close</button>
<button class="copy-btn" onclick="copyRegenText()">Copy</button>
</div>
</div>
</div>
</div>
<div class="canvas-wrap" id="canvas-wrap">
<a class="edit-btn" href="https://coder.john-service-wiki-a054078430ce38e4.containers.adom.inc/proxy/8785/wiki/components/cr0402-fx-6803glf/edit?field=content" target="_blank" title="Suggest corrections to this symbol on the Adom Wiki">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z"/>
<path d="M9.5 3.5l3 3"/>
</svg>
Suggest edit
</a>
<div class="canvas" id="canvas">
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="5.0292mm" height="10.0584mm" viewBox="-4.7567 -4.0808 12.6320 18.2200">
<defs>
<linearGradient id="adom-body-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a2332"/>
<stop offset="100%" stop-color="#141c27"/>
</linearGradient>
</defs>
<style>
/* Stroked text groups (KiCad renders text as paths in <g class="stroked-text">) */
g.stroked-text path {
stroke: #ffffff !important;
fill: #ffffff !important;
}
/* Hidden text elements (used for hit detection) */
text[opacity="0"] {
fill: #ffffff !important;
}
/* Pin highlight state (toggled by JS) */
.pin-highlight path {
stroke: #00e6dc !important;
}
.pin-highlight g.stroked-text path {
stroke: #00e6dc !important;
}
/* Group label highlight */
.group-highlight g.stroked-text path {
stroke: #FFD180 !important;
}
</style>
<desc>Image generated by Eeschema-SVG </desc>
<g style="fill:none; fill-opacity:1.0000;stroke:#000000; stroke-opacity:1.0000;
stroke-linecap:round; stroke-linejoin:round;"
transform="translate(0 0) scale(1 1)">
</g>
<g style="fill:none;
stroke:#4a5568; stroke-width:0.2540; stroke-opacity:1;
stroke-linecap:round; stroke-linejoin:round;">
<rect x="0.543300" y="2.489200" width="2.032000" height="5.080000" rx="0.000000" />
</g>
<g style="fill:none;
stroke:#4a5568; stroke-width:0.1524; stroke-opacity:1;
stroke-linecap:round; stroke-linejoin:round;">
<path d="M1.5593 2.4892
L1.5593 1.2192
" />
<path d="M1.5593 7.5692
L1.5593 8.8392
" />
</g>
<g style="fill:none;
stroke:#484f58; stroke-width:0.1524; stroke-opacity:1;
stroke-linecap:round; stroke-linejoin:round;">
<g transform="rotate(-90.000000 3.5913 5.0292)">
</g>
<g transform="rotate(-90.000000 1.5593 5.0292)">
</g>
</g>
<text x="1.559" y="0.219" font-size="1.1" font-family="sans-serif" fill="#ffffff" text-anchor="middle" class="pin-num-label">1</text>
<text x="1.559" y="10.939" font-size="1.1" font-family="sans-serif" fill="#ffffff" text-anchor="middle" class="pin-num-label">2</text>
<rect x="-4.4567" y="-3.7807999999999997" width="12.032" height="17.619999999999997" fill="none" stroke="#00e6dc" stroke-width="0.1" stroke-dasharray="0.3,0.3" opacity="0.4" class="debug-bbox"/></svg>
</div>
</div>
<div class="tooltip" id="tooltip"></div>
<div class="bottom-panel">
<div class="info-row">
<div class="info-item"><span class="info-label">Pins</span><span class="info-value">2</span></div><div class="info-item"><span class="info-label">Datasheet</span><span class="info-value"><a href="~" target="_blank">PDF</a></span></div><div class="info-item"><span class="info-label">Desc</span><span class="info-value">Resistor</span></div>
</div>
<div class="info-divider"></div>
<div class="coord-row">
<span id="cursor-pos">X: 0.00 Y: 0.00 mm</span>
<span style="margin-left:auto;color:#00b8b0;font-weight:600;font-size:12px">SymView</span>
</div>
</div>
<script>
const pins = [{"name":"1","number":"1","type":"Passive","electricalType":"passive","color":"#78909c","desc":"","x":0,"y":3.81,"angle":270,"length":1.27},{"name":"2","number":"2","type":"Passive","electricalType":"passive","color":"#78909c","desc":"","x":0,"y":-3.81,"angle":90,"length":1.27}];
const groups = [];
const svgW = 12.632;
const svgH = 18.22;
const contentBounds = {"x":-4.4567,"y":-3.7807999999999997,"w":12.032,"h":17.619999999999997};
const svgOffset = {"x":1.5593,"y":5.0292};
const hidePinNames = true;
const hidePinNumbers = true;
const datasheetUrl = "~";
const mfrName = "";
const _symbolName = "CR0402-FX-6803GLF";
const _symPath = "/home/adom/project/project-content/schematics/symbols/CR0402-FX-6803GLF/CR0402-FX-6803GLF.kicad_sym";
const wrap = document.getElementById('canvas-wrap');
const canvas = document.getElementById('canvas');
const tooltip = document.getElementById('tooltip');
const zoomLabel = document.getElementById('zoom-level');
const cursorPos = document.getElementById('cursor-pos');
let scale = 1, panX = 0, panY = 0;
let isDragging = false;
let dragStartX, dragStartY, panStartX, panStartY;
let gridVisible = true;
let stickyPin = null; // currently pinned tooltip
const MM_TO_PX = 3.78;
const svgPxW = svgW * MM_TO_PX;
const svgPxH = svgH * MM_TO_PX;
const svgEl = canvas.querySelector('svg');
if (svgEl) {
svgEl.setAttribute('width', svgPxW + 'px');
svgEl.setAttribute('height', svgPxH + 'px');
}
function updateTransform() {
canvas.style.transform = 'translate(' + panX + 'px,' + panY + 'px) scale(' + scale + ')';
zoomLabel.textContent = Math.round(scale * 100) + '%';
}
function zoomFit() {
const ww = wrap.clientWidth, wh = wrap.clientHeight;
const pad = 40;
// Read the bbox rect directly from the SVG DOM — it's already positioned correctly
const bboxRect = svgEl ? svgEl.querySelector('.debug-bbox') : null;
if (bboxRect) {
const bb = bboxRect.getBBox();
const vb = svgEl.viewBox.baseVal;
// Convert SVG user-space coords to pixel coords within the SVG element
const pxPerUnitX = svgPxW / vb.width;
const pxPerUnitY = svgPxH / vb.height;
const pxX = (bb.x - vb.x) * pxPerUnitX;
const pxY = (bb.y - vb.y) * pxPerUnitY;
const pxW = bb.width * pxPerUnitX;
const pxH = bb.height * pxPerUnitY;
scale = Math.min((ww - pad * 2) / pxW, (wh - pad * 2) / pxH, 10);
if (!isFinite(scale) || scale <= 0) scale = 1;
panX = (ww - pxW * scale) / 2 - pxX * scale;
panY = (wh - pxH * scale) / 2 - pxY * scale;
} else {
scale = Math.min((ww - pad * 2) / svgPxW, (wh - pad * 2) / svgPxH, 10);
if (!isFinite(scale) || scale <= 0) scale = 1;
panX = (ww - svgPxW * scale) / 2;
panY = (wh - svgPxH * scale) / 2;
}
updateTransform();
}
function zoomReset() {
scale = 1;
panX = (wrap.clientWidth - svgPxW) / 2;
panY = (wrap.clientHeight - svgPxH) / 2;
updateTransform();
}
function zoomIn() { zoomAt(wrap.clientWidth / 2, wrap.clientHeight / 2, 1.25); }
function zoomOut() { zoomAt(wrap.clientWidth / 2, wrap.clientHeight / 2, 0.8); }
function zoomAt(cx, cy, factor) {
const ns = Math.max(0.1, Math.min(scale * factor, 20));
const r = ns / scale;
panX = cx - (cx - panX) * r;
panY = cy - (cy - panY) * r;
scale = ns;
updateTransform();
}
function toggleGrid() {
gridVisible = !gridVisible;
wrap.classList.toggle('no-grid', !gridVisible);
document.getElementById('grid-btn').classList.toggle('active', gridVisible);
}
let bboxVisible = true;
function toggleBBox() {
bboxVisible = !bboxVisible;
const el = svgEl && svgEl.querySelector('.debug-bbox');
if (el) el.setAttribute('opacity', bboxVisible ? '0.4' : '0');
document.getElementById('bbox-btn').classList.toggle('active', bboxVisible);
}
let hitsVisible = false;
function toggleHits() {
hitsVisible = !hitsVisible;
if (svgEl) {
svgEl.querySelectorAll('.pin-hit-zone').forEach(r => {
r.style.visibility = hitsVisible ? '' : 'hidden';
});
}
document.getElementById('hits-btn').classList.toggle('active', hitsVisible);
}
// ── Regen prompt modal ──
let _regenPrompt = '';
function buildRegenPrompt() {
const folder = _symPath.substring(0, _symPath.lastIndexOf('/'));
return 'Regenerate the SymView for ' + _symbolName + '.\n\n'
+ 'Symbol: ' + _symPath + '\n'
+ 'Metadata: ' + folder + '/' + _symbolName + '-metadata.json\n\n'
+ 'Re-export the SVG via the KiCad service, regenerate the SymView HTML at the same path, '
+ 'and re-display it in the Adom app.';
}
function showRegenPrompt() {
_regenPrompt = buildRegenPrompt();
document.getElementById('regen-prompt-text').textContent = _regenPrompt;
document.getElementById('regen-overlay').classList.add('visible');
const status = document.getElementById('regen-copy-status');
navigator.clipboard.writeText(_regenPrompt).then(() => {
status.textContent = 'Copied to clipboard';
status.classList.add('copied');
}).catch(() => {
status.textContent = 'Click Copy to copy';
status.classList.remove('copied');
});
}
function closeRegenPrompt() {
document.getElementById('regen-overlay').classList.remove('visible');
}
function copyRegenText() {
navigator.clipboard.writeText(_regenPrompt).then(() => {
const status = document.getElementById('regen-copy-status');
status.textContent = 'Copied to clipboard';
status.classList.add('copied');
});
}
// Mouse wheel zoom
wrap.addEventListener('wheel', (e) => {
e.preventDefault();
const r = wrap.getBoundingClientRect();
zoomAt(e.clientX - r.left, e.clientY - r.top, e.deltaY < 0 ? 1.12 : 0.89);
}, { passive: false });
// Pan
wrap.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isDragging = true;
dragStartX = e.clientX; dragStartY = e.clientY;
panStartX = panX; panStartY = panY;
wrap.classList.add('dragging');
});
window.addEventListener('mousemove', (e) => {
const r = wrap.getBoundingClientRect();
const mx = (e.clientX - r.left - panX) / scale / MM_TO_PX;
const my = (e.clientY - r.top - panY) / scale / MM_TO_PX;
cursorPos.innerHTML = 'X: ' + mx.toFixed(2) + ' Y: ' + my.toFixed(2) + ' mm';
if (!isDragging) return;
panX = panStartX + (e.clientX - dragStartX);
panY = panStartY + (e.clientY - dragStartY);
updateTransform();
});
window.addEventListener('mouseup', () => { isDragging = false; wrap.classList.remove('dragging'); });
// ── Pin highlighting ──
const pinTextGroups = new Map();
const pinWireGroups = new Map();
if (svgEl) {
// Build text group lookup
svgEl.querySelectorAll('g.stroked-text').forEach(g => {
const desc = g.querySelector('desc');
if (desc) pinTextGroups.set(desc.textContent.trim(), g);
});
// Build wire group lookup using coordinate matching (not sibling navigation)
if (svgOffset) {
const ox = svgOffset.x, oy = svgOffset.y;
// Collect all simple 2-point line paths in the SVG
const allPaths = svgEl.querySelectorAll('path');
const wirePaths = [];
allPaths.forEach(p => {
const d = p.getAttribute('d');
if (!d) return;
const m = d.match(/^M([\d.]+)\s+([\d.]+)\s+L([\d.]+)\s+([\d.]+)\s*$/);
if (m) wirePaths.push({ el: p, x1: parseFloat(m[1]), y1: parseFloat(m[2]), x2: parseFloat(m[3]), y2: parseFloat(m[4]) });
});
for (const pin of pins) {
const tipX = ox + pin.x;
const tipY = oy - pin.y;
const wireLen = pin.length || 2.54;
const isHoriz = (pin.angle === 0 || pin.angle === 180);
for (const w of wirePaths) {
const dx = Math.abs(w.x2 - w.x1), dy = Math.abs(w.y2 - w.y1);
const len = Math.sqrt(dx * dx + dy * dy);
if (Math.abs(len - wireLen) > 0.05) continue;
if (isHoriz && dy > 0.01) continue;
if (!isHoriz && dx > 0.01) continue;
// Check if one endpoint is near the pin tip
const d1 = Math.hypot(w.x1 - tipX, w.y1 - tipY);
const d2 = Math.hypot(w.x2 - tipX, w.y2 - tipY);
if (Math.min(d1, d2) < 0.1) {
// Found the wire path element directly
pinWireGroups.set(pin.name, w.el);
break;
}
}
}
}
}
function highlightPin(pin) {
clearHighlight();
// IC symbols: stroked-text groups
[pin.name, pin.number].forEach(label => {
const g = pinTextGroups.get(label);
if (g) g.classList.add('pin-highlight');
});
const wireG = pinWireGroups.get(pin.name);
if (wireG) wireG.classList.add('pin-highlight');
// Discrete symbols: injected <text> labels
if (svgEl && hidePinNames) {
svgEl.querySelectorAll('text.pin-label').forEach(t => {
if (t.textContent.trim() === pin.name) t.classList.add('pin-label-highlight');
});
svgEl.querySelectorAll('text.pin-num-label').forEach(t => {
if (t.textContent.trim() === pin.number) t.classList.add('pin-num-highlight');
});
}
}
function clearHighlight() {
if (svgEl) {
svgEl.querySelectorAll('.pin-highlight').forEach(g => g.classList.remove('pin-highlight'));
svgEl.querySelectorAll('.group-highlight').forEach(g => g.classList.remove('group-highlight'));
svgEl.querySelectorAll('.pin-label-highlight').forEach(t => t.classList.remove('pin-label-highlight'));
svgEl.querySelectorAll('.pin-num-highlight').forEach(t => t.classList.remove('pin-num-highlight'));
}
}
// ── Tooltip positioning ──
function positionTooltip(x, y) {
const tw = tooltip.offsetWidth || 280;
const th = tooltip.offsetHeight || 120;
let tx = x + 14;
let ty = y + 14;
if (tx + tw > window.innerWidth - 14) tx = x - tw - 14;
if (ty + th > window.innerHeight - 14) ty = y - th - 14;
tx = Math.max(14, tx);
ty = Math.max(14, ty);
tooltip.style.left = tx + 'px';
tooltip.style.top = ty + 'px';
}
// ── Source attribution HTML ──
function sourceHtml() {
if (!datasheetUrl || !mfrName) return '';
return '<div class="tt-source"><span class="tt-source-label">Source:</span> <a href="' + datasheetUrl + '" target="_blank">' + mfrName + ' Datasheet</a></div>';
}
function hintHtml() {
return '<div class="tt-hint">click to pin · select text</div>';
}
function showTooltip(x, y, pin) {
if (stickyPin) return; // don't override sticky
highlightPin(pin);
let html = '<div><span class="tt-name">' + pin.name + '</span><span class="tt-pin">Pin ' + pin.number + '</span></div>' +
'<div><span class="tt-type" style="background:' + pin.color + '22; color:' + pin.color + '; border:1px solid ' + pin.color + '44">' + pin.type + '</span></div>';
if (pin.desc) html += '<div class="tt-desc">' + pin.desc + '</div>';
html += sourceHtml();
html += hintHtml();
tooltip.innerHTML = html;
tooltip.classList.add('visible');
tooltip.classList.remove('sticky');
positionTooltip(x, y);
}
function showGroupTooltip(x, y, group) {
if (stickyPin) return;
clearHighlight();
const g = pinTextGroups.get(group.name);
if (g) g.classList.add('group-highlight');
let html = '<div><span class="tt-group-name">' + group.name + '</span></div>' +
'<div><span class="tt-group-badge">Function Group</span></div>';
if (group.desc) html += '<div class="tt-desc">' + group.desc + '</div>';
tooltip.innerHTML = html;
tooltip.classList.add('visible');
tooltip.classList.remove('sticky');
positionTooltip(x, y);
}
function hideTooltip() {
if (stickyPin) return;
tooltip.classList.remove('visible');
clearHighlight();
}
// ── Sticky tooltip (click-to-pin) ──
function toggleSticky(pin, x, y) {
if (stickyPin === pin) {
// Unpin
stickyPin = null;
tooltip.classList.remove('sticky');
hideTooltip();
} else {
// Pin
stickyPin = pin;
highlightPin(pin);
let html = '<div><span class="tt-name">' + pin.name + '</span><span class="tt-pin">Pin ' + pin.number + '</span></div>' +
'<div><span class="tt-type" style="background:' + pin.color + '22; color:' + pin.color + '; border:1px solid ' + pin.color + '44">' + pin.type + '</span></div>';
if (pin.desc) html += '<div class="tt-desc">' + pin.desc + '</div>';
html += sourceHtml();
tooltip.innerHTML = html;
tooltip.classList.add('visible', 'sticky');
positionTooltip(x, y);
}
}
// Dismiss sticky on click outside
document.addEventListener('click', (e) => {
if (!stickyPin) return;
if (tooltip.contains(e.target)) return; // click inside tooltip
if (e.target.closest('[data-pin]') || e.target.closest('[data-pin-num]') || e.target.closest('[data-pin-wire]')) return;
stickyPin = null;
tooltip.classList.remove('sticky', 'visible');
clearHighlight();
});
wrap.addEventListener('mouseleave', hideTooltip);
// ── Pin hit zones ──
function setupPinHitZones() {
if (!svgEl) return;
if (hidePinNames) {
// Discrete components (hidden pin names): compute hit zones from injected labels
// Covers MOSFETs, resistors, etc. — standard KiCad discrete symbols hide pin names
// but may leave pin numbers visible, so we check hidePinNames only
setupDiscreteHitZones();
} else {
// Standard IC: use hidden text elements for hit detection
setupTextHitZones();
}
}
function setupTextHitZones() {
// IC symbols: two hit zones per pin, derived from actual rendered text bounds (getBBox):
// 1. INSIDE body — covers pin name text (bbox from stroked-text group)
// 2. OUTSIDE body — covers pin wire + number area (bbox or coordinate-based)
if (!svgOffset) return;
const ox = svgOffset.x, oy = svgOffset.y;
const pitch = 2.54;
const zoneH = pitch * 0.95;
const wireExt = 1.5;
const pad = 0.3; // padding around text bbox
// Build lookup: text content → stroked-text group bbox
const textBBoxMap = {};
svgEl.querySelectorAll('g.stroked-text').forEach(g => {
const desc = g.querySelector('desc');
if (desc) {
try {
const bbox = g.getBBox();
if (bbox.width > 0 && bbox.height > 0) {
const key = desc.textContent.trim();
if (!textBBoxMap[key]) textBBoxMap[key] = [];
textBBoxMap[key].push(bbox);
}
} catch (e) {}
}
});
function addHitZone(hx, hy, hw, hh, pin) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', hx);
rect.setAttribute('y', hy);
rect.setAttribute('width', hw);
rect.setAttribute('height', hh);
rect.setAttribute('class', 'pin-hit-zone');
rect.setAttribute('data-pin', pin.name);
rect.setAttribute('rx', '0.3');
svgEl.appendChild(rect);
rect.addEventListener('mouseenter', (ev) => showTooltip(ev.clientX, ev.clientY, pin));
rect.addEventListener('mousemove', (ev) => { if (!stickyPin) positionTooltip(ev.clientX, ev.clientY); });
rect.addEventListener('mouseleave', hideTooltip);
rect.addEventListener('click', (ev) => { ev.stopPropagation(); toggleSticky(pin, ev.clientX, ev.clientY); });
}
for (const pin of pins) {
const tipSvgX = ox + pin.x;
const tipSvgY = oy - pin.y;
const wireLen = pin.length || 2.54;
const isHoriz = (pin.angle === 0 || pin.angle === 180);
// Pin name hitbox: use actual text bbox if available
const nameBBoxes = textBBoxMap[pin.name];
if (nameBBoxes && nameBBoxes.length > 0) {
// Pick the bbox closest to this pin's body-edge position
const bodyEdgeX = isHoriz
? (pin.angle === 0 ? tipSvgX + wireLen : tipSvgX - wireLen)
: tipSvgX;
const bodyEdgeY = isHoriz
? tipSvgY
: (pin.angle === 90 ? tipSvgY - wireLen : tipSvgY + wireLen);
let best = nameBBoxes[0];
let bestDist = Infinity;
for (const bb of nameBBoxes) {
const cx = bb.x + bb.width / 2, cy = bb.y + bb.height / 2;
const d = Math.hypot(cx - bodyEdgeX, cy - bodyEdgeY);
if (d < bestDist) { bestDist = d; best = bb; }
}
addHitZone(best.x - pad, best.y - pad, best.width + pad * 2, best.height + pad * 2, pin);
} else {
// Fallback: fixed-width zone inside body
const bodyDepth = 8;
if (isHoriz) {
const cy = tipSvgY - zoneH / 2;
if (pin.angle === 0) addHitZone(tipSvgX + wireLen, cy, bodyDepth, zoneH, pin);
else addHitZone(tipSvgX - wireLen - bodyDepth, cy, bodyDepth, zoneH, pin);
} else {
const cx = tipSvgX - zoneH / 2;
if (pin.angle === 90) addHitZone(cx, tipSvgY - wireLen - bodyDepth, zoneH, bodyDepth, pin);
else addHitZone(cx, tipSvgY + wireLen, zoneH, bodyDepth, pin);
}
}
// Pin number hitbox: use actual text bbox if available
const numBBoxes = textBBoxMap[pin.number];
if (numBBoxes && numBBoxes.length > 0) {
const tipDist = (bb) => Math.hypot(bb.x + bb.width / 2 - tipSvgX, bb.y + bb.height / 2 - tipSvgY);
let best = numBBoxes[0], bestD = tipDist(best);
for (const bb of numBBoxes) { const d = tipDist(bb); if (d < bestD) { bestD = d; best = bb; } }
addHitZone(best.x - pad, best.y - pad, best.width + pad * 2, best.height + pad * 2, pin);
} else {
// Fallback: wire area
if (isHoriz) {
const cy = tipSvgY - zoneH / 2;
if (pin.angle === 0) addHitZone(tipSvgX - wireExt, cy, wireLen + wireExt, zoneH, pin);
else addHitZone(tipSvgX, cy, wireLen + wireExt, zoneH, pin);
} else {
const cx = tipSvgX - zoneH / 2;
if (pin.angle === 90) addHitZone(cx, tipSvgY, zoneH, wireLen + wireExt, pin);
else addHitZone(cx, tipSvgY - wireExt, zoneH, wireLen + wireExt, pin);
}
}
}
}
function setupDiscreteHitZones() {
// For discrete components (hidden pin names/numbers), create hit zones
// using getBBox() on injected text labels for precise alignment.
// Falls back to coordinate-based zones if text elements aren't found.
if (!svgOffset) return;
const ox = svgOffset.x, oy = svgOffset.y;
const pad = 0.5; // generous padding for easy hover on small labels
// Build lookup: text content → getBBox for injected labels
const labelBBoxMap = {};
const allTextEls = svgEl.querySelectorAll('text.pin-label, text.pin-num-label');
allTextEls.forEach(t => {
try {
const bbox = t.getBBox();
if (bbox.width > 0 && bbox.height > 0) {
const key = t.textContent.trim();
if (!labelBBoxMap[key]) labelBBoxMap[key] = [];
labelBBoxMap[key].push(bbox);
}
} catch (e) {}
});
for (const pin of pins) {
const tipSvgX = ox + pin.x;
const tipSvgY = oy - pin.y;
const wireLen = pin.length || 2.54;
// Collect bounding boxes of this pin's name and number labels
const bboxes = [];
// Find name label bbox (closest to pin tip)
const nameBBs = labelBBoxMap[pin.name];
if (nameBBs) {
let best = nameBBs[0], bestD = Infinity;
for (const bb of nameBBs) {
const d = Math.hypot(bb.x + bb.width / 2 - tipSvgX, bb.y + bb.height / 2 - tipSvgY);
if (d < bestD) { bestD = d; best = bb; }
}
bboxes.push(best);
}
// Find number label bbox (closest to pin tip)
const numBBs = labelBBoxMap[pin.number];
if (numBBs) {
let best = numBBs[0], bestD = Infinity;
for (const bb of numBBs) {
const d = Math.hypot(bb.x + bb.width / 2 - tipSvgX, bb.y + bb.height / 2 - tipSvgY);
if (d < bestD) { bestD = d; best = bb; }
}
bboxes.push(best);
}
if (bboxes.length > 0) {
// Union text bboxes into a single hit zone (just covers labels near the tip)
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const bb of bboxes) {
minX = Math.min(minX, bb.x);
minY = Math.min(minY, bb.y);
maxX = Math.max(maxX, bb.x + bb.width);
maxY = Math.max(maxY, bb.y + bb.height);
}
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', minX - pad);
rect.setAttribute('y', minY - pad);
rect.setAttribute('width', maxX - minX + pad * 2);
rect.setAttribute('height', maxY - minY + pad * 2);
rect.setAttribute('class', 'pin-hit-zone');
rect.setAttribute('data-pin-wire', pin.name);
rect.setAttribute('rx', '0.3');
svgEl.appendChild(rect);
rect.addEventListener('mouseenter', (ev) => showTooltip(ev.clientX, ev.clientY, pin));
rect.addEventListener('mousemove', (ev) => { if (!stickyPin) positionTooltip(ev.clientX, ev.clientY); });
rect.addEventListener('mouseleave', hideTooltip);
rect.addEventListener('click', (ev) => { ev.stopPropagation(); toggleSticky(pin, ev.clientX, ev.clientY); });
} else {
// Fallback: coordinate-based zone around wire area
const perpHalf = 1.5;
let hitX, hitY, hitW, hitH;
switch (pin.angle) {
case 0: hitX = tipSvgX - 1; hitY = tipSvgY - perpHalf; hitW = wireLen + 1; hitH = perpHalf * 2; break;
case 180: hitX = tipSvgX - wireLen; hitY = tipSvgY - perpHalf; hitW = wireLen + 1; hitH = perpHalf * 2; break;
case 270: hitX = tipSvgX - perpHalf; hitY = tipSvgY; hitW = perpHalf * 2; hitH = wireLen + 1; break;
case 90: hitX = tipSvgX - perpHalf; hitY = tipSvgY - wireLen; hitW = perpHalf * 2; hitH = wireLen + 1; break;
default: hitX = tipSvgX - 2; hitY = tipSvgY - 2; hitW = 4; hitH = 4;
}
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', hitX);
rect.setAttribute('y', hitY);
rect.setAttribute('width', hitW);
rect.setAttribute('height', hitH);
rect.setAttribute('class', 'pin-hit-zone');
rect.setAttribute('data-pin-wire', pin.name);
rect.setAttribute('rx', '0.3');
svgEl.appendChild(rect);
rect.addEventListener('mouseenter', (ev) => showTooltip(ev.clientX, ev.clientY, pin));
rect.addEventListener('mousemove', (ev) => { if (!stickyPin) positionTooltip(ev.clientX, ev.clientY); });
rect.addEventListener('mouseleave', hideTooltip);
rect.addEventListener('click', (ev) => { ev.stopPropagation(); toggleSticky(pin, ev.clientX, ev.clientY); });
}
}
}
// ── Group label hit zones ──
function setupGroupHitZones() {
if (!svgEl) return;
const texts = svgEl.querySelectorAll('text');
texts.forEach(textEl => {
const label = textEl.textContent.trim();
const group = groups.find(g => g.name === label);
if (!group) return;
const fSize = parseFloat(textEl.getAttribute('font-size')) || 1;
if (Math.abs(fSize - 1.016) > 0.1) return;
const x = parseFloat(textEl.getAttribute('x')) || 0;
const y = parseFloat(textEl.getAttribute('y')) || 0;
const tLen = parseFloat(textEl.getAttribute('textLength')) || 6;
const anchor = textEl.getAttribute('text-anchor') || 'start';
const padX = fSize * 0.4;
const padY = fSize * 0.15; // tight vertical padding to avoid spilling into pin zones
let rx, rw = tLen + padX * 2, rh = fSize + padY * 2;
if (anchor === 'end') rx = x - tLen - padX;
else if (anchor === 'middle') rx = x - tLen / 2 - padX;
else rx = x - padX;
const ry = y - fSize - padY;
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', rx);
rect.setAttribute('y', ry);
rect.setAttribute('width', rw);
rect.setAttribute('height', rh);
rect.setAttribute('class', 'pin-hit-zone');
rect.setAttribute('data-group', group.name);
rect.setAttribute('rx', '0.3');
svgEl.appendChild(rect);
rect.addEventListener('mouseenter', (ev) => showGroupTooltip(ev.clientX, ev.clientY, group));
rect.addEventListener('mousemove', (ev) => { if (!stickyPin) positionTooltip(ev.clientX, ev.clientY); });
rect.addEventListener('mouseleave', hideTooltip);
});
}
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.key === 'f' || e.key === 'F') zoomFit();
else if (e.key === '1') zoomReset();
else if (e.key === '+' || e.key === '=') zoomIn();
else if (e.key === '-') zoomOut();
else if (e.key === 'g' || e.key === 'G') toggleGrid();
else if (e.key === 'Escape') {
closeRegenPrompt();
stickyPin = null;
tooltip.classList.remove('sticky', 'visible');
clearHighlight();
}
});
// Auto-fit on window/container resize (e.g. iframe becoming visible)
new ResizeObserver(() => zoomFit()).observe(wrap);
// Init
requestAnimationFrame(() => {
zoomFit();
setupPinHitZones();
setupGroupHitZones();
// Hide hit zones by default (user can toggle with Hits button)
if (!hitsVisible && svgEl) {
svgEl.querySelectorAll('.pin-hit-zone').forEach(r => { r.style.visibility = 'hidden'; });
}
});
// Zoom to a named region of the symbol (for AI screenshot verification)
// Regions: 'top', 'bottom', 'left', 'right', 'fit' (full symbol)
function zoomToRegion(region) {
if (!contentBounds) { zoomFit(); return; }
const ww = wrap.clientWidth, wh = wrap.clientHeight;
const svgEl = document.querySelector('#canvas svg');
if (!svgEl) return;
const vb = svgEl.viewBox.baseVal;
const svgPxW = svgEl.clientWidth || svgEl.getBoundingClientRect().width;
const svgPxH = svgEl.clientHeight || svgEl.getBoundingClientRect().height;
const pxPerUnitX = svgPxW / vb.width;
const pxPerUnitY = svgPxH / vb.height;
// Content bounds in SVG user-space
const cb = contentBounds;
const cx = cb.x, cy = cb.y, cw = cb.w || cb.width, ch = cb.h || cb.height;
// Region crop (SVG user-space coordinates)
let rx, ry, rw, rh;
const margin = 2; // mm padding
if (region === 'top') {
rx = cx - margin; ry = cy - margin; rw = cw + margin * 2; rh = Math.min(ch * 0.12, 50) + margin;
} else if (region === 'bottom') {
const cropH = Math.min(ch * 0.12, 50);
ry = cy + ch - cropH - margin; rx = cx - margin; rw = cw + margin * 2; rh = cropH + margin * 2;
} else if (region === 'top-left') {
rx = cx - margin; ry = cy - margin; rw = cw * 0.4 + margin; rh = Math.min(ch * 0.12, 50) + margin;
} else if (region === 'top-right') {
rx = cx + cw * 0.6 - margin; ry = cy - margin; rw = cw * 0.4 + margin * 2; rh = Math.min(ch * 0.12, 50) + margin;
} else if (region === 'bottom-left') {
const cropH = Math.min(ch * 0.12, 50);
ry = cy + ch - cropH - margin; rx = cx - margin; rw = cw * 0.4 + margin; rh = cropH + margin * 2;
} else if (region === 'bottom-right') {
const cropH = Math.min(ch * 0.12, 50);
ry = cy + ch - cropH - margin; rx = cx + cw * 0.6 - margin; rw = cw * 0.4 + margin * 2; rh = cropH + margin * 2;
} else if (region === 'left') {
rx = cx - margin; ry = cy - margin; rw = cw * 0.35 + margin; rh = ch + margin * 2;
} else if (region === 'right') {
rx = cx + cw * 0.65 - margin; ry = cy - margin; rw = cw * 0.35 + margin * 2; rh = ch + margin * 2;
} else {
zoomFit(); return;
}
// Convert to pixel coords
const pxX = (rx - vb.x) * pxPerUnitX;
const pxY = (ry - vb.y) * pxPerUnitY;
const pxW = rw * pxPerUnitX;
const pxH = rh * pxPerUnitY;
// Compute scale to fit region in viewport
const ns = Math.min(ww / pxW, wh / pxH) * 0.92;
scale = Math.max(0.1, Math.min(ns, 20));
panX = ww / 2 - (pxX + pxW / 2) * scale;
panY = wh / 2 - (pxY + pxH / 2) * scale;
updateTransform();
}
// Screenshot capture handler for Adom Viewer mgmt relay
// Send SVG data back to parent for rendering (avoids sandbox restrictions on Image loading)
window.addEventListener('message', (e) => {
// Zoom-to-region command (from AI via mgmt relay)
if (e.data?.type === 'sym_zoom_region') {
zoomToRegion(e.data.region || 'fit');
return;
}
if (e.data?.type === 'mgmt_capture_request') {
const reqId = e.data._reqId;
try {
const svg = document.querySelector('#canvas svg');
if (!svg) { window.parent.postMessage({ type: 'mgmt_canvas_capture', _reqId: reqId, data: null }, '*'); return; }
const clone = svg.cloneNode(true);
// Inline pin-hit-zone styles so they're visible in standalone SVG
clone.querySelectorAll('.pin-hit-zone').forEach(el => {
el.setAttribute('fill', 'rgba(0,230,220,0.06)');
el.setAttribute('stroke', '#00e6dc');
el.setAttribute('stroke-width', '0.12');
el.setAttribute('stroke-dasharray', '0.3,0.2');
el.setAttribute('stroke-opacity', '0.5');
el.setAttribute('rx', '0.3');
});
const vb = svg.getAttribute('viewBox');
const parts = vb ? vb.split(/\s+/).map(Number) : [0, 0, 100, 100];
let vbX = parts[0], vbY = parts[1], vbW = parts[2] || 100, vbH = parts[3] || 100;
// Crop to visible viewport when zoomed/panned
const svgPxW = svg.clientWidth || svg.getBoundingClientRect().width;
const svgPxH = svg.clientHeight || svg.getBoundingClientRect().height;
const ww = wrap.clientWidth, wh = wrap.clientHeight;
if (scale !== 1 || panX !== (ww - svgPxW) / 2 || panY !== (wh - svgPxH) / 2) {
const pxPerUnitX = svgPxW / vbW;
const pxPerUnitY = svgPxH / vbH;
// Visible viewport in pixel space of the SVG element
const visLeft = -panX / scale;
const visTop = -panY / scale;
const visW = ww / scale;
const visH = wh / scale;
// Convert to SVG user-space
vbX = vbX + visLeft / pxPerUnitX;
vbY = vbY + visTop / pxPerUnitY;
vbW = visW / pxPerUnitX;
vbH = visH / pxPerUnitY;
}
clone.setAttribute('viewBox', vbX + ' ' + vbY + ' ' + vbW + ' ' + vbH);
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
const xml = new XMLSerializer().serializeToString(clone);
// Send SVG XML + dimensions to parent for canvas rendering
window.parent.postMessage({
type: 'mgmt_canvas_capture',
_reqId: reqId,
svgXml: xml,
svgWidth: vbW,
svgHeight: vbH,
bgColor: '#0d1117'
}, '*');
} catch (err) {
window.parent.postMessage({ type: 'mgmt_canvas_capture', _reqId: reqId, data: null }, '*');
}
}
});
</script>
</body>
</html>