Skip to content

Instantly share code, notes, and snippets.

@Rene-Damm
Last active February 23, 2026 21:33
Show Gist options
  • Select an option

  • Save Rene-Damm/63831646257f2348b83a861212fe061d to your computer and use it in GitHub Desktop.

Select an option

Save Rene-Damm/63831646257f2348b83a861212fe061d to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GraphViz Viewer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0d1117;
color: #c9d1d9;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
#toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
background: #161b22;
border-bottom: 1px solid #30363d;
flex-shrink: 0;
user-select: none;
}
#toolbar h1 {
font-size: 14px;
font-weight: 700;
color: #58a6ff;
letter-spacing: 0.5px;
white-space: nowrap;
}
#searchWrap { display: flex; align-items: center; gap: 6px; }
#searchBox {
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 5px 10px;
border-radius: 6px;
width: 220px;
font-size: 13px;
outline: none;
transition: border-color 0.15s;
}
#searchBox:focus { border-color: #58a6ff; }
#searchBox::placeholder { color: #484f58; }
#searchInfo { font-size: 12px; color: #3fb950; white-space: nowrap; min-width: 70px; }
.btn {
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 5px 14px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
transition: background 0.15s, border-color 0.15s;
}
.btn:hover { background: #30363d; border-color: #58a6ff; }
.btn.primary { background: #238636; border-color: #238636; color: #fff; font-weight: 600; }
.btn.primary:hover { background: #2ea043; border-color: #2ea043; }
.spacer { flex: 1; }
#layoutSelect {
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 5px 8px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
outline: none;
}
#layoutSelect:focus { border-color: #58a6ff; }
#main { display: flex; flex: 1; overflow: hidden; }
#inputPanel {
width: 280px;
min-width: 180px;
max-width: 520px;
display: flex;
flex-direction: column;
background: #161b22;
border-right: 1px solid #30363d;
flex-shrink: 0;
}
#inputPanel label {
padding: 8px 10px 4px;
font-size: 11px;
color: #484f58;
text-transform: uppercase;
letter-spacing: 0.5px;
}
#dotInput {
flex: 1;
background: #0d1117;
color: #79c0ff;
border: none;
padding: 10px;
font-family: 'Consolas', 'Cascadia Code', monospace;
font-size: 12px;
line-height: 1.5;
resize: none;
outline: none;
overflow-y: auto;
}
#dotInput::placeholder { color: #30363d; }
#errorMsg {
padding: 5px 10px;
font-size: 11px;
color: #f85149;
min-height: 22px;
word-break: break-word;
}
#inputActions {
padding: 8px 10px;
display: flex;
gap: 8px;
border-top: 1px solid #21262d;
}
#resizer {
width: 4px;
background: #21262d;
cursor: col-resize;
flex-shrink: 0;
transition: background 0.15s;
}
#resizer:hover, #resizer.active { background: #58a6ff; }
#graphPanel {
flex: 1;
position: relative;
overflow: hidden;
background: #0d1117;
cursor: grab;
}
#graphPanel.panning { cursor: grabbing; }
#graphPanel.dragging-node { cursor: grabbing; }
#svgCanvas { width: 100%; height: 100%; display: block; }
#hint {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #30363d;
pointer-events: none;
user-select: none;
}
#hint h2 { font-size: 18px; margin-bottom: 8px; }
#hint p { font-size: 13px; line-height: 1.7; }
#statusBar {
padding: 4px 14px;
font-size: 11px;
color: #484f58;
background: #161b22;
border-top: 1px solid #30363d;
flex-shrink: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#statusBar span { color: #8b949e; }
</style>
</head>
<body>
<div id="toolbar">
<h1>&#x25C6; GraphViz Viewer</h1>
<div id="searchWrap">
<input id="searchBox" type="text" placeholder="&#x1F50D; Search nodes..." autocomplete="off" spellcheck="false"/>
<span id="searchInfo"></span>
</div>
<div class="spacer"></div>
<select id="layoutSelect" title="Layout algorithm">
<option value="force">Force layout</option>
<option value="hierarchical">Hierarchical</option>
<option value="circular">Circular</option>
</select>
<button class="btn" id="fitBtn" title="Fit graph to view (F)">Fit</button>
<button class="btn primary" id="renderBtn" title="Render graph (Ctrl+Enter)">Render</button>
</div>
<div id="main">
<div id="inputPanel">
<label>DOT Source</label>
<textarea id="dotInput" spellcheck="false" placeholder="Paste your GraphViz DOT source here..."></textarea>
<div id="errorMsg"></div>
<div id="inputActions">
<button class="btn primary" id="renderBtn2">Render</button>
<button class="btn" id="clearBtn">Clear</button>
</div>
</div>
<div id="resizer"></div>
<div id="graphPanel">
<svg id="svgCanvas" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="arr-def" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#3d8bcd"/></marker>
<marker id="arr-hi" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#e3b341"/></marker>
<marker id="arr-srch" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#3fb950"/></marker>
<marker id="arr-dim" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#21262d"/></marker>
<marker id="arr-def-s" markerWidth="8" markerHeight="6" refX="1" refY="3" orient="auto-start-reverse"><polygon points="0 0,8 3,0 6" fill="#3d8bcd"/></marker>
<marker id="arr-hi-s" markerWidth="8" markerHeight="6" refX="1" refY="3" orient="auto-start-reverse"><polygon points="0 0,8 3,0 6" fill="#e3b341"/></marker>
</defs>
<g id="viewport">
<g id="edgesGroup"></g>
<g id="nodeShapesGroup"></g>
<g id="nodeLabelsGroup"></g>
</g>
</svg>
<div id="hint">
<h2>No graph rendered</h2>
<p>Paste DOT source in the left panel<br>and click <strong>Render</strong> (or press <strong>Ctrl+Enter</strong>)</p>
</div>
</div>
</div>
<div id="statusBar">Ready &mdash; paste a DOT graph and click Render.</div>
<script>
'use strict';
const NS = 'http://www.w3.org/2000/svg';
const PAD_X = 16, PAD_Y = 10;
// ── HTML label → styled SVG lines ────────────────────────────────────────────
// Converts a GraphViz HTML-like label string into an array of lines,
// where each line is an array of styled text runs.
function htmlToLines(html) {
const div = document.createElement('div');
div.innerHTML = html;
const lines = [[]];
function walk(node, s) {
if (node.nodeType === 3) {
if (node.textContent) lines[lines.length-1].push({text: node.textContent, ...s});
return;
}
if (node.nodeType !== 1) return;
const tag = node.tagName.toLowerCase();
s = {...s};
if (tag === 'b' || tag === 'strong') s.bold = true;
if (tag === 'i' || tag === 'em') s.italic = true;
if (tag === 'u') s.underline = true;
if (tag === 's' || tag === 'strike') s.strike = true;
if (tag === 'font') {
const c = node.getAttribute('color');
const ps = node.getAttribute('point-size') || node.getAttribute('size');
const f = node.getAttribute('face');
if (c) s.color = c;
if (ps) s.fontSize = parseFloat(ps);
if (f) s.fontFamily = f;
}
if (tag === 'sub') s.sub = true;
if (tag === 'sup') s.sup = true;
if (tag === 'br') { lines.push([]); return; }
// Table: each row → new line; cells separated by space
if (tag === 'tr' && lines[lines.length-1].length) lines.push([]);
if (tag === 'td') {
for (const ch of node.childNodes) walk(ch, s);
if (node.nextElementSibling) lines[lines.length-1].push({text: ' ', ...s});
return;
}
for (const ch of node.childNodes) walk(ch, s);
if (tag === 'p' || tag === 'tr') lines.push([]);
}
const base = {bold:false,italic:false,underline:false,strike:false,
color:null,fontSize:null,fontFamily:null,sub:false,sup:false};
walk(div, base);
// strip empty lines at edges
while (lines.length > 1 && !lines[0].length) lines.shift();
while (lines.length > 1 && !lines[lines.length-1].length) lines.pop();
return lines;
}
// Build SVG <text>/<tspan> elements for styled lines, centred at (cx, cy).
function buildSvgLines(lines, cx, cy, defFs, defFf, defColor) {
const lineH = defFs * 1.4;
const startY = cy - (lines.length * lineH) / 2 + lineH * 0.5;
const g = document.createElementNS(NS, 'g');
lines.forEach((spans, i) => {
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', cx);
t.setAttribute('y', (startY + i * lineH).toFixed(2));
t.setAttribute('text-anchor', 'middle');
t.setAttribute('dominant-baseline', 'middle');
t.setAttribute('font-size', defFs);
t.setAttribute('font-family', defFf || 'system-ui,sans-serif');
t.setAttribute('fill', defColor || '#c9d1d9');
if (!spans.length) { t.textContent = '\u00A0'; g.appendChild(t); return; }
for (const sp of spans) {
if (!sp.text) continue;
const ts = document.createElementNS(NS, 'tspan');
ts.textContent = sp.text;
if (sp.bold) ts.setAttribute('font-weight', 'bold');
if (sp.italic) ts.setAttribute('font-style', 'italic');
if (sp.color) ts.setAttribute('fill', sp.color);
if (sp.fontSize) ts.setAttribute('font-size', sp.fontSize);
if (sp.fontFamily) ts.setAttribute('font-family', sp.fontFamily);
if (sp.underline || sp.strike)
ts.setAttribute('text-decoration', [sp.underline&&'underline', sp.strike&&'line-through'].filter(Boolean).join(' '));
if (sp.sub) { ts.setAttribute('baseline-shift','sub'); ts.setAttribute('font-size',(defFs*0.75).toFixed(1)); }
if (sp.sup) { ts.setAttribute('baseline-shift','super'); ts.setAttribute('font-size',(defFs*0.75).toFixed(1)); }
t.appendChild(ts);
}
g.appendChild(t);
});
return g;
}
// ── Measurement SVG (getBBox gives exact rendered dimensions) ─────────────────
const measSvg = document.createElementNS(NS, 'svg');
measSvg.style.cssText = 'position:fixed;visibility:hidden;top:0;left:-9999px;width:1px;height:1px;overflow:visible;';
document.body.appendChild(measSvg);
// ── DOT PARSER ────────────────────────────────────────────────────────────────
function parseDOT(src) {
src = src.replace(/\/\/[^\n]*/g, ' ')
.replace(/\/\*[\s\S]*?\*\//g, ' ')
.replace(/#[^\n]*/g, ' ');
let pos = 0;
let lastHtml = false;
function ws() { while (pos < src.length && src[pos] <= ' ') pos++; }
function consume(ch) {
ws();
if (src[pos] !== ch) throw new Error(`Expected '${ch}' near: "${src.slice(pos, pos+20)}"`);
pos++;
}
function readString() {
ws(); lastHtml = false;
if (pos >= src.length) return null;
if (src[pos] === '"') {
pos++;
let s = '';
while (pos < src.length && src[pos] !== '"') {
if (src[pos] === '\\') {
pos++;
const c = src[pos++] || '';
if (c === 'n') s += '\n';
else if (c === 'l') s += '\n'; // left-aligned newline
else if (c === 'r') s += '\n'; // right-aligned newline
else if (c === '"') s += '"';
else if (c === '\\') s += '\\';
else s += c;
} else {
s += src[pos++];
}
}
if (src[pos] === '"') pos++;
return s;
}
if (src[pos] === '<') {
// HTML label — collect balancing angle brackets, keeping inner content raw
lastHtml = true;
let depth = 0, s = '';
while (pos < src.length) {
const ch = src[pos++];
if (ch === '<') { depth++; if (depth > 1) s += ch; }
else if (ch === '>') { depth--; if (depth === 0) break; s += ch; }
else s += ch;
}
return s;
}
// Identifier / number
let s = '';
while (pos < src.length && /[a-zA-Z0-9_.\-\u0080-\uffff]/.test(src[pos]))
s += src[pos++];
return s || null;
}
function readAttrs() {
ws();
const attrs = {};
while (src[pos] === '[') {
pos++;
while (true) {
ws();
if (!src[pos] || src[pos] === ']') break;
const key = readString(); if (!key) { pos++; continue; }
ws();
if (src[pos] === '=') { pos++; const val = readString(); attrs[key] = val; if (key === 'label') attrs._lhtml = lastHtml; }
else attrs[key] = true;
ws();
if (src[pos] === ',' || src[pos] === ';') pos++;
}
if (src[pos] === ']') pos++;
ws();
}
return attrs;
}
const nodes = new Map();
const edges = [];
let directed = true;
let defNode = {}, defEdge = {};
function ensure(id) {
if (!nodes.has(id)) nodes.set(id, { id, label: id, isHtml: false, attrs: {} });
return nodes.get(id);
}
function parseBody() {
ws();
while (pos < src.length && src[pos] !== '}') {
ws(); if (!src[pos] || src[pos] === '}') break;
if (src.slice(pos, pos+8) === 'subgraph') {
pos += 8; ws();
if (src[pos] !== '{') readString();
ws(); if (src[pos] === '{') { pos++; parseBody(); ws(); if (src[pos] === '}') pos++; }
ws(); if (src[pos] === ';') pos++;
continue;
}
if (src[pos] === '{') { pos++; parseBody(); ws(); if (src[pos] === '}') pos++; ws(); if (src[pos] === ';') pos++; continue; }
const kw = ['graph','node','edge'].find(k => src.slice(pos, pos+k.length) === k && !/\w/.test(src[pos+k.length]||''));
if (kw) {
pos += kw.length; ws();
if (src[pos] === '[') { const a = readAttrs(); if (kw==='node') defNode={...defNode,...a}; else if (kw==='edge') defEdge={...defEdge,...a}; }
else if (src[pos] === '=') { pos++; readString(); }
ws(); if (src[pos] === ';') pos++;
continue;
}
const id = readString(); if (!id) { pos++; continue; }
ws();
if (src.slice(pos,pos+2) === '->' || src.slice(pos,pos+2) === '--') {
let from = id; ensure(from);
const chain = [];
while (src.slice(pos,pos+2) === '->' || src.slice(pos,pos+2) === '--') {
pos += 2; ws();
if (src.slice(pos,pos+8)==='subgraph'||src[pos]==='{') break;
const to = readString(); if (!to) break;
ensure(to); chain.push({from, to}); from = to; ws();
}
const a = readAttrs();
for (const c of chain) edges.push({from:c.from, to:c.to, attrs:{...defEdge,...a}});
} else if (src[pos] === '=') { pos++; readString(); }
else {
const a = readAttrs(); const nd = ensure(id);
Object.assign(nd.attrs, {...defNode,...a});
if (a.label !== undefined) { nd.label = String(a.label); nd.isHtml = a._lhtml||false; }
}
ws(); if (src[pos] === ';') pos++;
}
}
ws();
if (src.slice(pos,pos+6)==='strict') { pos+=6; ws(); }
if (src.slice(pos,pos+7)==='digraph') { directed=true; pos+=7; }
else if (src.slice(pos,pos+5)==='graph') { directed=false; pos+=5; }
ws();
if (src[pos]!=='{') readString(); // optional name
ws();
if (src[pos]!=='{') throw new Error('Expected opening {');
pos++;
parseBody();
return { nodes:[...nodes.values()], edges, directed };
}
// ── Label sizing ───────────────────────────────────────────────────────────────
const sizeCache = new Map();
function nodeFontSize(nd) { return parseFloat(nd.attrs.fontsize||nd.attrs['font-size']||13)||13; }
function nodeFontFace(nd) { return nd.attrs.fontname||nd.attrs['font-name']||'system-ui,sans-serif'; }
function labelSize(nd) {
if (sizeCache.has(nd.id)) return sizeCache.get(nd.id);
const sz = _computeSize(nd);
sizeCache.set(nd.id, sz);
return sz;
}
function _computeSize(nd) {
const shape = (nd.attrs.shape||'ellipse').toLowerCase();
if (shape==='point') return {w:10,h:10};
const fs = nodeFontSize(nd);
const ff = nodeFontFace(nd);
// Explicit width/height in inches (96 dpi)
if (nd.attrs.width||nd.attrs.height) {
const w = nd.attrs.width ? parseFloat(nd.attrs.width)*96 : 0;
const h = nd.attrs.height ? parseFloat(nd.attrs.height)*96 : 0;
if (w && h) return {w, h};
}
let w, h;
// For both HTML and plain text, render into the measurement SVG and use getBBox —
// the single source of truth that matches exactly what the browser will render.
const lines = nd.isHtml ? (nd._htmlLines = htmlToLines(nd.label)) : nd.label.split('\n');
const g = nd.isHtml
? buildSvgLines(lines, 0, 0, fs, ff, '#000')
: (() => {
const tmp = document.createElementNS(NS, 'g');
const lineH = fs * 1.4;
const startY = -(lines.length * lineH) / 2 + lineH * 0.5;
lines.forEach((line, i) => {
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', 0);
t.setAttribute('y', (startY + i * lineH).toFixed(2));
t.setAttribute('text-anchor', 'middle');
t.setAttribute('dominant-baseline', 'middle');
t.setAttribute('font-size', fs);
t.setAttribute('font-family', ff);
t.textContent = line || '\u00A0';
tmp.appendChild(t);
});
return tmp;
})();
measSvg.appendChild(g);
const bb = g.getBBox();
measSvg.removeChild(g);
w = Math.ceil(bb.width) + PAD_X * 2;
h = Math.ceil(bb.height) + PAD_Y * 2;
// Shapes that need more room
switch (shape) {
case 'diamond': case 'mdiamond': return { w: Math.max(70,w*1.6), h: Math.max(50,h*1.6) };
case 'parallelogram': return { w: Math.max(60,w+h*0.4), h: Math.max(30,h) };
case 'triangle': case 'invtriangle': return { w: Math.max(60,w*1.4), h: Math.max(40,h*1.4) };
case 'trapezium': case 'invtrapezium': return { w: Math.max(60,w*1.2), h: Math.max(30,h) };
default: return { w: Math.max(50,w), h: Math.max(26,h) };
}
}
// ── Shape drawing ─────────────────────────────────────────────────────────────
function parseStyle(s) {
const p = String(s||'').toLowerCase().split(',').map(x=>x.trim());
return { filled:p.includes('filled'), dashed:p.includes('dashed'), dotted:p.includes('dotted'),
rounded:p.includes('rounded'), bold:p.includes('bold'), invis:p.includes('invis') };
}
function applyStroke(el, stroke, sw, st) {
el.setAttribute('stroke', stroke);
el.setAttribute('stroke-width', sw);
if (st.dashed) el.setAttribute('stroke-dasharray','6,3');
if (st.dotted) el.setAttribute('stroke-dasharray','2,3');
}
function drawShape(shape, cx, cy, w, h, fill, stroke, sw, st) {
shape = (shape||'ellipse').toLowerCase();
const els = [];
function mk(tag) { return document.createElementNS(NS,tag); }
function styled(el, fillOvr) {
el.setAttribute('fill', fillOvr!==undefined ? fillOvr : fill);
applyStroke(el, stroke, sw, st);
els.push(el);
return el;
}
function poly(pts) { const e=mk('polygon'); e.setAttribute('points',pts); styled(e); }
switch(shape) {
case 'ellipse': case 'oval': case 'egg': default: {
const e=mk('ellipse'); e.setAttribute('cx',cx); e.setAttribute('cy',cy);
e.setAttribute('rx',w/2); e.setAttribute('ry',h/2); styled(e); break;
}
case 'circle': {
const r=Math.max(w,h)/2, e=mk('circle');
e.setAttribute('cx',cx); e.setAttribute('cy',cy); e.setAttribute('r',r); styled(e); break;
}
case 'doublecircle': {
const r=Math.max(w,h)/2;
const c1=mk('circle'); c1.setAttribute('cx',cx); c1.setAttribute('cy',cy); c1.setAttribute('r',r); styled(c1);
const c2=mk('circle'); c2.setAttribute('cx',cx); c2.setAttribute('cy',cy); c2.setAttribute('r',r-4); styled(c2,'none'); break;
}
case 'box': case 'rect': case 'rectangle': case 'square': case 'msquare': {
const rx2 = st.rounded ? Math.min(8,h/4) : (shape==='box'?2:0);
const r=mk('rect'); r.setAttribute('x',cx-w/2); r.setAttribute('y',cy-h/2);
r.setAttribute('width',w); r.setAttribute('height',h); r.setAttribute('rx',rx2); styled(r);
if (shape==='msquare') {
const p=4, r2=mk('rect'); r2.setAttribute('x',cx-w/2+p); r2.setAttribute('y',cy-h/2+p);
r2.setAttribute('width',w-p*2); r2.setAttribute('height',h-p*2); r2.setAttribute('rx',rx2); styled(r2,'none');
}
break;
}
case 'diamond': case 'mdiamond': {
poly(`${cx},${cy-h/2} ${cx+w/2},${cy} ${cx},${cy+h/2} ${cx-w/2},${cy}`);
if (shape==='mdiamond') {
['top','bot'].forEach((_, i) => {
const yy = cy + (i===0 ? -h*0.35 : h*0.35);
const ln=mk('line'); ln.setAttribute('x1',cx-w*0.2); ln.setAttribute('y1',yy);
ln.setAttribute('x2',cx+w*0.2); ln.setAttribute('y2',yy);
applyStroke(ln,stroke,sw,st); ln.setAttribute('fill','none'); els.push(ln);
});
}
break;
}
case 'parallelogram': {
const sk=h*0.3;
poly(`${cx-w/2+sk},${cy-h/2} ${cx+w/2+sk},${cy-h/2} ${cx+w/2-sk},${cy+h/2} ${cx-w/2-sk},${cy+h/2}`); break;
}
case 'trapezium': { const cut=w*0.2; poly(`${cx-w/2+cut},${cy-h/2} ${cx+w/2-cut},${cy-h/2} ${cx+w/2},${cy+h/2} ${cx-w/2},${cy+h/2}`); break; }
case 'invtrapezium': { const cut=w*0.2; poly(`${cx-w/2},${cy-h/2} ${cx+w/2},${cy-h/2} ${cx+w/2-cut},${cy+h/2} ${cx-w/2+cut},${cy+h/2}`); break; }
case 'triangle': { poly(`${cx},${cy-h/2} ${cx+w/2},${cy+h/2} ${cx-w/2},${cy+h/2}`); break; }
case 'invtriangle': { poly(`${cx-w/2},${cy-h/2} ${cx+w/2},${cy-h/2} ${cx},${cy+h/2}`); break; }
case 'hexagon': {
const dx=w*0.25;
poly(`${cx-w/2},${cy} ${cx-dx},${cy-h/2} ${cx+dx},${cy-h/2} ${cx+w/2},${cy} ${cx+dx},${cy+h/2} ${cx-dx},${cy+h/2}`); break;
}
case 'octagon': {
const ox=w*0.29, oy=h*0.29;
poly(`${cx-ox},${cy-h/2} ${cx+ox},${cy-h/2} ${cx+w/2},${cy-oy} ${cx+w/2},${cy+oy} ${cx+ox},${cy+h/2} ${cx-ox},${cy+h/2} ${cx-w/2},${cy+oy} ${cx-w/2},${cy-oy}`); break;
}
case 'cylinder': {
const ry=h*0.15;
const p=mk('path');
p.setAttribute('d',`M${cx-w/2},${cy-h/2+ry} A${w/2},${ry} 0 0 1 ${cx+w/2},${cy-h/2+ry} L${cx+w/2},${cy+h/2-ry} A${w/2},${ry} 0 0 1 ${cx-w/2},${cy+h/2-ry} Z`);
styled(p);
const top=mk('ellipse'); top.setAttribute('cx',cx); top.setAttribute('cy',cy-h/2+ry);
top.setAttribute('rx',w/2); top.setAttribute('ry',ry); styled(top,'none'); break;
}
case 'note': {
const f=Math.min(w*0.18,h*0.28,14);
const p=mk('path');
p.setAttribute('d',`M${cx-w/2},${cy-h/2} L${cx+w/2-f},${cy-h/2} L${cx+w/2},${cy-h/2+f} L${cx+w/2},${cy+h/2} L${cx-w/2},${cy+h/2} Z`);
styled(p);
const crease=mk('path');
crease.setAttribute('d',`M${cx+w/2-f},${cy-h/2} L${cx+w/2-f},${cy-h/2+f} L${cx+w/2},${cy-h/2+f}`);
crease.setAttribute('fill','none'); applyStroke(crease,stroke,sw*0.7,st); els.push(crease); break;
}
case 'tab': {
const tw=w*0.3, th=h*0.2;
const p=mk('path');
p.setAttribute('d',`M${cx-w/2},${cy-h/2+th} L${cx-w/2+tw},${cy-h/2+th} L${cx-w/2+tw},${cy-h/2} L${cx-w/2+tw*2},${cy-h/2} L${cx-w/2+tw*2},${cy-h/2+th} L${cx+w/2},${cy-h/2+th} L${cx+w/2},${cy+h/2} L${cx-w/2},${cy+h/2} Z`);
styled(p); break;
}
case 'folder': {
const tw=w*0.38, th=h*0.18;
const p=mk('path');
p.setAttribute('d',`M${cx-w/2},${cy-h/2+th} L${cx-w/2+tw*0.25},${cy-h/2+th} L${cx-w/2+tw*0.45},${cy-h/2} L${cx-w/2+tw},${cy-h/2} L${cx-w/2+tw*1.15},${cy-h/2+th} L${cx+w/2},${cy-h/2+th} L${cx+w/2},${cy+h/2} L${cx-w/2},${cy+h/2} Z`);
styled(p); break;
}
case 'component': {
const cw=Math.min(w*0.18,20), ch=Math.max(8,h*0.2);
const r=mk('rect'); r.setAttribute('x',cx-w/2+cw); r.setAttribute('y',cy-h/2);
r.setAttribute('width',w-cw); r.setAttribute('height',h); styled(r);
for (let i=0; i<2; i++) {
const sm=mk('rect'); sm.setAttribute('x',cx-w/2-cw*0.3);
sm.setAttribute('y',cy-h*0.25+i*h*0.5-ch/2);
sm.setAttribute('width',cw*1.3); sm.setAttribute('height',ch); styled(sm);
}
break;
}
case 'point': {
const pt=mk('circle'); pt.setAttribute('cx',cx); pt.setAttribute('cy',cy); pt.setAttribute('r',5);
pt.setAttribute('fill',stroke); pt.setAttribute('stroke','none'); els.push(pt); break;
}
case 'none': case 'plain': case 'plaintext': case 'invis': break;
}
return els;
}
// ── Border point for edge attachment ──────────────────────────────────────────
function borderPt(nd, tx, ty) {
const {w,h} = labelSize(nd);
const dx=tx-nd.x, dy=ty-nd.y;
const len=Math.sqrt(dx*dx+dy*dy)||1;
const ux=dx/len, uy=dy/len;
const shape=(nd.attrs.shape||'ellipse').toLowerCase();
const pad=4;
switch(shape) {
case 'ellipse': case 'oval': case 'circle': case 'doublecircle': case 'egg': {
const rx=w/2+pad, ry=h/2+pad;
const t=1/(Math.sqrt((ux*ux)/(rx*rx)+(uy*uy)/(ry*ry))||1);
return {x:nd.x+ux*t, y:nd.y+uy*t};
}
case 'diamond': case 'mdiamond': {
const denom=Math.abs(ux)/(w/2+pad)+Math.abs(uy)/(h/2+pad);
const t=denom>0?1/denom:w/2;
return {x:nd.x+ux*t, y:nd.y+uy*t};
}
default: {
const hw=w/2+pad, hh=h/2+pad;
const aUx=Math.abs(ux), aUy=Math.abs(uy);
const t=(aUx*hh>=aUy*hw)?(hw/aUx):(hh/aUy);
return {x:nd.x+ux*t, y:nd.y+uy*t};
}
}
}
// ── Text label drawing ────────────────────────────────────────────────────────
function drawLabel(nd, cx, cy, textColor) {
const shape=(nd.attrs.shape||'ellipse').toLowerCase();
if (shape==='point'||shape==='invis') return null;
const fs = nodeFontSize(nd);
const ff = nodeFontFace(nd);
const fc = textColor || nd.attrs.fontcolor || '#c9d1d9';
// HTML labels: use buildSvgLines (never clips, same elements as measured with getBBox)
if (nd.isHtml) {
const lines = nd._htmlLines || htmlToLines(nd.label);
return buildSvgLines(lines, cx, cy, fs, ff, fc);
}
// Plain text: split on \n, one <text> element per line
const lines = nd.label.split('\n');
const lineH = fs * 1.4;
const startY = cy - (lines.length * lineH) / 2 + lineH * 0.5;
const g = document.createElementNS(NS, 'g');
lines.forEach((line, i) => {
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', cx);
t.setAttribute('y', (startY + i * lineH).toFixed(2));
t.setAttribute('text-anchor', 'middle');
t.setAttribute('dominant-baseline', 'middle');
t.setAttribute('fill', fc);
t.setAttribute('font-size', fs);
t.setAttribute('font-family', ff);
t.textContent = line || '\u00A0';
g.appendChild(t);
});
return g;
}
// ── Overlap removal (post-layout pass) ────────────────────────────────────────
// Iteratively pushes overlapping node bounding boxes apart until none overlap.
function removeOverlaps(nodes) {
const n = nodes.length;
if (n < 2) return;
const sz = nodes.map(nd => labelSize(nd));
const MARGIN = 10;
let moved = true, iter = 0;
while (moved && iter++ < 100) {
moved = false;
for (let i = 0; i < n; i++) {
for (let j = i+1; j < n; j++) {
const dx = nodes[j].x - nodes[i].x;
const dy = nodes[j].y - nodes[i].y;
// Axis-aligned overlap check
const overlapX = (sz[i].w + sz[j].w) / 2 + MARGIN - Math.abs(dx);
const overlapY = (sz[i].h + sz[j].h) / 2 + MARGIN - Math.abs(dy);
if (overlapX > 0 && overlapY > 0) {
// Resolve along the axis with the smaller overlap (least displacement)
const d = Math.sqrt(dx*dx + dy*dy) || 0.01;
let push, ux, uy;
if (overlapX < overlapY) {
push = overlapX / 2 + 0.5;
ux = dx < 0 ? -1 : 1; uy = 0;
} else {
push = overlapY / 2 + 0.5;
ux = 0; uy = dy < 0 ? -1 : 1;
}
nodes[i].x -= ux*push; nodes[i].y -= uy*push;
nodes[j].x += ux*push; nodes[j].y += uy*push;
moved = true;
}
}
}
}
}
// ── Layout algorithms ─────────────────────────────────────────────────────────
function layoutCircular(nodes) {
const n=nodes.length, r=Math.max(120, n*50);
nodes.forEach((nd,i) => {
const a=(2*Math.PI*i/n)-Math.PI/2;
nd.x=Math.cos(a)*r; nd.y=Math.sin(a)*r;
});
}
function layoutHierarchical(nodes, edges) {
const n=nodes.length; if (!n) return;
const idx=new Map(nodes.map((nd,i)=>[nd.id,i]));
const indeg=new Array(n).fill(0);
const out=Array.from({length:n},()=>[]);
for (const e of edges) {
const f=idx.get(e.from), t=idx.get(e.to);
if (f!==undefined&&t!==undefined&&f!==t) { out[f].push(t); indeg[t]++; }
}
const queue=[], layer=new Array(n).fill(0), order=[];
for (let i=0;i<n;i++) if (!indeg[i]) queue.push(i);
let head=0;
while (head<queue.length) {
const u=queue[head++]; order.push(u);
for (const v of out[u]) { layer[v]=Math.max(layer[v],layer[u]+1); if (!--indeg[v]) queue.push(v); }
}
const maxL=Math.max(...layer,0);
for (let i=0;i<n;i++) if (!order.includes(i)) { layer[i]=maxL+1; order.push(i); }
const byL=new Map();
for (let i=0;i<n;i++) { if (!byL.has(layer[i])) byL.set(layer[i],[]); byL.get(layer[i]).push(i); }
for (const [l,idxs] of byL) {
const sepX=180, sepY=120, rowW=(idxs.length-1)*sepX;
idxs.forEach((id,k)=>{ nodes[id].x=k*sepX-rowW/2; nodes[id].y=l*sepY; });
}
}
function layoutForce(nodes, edges, maxMs=700) {
const n=nodes.length; if (!n) return;
// Node sizes are pre-cached by doRender before calling layout
const sz = nodes.map(nd => labelSize(nd));
nodes.forEach((nd,i) => {
const a=(2*Math.PI*i/n), r=Math.max(150,n*40);
nd.x=Math.cos(a)*r; nd.y=Math.sin(a)*r;
});
const idIdx=new Map(nodes.map((nd,i)=>[nd.id,i]));
const links=edges.map(e=>({f:idIdx.get(e.from),t:idIdx.get(e.to)})).filter(e=>e.f!==undefined&&e.t!==undefined&&e.f!==e.t);
const area=Math.max(800*600,n*n*600);
const k=Math.sqrt(area/Math.max(n,1));
const kk=k*k;
const dx=new Float64Array(n), dy=new Float64Array(n);
const start=performance.now(); let temp=k*2;
for (let iter=0; iter<1000; iter++) {
if (performance.now()-start>maxMs) break;
dx.fill(0); dy.fill(0);
for (let i=0;i<n;i++) for (let j=i+1;j<n;j++) {
let rx=nodes[i].x-nodes[j].x, ry=nodes[i].y-nodes[j].y;
const d=Math.sqrt(rx*rx+ry*ry)||0.1;
const ux=rx/d, uy=ry/d;
// Standard repulsion
dx[i]+=ux*kk/d; dy[i]+=uy*kk/d; dx[j]-=ux*kk/d; dy[j]-=uy*kk/d;
// Overlap prevention: push apart if centres are closer than combined half-widths + margin
const minSep = (sz[i].w + sz[j].w) / 2 + (sz[i].h + sz[j].h) / 4 + 14;
if (d < minSep) {
const push = (minSep - d) * 3;
dx[i]+=ux*push; dy[i]+=uy*push; dx[j]-=ux*push; dy[j]-=uy*push;
}
}
for (const {f,t} of links) {
let rx=nodes[t].x-nodes[f].x, ry=nodes[t].y-nodes[f].y;
const d=Math.sqrt(rx*rx+ry*ry)||0.1, force=d*d/k;
rx=rx/d*force; ry=ry/d*force;
dx[f]+=rx; dy[f]+=ry; dx[t]-=rx; dy[t]-=ry;
}
for (let i=0;i<n;i++) {
const sp=Math.sqrt(dx[i]*dx[i]+dy[i]*dy[i])||1;
const cap=Math.min(sp,temp);
nodes[i].x+=dx[i]/sp*cap; nodes[i].y+=dy[i]/sp*cap;
}
temp*=0.97;
}
}
// ── Color contrast helpers ────────────────────────────────────────────────────
// Parse any CSS color string to {r,g,b} using a canvas (handles named colors too)
const _cc = document.createElement('canvas'); _cc.width=_cc.height=1;
const _cx = _cc.getContext('2d');
function colorToRgb(color) {
if (!color || color==='none' || color==='transparent') return null;
_cx.clearRect(0,0,1,1); _cx.fillStyle='#000'; _cx.fillStyle=color;
const css=_cx.fillStyle;
const m=css.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
if (m) return {r:parseInt(m[1],16), g:parseInt(m[2],16), b:parseInt(m[3],16)};
const m2=css.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (m2) return {r:+m2[1], g:+m2[2], b:+m2[3]};
return null;
}
function relativeLuminance({r,g,b}) {
return [r,g,b].reduce((s,c,i)=>{ c/=255; return s+[0.2126,0.7152,0.0722][i]*(c<=0.03928?c/12.92:Math.pow((c+0.055)/1.055,2.4)); },0);
}
// Return a readable text color for a given background color
function autoTextColor(bgColor, explicit) {
if (explicit) return explicit;
const rgb = colorToRgb(bgColor);
if (!rgb) return '#c9d1d9';
return relativeLuminance(rgb) > 0.35 ? '#111111' : '#e8e8e8';
}
// ── Graph state ───────────────────────────────────────────────────────────────
let graph=null, selectedId=null, searchMatches=new Set();
function isConnected(id) {
if (!selectedId||!graph) return false;
return graph.edges.some(e=>(e.from===selectedId&&e.to===id)||(e.to===selectedId&&e.from===id));
}
// ── Rendering ─────────────────────────────────────────────────────────────────
const svgEl = document.getElementById('svgCanvas');
const viewport = document.getElementById('viewport');
const edgesGrp = document.getElementById('edgesGroup');
const nodeShapesGrp = document.getElementById('nodeShapesGroup');
const nodeLabelsGrp = document.getElementById('nodeLabelsGroup');
function nodeColors(nd) {
const isSel = nd.id===selectedId;
const isConn = !isSel && isConnected(nd.id);
const isDim = !!selectedId && !isSel && !isConn;
const inSrch = searchMatches.has(nd.id);
const st = parseStyle(nd.attrs.style);
let fill, stroke, sw;
if (isSel) { fill='#1f4068'; stroke='#58a6ff'; sw=2.5; }
else if (isConn) { fill='#2d2208'; stroke='#e3b341'; sw=2; }
else if (inSrch) { fill='#0d2a18'; stroke='#3fb950'; sw=2; }
else if (isDim) { fill='#161b22'; stroke='#30363d'; sw=1; }
else {
// Honour DOT fill/stroke attrs
if (st.filled) {
fill = nd.attrs.fillcolor || nd.attrs.color || '#888';
} else if (nd.attrs.fillcolor) {
fill = nd.attrs.fillcolor; // fillcolor without style=filled still used in practice
} else {
fill = '#161b22';
}
stroke = nd.attrs.color || '#388bfd';
sw = st.bold ? 3 : 1.5;
}
const textColor = isSel ? '#79c0ff' : isConn ? '#e3b341' : inSrch ? '#3fb950' : autoTextColor(fill, nd.attrs.fontcolor);
const opacity = isDim ? 0.3 : 1;
return {fill, stroke, sw, st, textColor, opacity};
}
function renderGraph() {
if (!graph) return;
edgesGrp.innerHTML=''; nodeShapesGrp.innerHTML=''; nodeLabelsGrp.innerHTML='';
const nm=new Map(graph.nodes.map(n=>[n.id,n]));
// Edges
for (const edge of graph.edges) {
const fn=nm.get(edge.from), tn=nm.get(edge.to);
if (!fn||!tn||fn===tn) continue;
const p1=borderPt(fn,tn.x,tn.y), p2=borderPt(tn,fn.x,fn.y);
const isHi = selectedId && (edge.from===selectedId||edge.to===selectedId);
const isDim = selectedId && !isHi;
const isSrch= !selectedId && searchMatches.size && (searchMatches.has(edge.from)||searchMatches.has(edge.to));
let stroke, opacity, sw, mEnd, mStart;
if (isDim) { stroke='#21262d'; opacity=0.25; sw=1.2; mEnd='url(#arr-dim)'; mStart='url(#arr-dim)'; }
else if (isHi) { stroke='#e3b341'; opacity=1; sw=2.2; mEnd='url(#arr-hi)'; mStart='url(#arr-hi-s)'; }
else if (isSrch) { stroke='#3fb950'; opacity=0.9; sw=1.8; mEnd='url(#arr-srch)'; mStart='url(#arr-srch)'; }
else { stroke=edge.attrs.color||'#3d8bcd'; opacity=0.75; sw=1.4; mEnd='url(#arr-def)'; mStart='url(#arr-def-s)'; }
const eSt = parseStyle(edge.attrs.style);
const ln = document.createElementNS(NS,'line');
ln.setAttribute('x1',p1.x.toFixed(1)); ln.setAttribute('y1',p1.y.toFixed(1));
ln.setAttribute('x2',p2.x.toFixed(1)); ln.setAttribute('y2',p2.y.toFixed(1));
ln.setAttribute('stroke',stroke); ln.setAttribute('stroke-width',sw); ln.setAttribute('opacity',opacity);
if (eSt.dashed) ln.setAttribute('stroke-dasharray','8,4');
if (eSt.dotted) ln.setAttribute('stroke-dasharray','2,4');
const dir = String(edge.attrs.dir||(graph.directed?'forward':'none')).toLowerCase();
const aHead = String(edge.attrs.arrowhead||'normal').toLowerCase();
const aTail = String(edge.attrs.arrowtail||'none').toLowerCase();
if ((dir==='forward'||dir==='') && aHead!=='none') ln.setAttribute('marker-end', mEnd);
if (dir==='back' && aHead!=='none') ln.setAttribute('marker-start', mStart);
if (dir==='both') { ln.setAttribute('marker-end', mEnd); ln.setAttribute('marker-start', mStart); }
if (dir==='none' && graph.directed===false && aTail!=='none') {} // undirected: no arrows by default
edgesGrp.appendChild(ln);
// Edge label
if (edge.attrs.label) {
const lbl = String(edge.attrs.label);
const lines = lbl.split('\n');
const mx=(p1.x+p2.x)/2, my=(p1.y+p2.y)/2;
const lh=12;
lines.forEach((line,i)=>{
const t=document.createElementNS(NS,'text');
t.setAttribute('x',mx); t.setAttribute('y', my+(i-(lines.length-1)/2)*lh);
t.setAttribute('text-anchor','middle'); t.setAttribute('dominant-baseline','middle');
t.setAttribute('fill',stroke); t.setAttribute('font-size','11'); t.setAttribute('opacity',opacity);
t.textContent=line; edgesGrp.appendChild(t);
});
}
}
// Nodes — two-pass: all shapes first, then all labels on top.
// This guarantees labels are never painted over by an overlapping neighbour's fill.
for (const nd of graph.nodes) {
const {w,h}=labelSize(nd);
const {fill,stroke,sw,st,opacity}=nodeColors(nd);
const shapeG=document.createElementNS(NS,'g');
shapeG.setAttribute('data-id',nd.id);
shapeG.setAttribute('opacity',opacity);
shapeG.style.cursor='pointer';
for (const el of drawShape(nd.attrs.shape||'ellipse', nd.x, nd.y, w, h, fill, stroke, sw, st))
shapeG.appendChild(el);
nodeShapesGrp.appendChild(shapeG);
shapeG.addEventListener('mousedown', e=>{ e.stopPropagation(); onNodeDown(e,nd); });
}
for (const nd of graph.nodes) {
const {textColor,opacity}=nodeColors(nd);
const lbl=drawLabel(nd, nd.x, nd.y, textColor);
if (lbl) {
lbl.setAttribute('pointer-events','none');
lbl.setAttribute('opacity',opacity);
nodeLabelsGrp.appendChild(lbl);
}
}
}
// ── Pan & zoom ────────────────────────────────────────────────────────────────
let tx=0, ty=0, sc=1;
let panning=false, panOrig={x:0,y:0}, panMoved=false, panStart={x:0,y:0};
function applyVP() { viewport.setAttribute('transform',`translate(${tx.toFixed(2)},${ty.toFixed(2)}) scale(${sc.toFixed(4)})`); }
const graphPanel=document.getElementById('graphPanel');
graphPanel.addEventListener('mousedown', e=>{
if (e.button!==0) return;
panning=true; panMoved=false;
panOrig={x:e.clientX-tx, y:e.clientY-ty};
panStart={x:e.clientX, y:e.clientY};
graphPanel.classList.add('panning');
});
svgEl.addEventListener('wheel', e=>{
e.preventDefault();
const r=svgEl.getBoundingClientRect(), mx=e.clientX-r.left, my=e.clientY-r.top;
const ns=Math.min(20,Math.max(0.04,sc*(e.deltaY<0?1.12:1/1.12)));
tx=mx-(mx-tx)*(ns/sc); ty=my-(my-ty)*(ns/sc); sc=ns; applyVP();
},{passive:false});
// ── Node drag ─────────────────────────────────────────────────────────────────
let dragNd=null, dragOff={x:0,y:0}, dragMoved=false, dragStart={x:0,y:0};
function svgPt(cx,cy) {
const r=svgEl.getBoundingClientRect();
return {x:(cx-r.left-tx)/sc, y:(cy-r.top-ty)/sc};
}
function onNodeDown(e,nd) {
if (e.button!==0) return;
dragNd=nd; dragMoved=false; dragStart={x:e.clientX,y:e.clientY};
const p=svgPt(e.clientX,e.clientY); dragOff={x:p.x-nd.x,y:p.y-nd.y};
graphPanel.classList.add('dragging-node');
}
window.addEventListener('mousemove', e=>{
if (panning) { if (Math.hypot(e.clientX-panStart.x,e.clientY-panStart.y)>3) panMoved=true; tx=e.clientX-panOrig.x; ty=e.clientY-panOrig.y; applyVP(); return; }
if (dragNd) {
if (Math.hypot(e.clientX-dragStart.x,e.clientY-dragStart.y)>3) {
dragMoved=true;
const p=svgPt(e.clientX,e.clientY);
dragNd.x=p.x-dragOff.x; dragNd.y=p.y-dragOff.y;
renderGraph();
}
}
});
window.addEventListener('mouseup', e=>{
if (panning) { panning=false; graphPanel.classList.remove('panning'); if (!panMoved) { selectedId=null; renderGraph(); updateStatus(); } }
if (dragNd) {
if (!dragMoved) { selectedId=selectedId===dragNd.id?null:dragNd.id; renderGraph(); updateStatus(); }
dragNd=null; graphPanel.classList.remove('dragging-node');
}
});
// ── Search ────────────────────────────────────────────────────────────────────
const searchBox=document.getElementById('searchBox');
const searchInfo=document.getElementById('searchInfo');
let srchCursor=0;
searchBox.addEventListener('input',()=>{
const term=searchBox.value.trim().toLowerCase();
searchMatches.clear();
if (!graph) return;
if (term) {
for (const nd of graph.nodes)
if (nd.label.toLowerCase().includes(term)||nd.id.toLowerCase().includes(term))
searchMatches.add(nd.id);
searchInfo.textContent=searchMatches.size?`${searchMatches.size} match${searchMatches.size>1?'es':''}`:'No matches';
searchInfo.style.color=searchMatches.size?'#3fb950':'#f85149';
if (searchMatches.size) { const first=graph.nodes.find(n=>searchMatches.has(n.id)); if (first) panTo(first.x,first.y); }
} else { searchInfo.textContent=''; }
renderGraph();
});
searchBox.addEventListener('keydown',e=>{
if (!graph||!searchMatches.size) return;
if (e.key==='Enter'||e.key==='ArrowDown') { e.preventDefault(); srchCursor=(srchCursor+1)%searchMatches.size; }
else if (e.key==='ArrowUp') { e.preventDefault(); srchCursor=(srchCursor-1+searchMatches.size)%searchMatches.size; }
else return;
const nd=graph.nodes.find(n=>n.id===[...searchMatches][srchCursor]);
if (nd) panTo(nd.x,nd.y);
});
function panTo(x,y) { const r=svgEl.getBoundingClientRect(); tx=r.width/2-x*sc; ty=r.height/2-y*sc; applyVP(); }
// ── Fit view ──────────────────────────────────────────────────────────────────
function fitView() {
if (!graph||!graph.nodes.length) return;
let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
for (const nd of graph.nodes) {
const {w,h}=labelSize(nd);
minX=Math.min(minX,nd.x-w/2); minY=Math.min(minY,nd.y-h/2);
maxX=Math.max(maxX,nd.x+w/2); maxY=Math.max(maxY,nd.y+h/2);
}
const pad=50, r=svgEl.getBoundingClientRect();
const gw=maxX-minX+pad*2, gh=maxY-minY+pad*2;
sc=Math.min(r.width/gw, r.height/gh, 2);
tx=r.width/2-((minX+maxX)/2)*sc; ty=r.height/2-((minY+maxY)/2)*sc;
applyVP();
}
// ── Render pipeline ───────────────────────────────────────────────────────────
const dotInput = document.getElementById('dotInput');
const errorMsg = document.getElementById('errorMsg');
const layoutSel = document.getElementById('layoutSelect');
function doRender() {
const src=dotInput.value.trim();
if (!src) { errorMsg.textContent='Paste a DOT graph first.'; return; }
errorMsg.textContent='';
try {
graph=parseDOT(src);
if (!graph.nodes.length) { errorMsg.textContent='No nodes found.'; return; }
selectedId=null; searchMatches.clear(); searchBox.value=''; searchInfo.textContent=''; srchCursor=0;
sizeCache.clear();
for (const nd of graph.nodes) { nd.x=0; nd.y=0; labelSize(nd); } // pre-measure before layout
const algo=layoutSel.value;
if (algo==='circular') layoutCircular(graph.nodes);
else if (algo==='hierarchical') layoutHierarchical(graph.nodes, graph.edges);
else layoutForce(graph.nodes, graph.edges);
removeOverlaps(graph.nodes); // guarantee no node bounding box covers another's label
document.getElementById('hint').style.display='none';
renderGraph(); fitView(); updateStatus();
} catch(err) { errorMsg.textContent='Error: '+err.message; console.error(err); }
}
function updateStatus() {
const bar=document.getElementById('statusBar');
if (!graph) { bar.innerHTML='Ready &mdash; paste a DOT graph and click Render.'; return; }
const n=graph.nodes.length, e=graph.edges.length;
let s=`<span>${n} node${n!==1?'s':''}, ${e} edge${e!==1?'s':''}</span>`;
if (selectedId) {
const nd=graph.nodes.find(x=>x.id===selectedId);
const out=graph.edges.filter(x=>x.from===selectedId).length;
const inc=graph.edges.filter(x=>x.to===selectedId).length;
s+=` &nbsp;|&nbsp; Selected: <span>"${nd?.label||selectedId}"</span> &nbsp;<span>${inc} in, ${out} out</span>`;
}
bar.innerHTML=s;
}
document.getElementById('renderBtn').addEventListener('click',doRender);
document.getElementById('renderBtn2').addEventListener('click',doRender);
document.getElementById('clearBtn').addEventListener('click',()=>{
dotInput.value=''; graph=null; selectedId=null; searchMatches.clear();
edgesGrp.innerHTML=''; nodeShapesGrp.innerHTML=''; nodeLabelsGrp.innerHTML='';
document.getElementById('hint').style.display='';
errorMsg.textContent=''; updateStatus();
});
document.getElementById('fitBtn').addEventListener('click',fitView);
dotInput.addEventListener('keydown',e=>{ if ((e.ctrlKey||e.metaKey)&&e.key==='Enter') { e.preventDefault(); doRender(); } });
document.addEventListener('keydown',e=>{
if (e.key==='f'||e.key==='F') { if (document.activeElement!==dotInput&&document.activeElement!==searchBox) fitView(); }
if (e.key==='Escape') { selectedId=null; if (graph){renderGraph();updateStatus();} }
});
// Panel resize
const resizer=document.getElementById('resizer');
const inputPanel=document.getElementById('inputPanel');
let resizing=false, rsStart=0, rsW0=0;
resizer.addEventListener('mousedown',e=>{ resizing=true; rsStart=e.clientX; rsW0=inputPanel.offsetWidth; resizer.classList.add('active'); e.preventDefault(); });
window.addEventListener('mousemove',e=>{ if (resizing) inputPanel.style.width=Math.max(180,Math.min(600,rsW0+e.clientX-rsStart))+'px'; });
window.addEventListener('mouseup',()=>{ if (resizing){resizing=false;resizer.classList.remove('active');} });
// ── Demo ──────────────────────────────────────────────────────────────────────
dotInput.value = `digraph TypeGraph {
rankdir=TB
node [ fontname="Helvetica" style=filled ]
1 [ label=<<b>Value</b><br/>t:1 (NamedType)> shape=box fillcolor=lightblue ]
2 [ label=<<b>Integer</b><br/>t:2 (NamedType)> shape=box fillcolor=lightblue ]
3 [ label=<<b>Float</b><br/>t:3 (NamedType)> shape=box fillcolor=lightblue ]
4 [ label=<<b>String</b><br/>t:4 (NamedType)> shape=box fillcolor=lightblue ]
5 [ label=<<b>Nothing</b><br/>t:5 (NamedType)> shape=box fillcolor=lightblue ]
6 [ label=<<b>True</b><br/>t:6 (NamedType)> shape=box fillcolor=lightblue ]
7 [ label=<<b>False</b><br/>t:7 (NamedType)> shape=box fillcolor=lightblue ]
8 [ label=<<b>Entity</b><br/>t:8 (NamedType)> shape=box fillcolor=lightblue ]
9 [ label=<<b>Component</b><br/>t:9 (NamedType)> shape=box fillcolor=lightblue ]
10 [ label=<<b>Structure</b><br/>t:10 (NamedType)> shape=box fillcolor=lightblue ]
11 [ label=<<b>Primitive</b><br/>t:11 (NamedType)> shape=box fillcolor=lightblue ]
12 [ label=<<b>Process</b><br/>t:12 (NamedType)> shape=box fillcolor=lightblue ]
13 [ label="Port {'Value}\nt:13 (ParameterizedType)" shape=hexagon fillcolor=orange ]
14 [ label="Pipe {'Value}\nt:14 (ParameterizedType)" shape=hexagon fillcolor=orange ]
15 [ label=<<b>Component::ProcessData</b><br/>t:15 (NamedType)> shape=box fillcolor=lightblue ]
16 [ label=<<b>Component::PortData</b><br/>t:16 (NamedType)> shape=box fillcolor=lightblue ]
17 [ label=<<b>Component::BehaviorAgentData</b><br/>t:17 (NamedType)> shape=box fillcolor=lightblue ]
18 [ label=<<b>Component::PlaneData</b><br/>t:18 (NamedType)> shape=box fillcolor=lightblue ]
19 [ label="<invalid>\nt:19 (Invalid)" shape=ellipse fillcolor=white ]
20 [ label=<<b>Component::PlaneVertexData</b><br/>t:20 (NamedType)> shape=box fillcolor=lightblue ]
21 [ label=<<b>Component::ArtifactData</b><br/>t:21 (NamedType)> shape=box fillcolor=lightblue ]
22 [ label="<invalid>\nt:22 (Invalid)" shape=ellipse fillcolor=white ]
23 [ label="<invalid>\nt:23 (Invalid)" shape=ellipse fillcolor=white ]
24 [ label="<invalid>\nt:24 (Invalid)" shape=ellipse fillcolor=white ]
25 [ label="<invalid>\nt:25 (Invalid)" shape=ellipse fillcolor=white ]
26 [ label=<<b>Float2</b><br/>t:26 (NamedType)> shape=box fillcolor=lightblue ]
27 [ label="<invalid>\nt:27 (Invalid)" shape=ellipse fillcolor=white ]
28 [ label="<invalid>\nt:28 (Invalid)" shape=ellipse fillcolor=white ]
29 [ label="<invalid>\nt:29 (Invalid)" shape=ellipse fillcolor=white ]
30 [ label="<invalid>\nt:30 (Invalid)" shape=ellipse fillcolor=white ]
31 [ label="<invalid>\nt:31 (Invalid)" shape=ellipse fillcolor=white ]
32 [ label=<<b>Component::LocalTransform</b><br/>t:32 (NamedType)> shape=box fillcolor=lightblue ]
33 [ label=<<b>Component::LocalToWorld</b><br/>t:33 (NamedType)> shape=box fillcolor=lightblue ]
34 [ label="<invalid>\nt:34 (Invalid)" shape=ellipse fillcolor=white ]
35 [ label=<<b>test</b><br/>t:35 (NamedType)> shape=box fillcolor=lightblue ]
36 [ label=<<b>Boolean</b><br/>t:36 (NamedType)> shape=box fillcolor=lightblue ]
37 [ label=<<b>Float3</b><br/>t:37 (NamedType)> shape=box fillcolor=lightblue ]
38 [ label=<<b>Float4</b><br/>t:38 (NamedType)> shape=box fillcolor=lightblue ]
39 [ label=<<b>Integer2</b><br/>t:39 (NamedType)> shape=box fillcolor=lightblue ]
40 [ label=<<b>Integer3</b><br/>t:40 (NamedType)> shape=box fillcolor=lightblue ]
41 [ label=<<b>Integer4</b><br/>t:41 (NamedType)> shape=box fillcolor=lightblue ]
42 [ label=<<b>Color</b><br/>t:42 (NamedType)> shape=box fillcolor=lightblue ]
43 [ label=<<b>Color::RGBA</b><br/>t:43 (NamedType)> shape=box fillcolor=lightblue ]
44 [ label=<<b>Color::HSV</b><br/>t:44 (NamedType)> shape=box fillcolor=lightblue ]
45 [ label=<<b>Date</b><br/>t:45 (NamedType)> shape=box fillcolor=lightblue ]
46 [ label=<<b>Date::Today</b><br/>t:46 (NamedType)> shape=box fillcolor=lightblue ]
47 [ label=<<b>Time</b><br/>t:47 (NamedType)> shape=box fillcolor=lightblue ]
48 [ label=<<b>Time::Now</b><br/>t:48 (NamedType)> shape=box fillcolor=lightblue ]
49 [ label=<<b>Component::Buffer</b><br/>t:49 (NamedType)> shape=box fillcolor=lightblue ]
50 [ label=<<b>Artifact</b><br/>t:50 (NamedType)> shape=box fillcolor=lightblue ]
51 [ label=<<b>LocalArtifact</b><br/>t:51 (NamedType)> shape=box fillcolor=lightblue ]
52 [ label=<<b>Plane</b><br/>t:52 (NamedType)> shape=box fillcolor=lightblue ]
53 [ label=<<b>DetectedObject</b><br/>t:53 (NamedType)> shape=box fillcolor=lightblue ]
54 [ label=<<b>Anchor</b><br/>t:54 (NamedType)> shape=box fillcolor=lightblue ]
55 [ label=<<b>Prop</b><br/>t:55 (NamedType)> shape=box fillcolor=lightblue ]
56 [ label=<<b>World::Location</b><br/>t:56 (NamedType)> shape=box fillcolor=lightblue ]
57 [ label=<<b>World::Sky</b><br/>t:57 (NamedType)> shape=box fillcolor=lightblue ]
58 [ label=<<b>Player</b><br/>t:58 (NamedType)> shape=box fillcolor=lightblue ]
59 [ label=<<b>Appearance</b><br/>t:59 (NamedType)> shape=box fillcolor=lightblue ]
60 [ label=<<b>Appearance::UI</b><br/>t:60 (NamedType)> shape=box fillcolor=lightblue ]
61 [ label=<<b>Appearance::Model</b><br/>t:61 (NamedType)> shape=box fillcolor=lightblue ]
62 [ label=<<b>Appearance::Effect</b><br/>t:62 (NamedType)> shape=box fillcolor=lightblue ]
63 [ label=<<b>Behavior</b><br/>t:63 (NamedType)> shape=box fillcolor=lightblue ]
64 [ label=<<b>Action</b><br/>t:64 (NamedType)> shape=box fillcolor=lightblue ]
65 [ label=<<b>State</b><br/>t:65 (NamedType)> shape=box fillcolor=lightblue ]
66 [ label=<<b>'Value</b><br/>t:66 (VariableType)> shape=box fillcolor=lightyellow ]
67 [ label=<<b>Port</b><br/>t:67 (NamedType)> shape=box fillcolor=lightblue ]
68 [ label=<<b>'Value</b><br/>t:68 (VariableType)> shape=box fillcolor=lightyellow ]
69 [ label=<<b>InputPort</b><br/>t:69 (NamedType)> shape=box fillcolor=lightblue ]
70 [ label="'Value {'Value}\nt:70 (ParameterizedType)" shape=hexagon fillcolor=orange ]
71 [ label=<<b>Trigger</b><br/>t:71 (NamedType)> shape=box fillcolor=lightblue ]
72 [ label=<<b>'Value</b><br/>t:72 (VariableType)> shape=box fillcolor=lightyellow ]
73 [ label=<<b>OutputPort</b><br/>t:73 (NamedType)> shape=box fillcolor=lightblue ]
74 [ label="'Value {'Value}\nt:74 (ParameterizedType)" shape=hexagon fillcolor=orange ]
75 [ label=<<b>'Value</b><br/>t:75 (VariableType)> shape=box fillcolor=lightyellow ]
76 [ label=<<b>Pipe</b><br/>t:76 (NamedType)> shape=box fillcolor=lightblue ]
77 [ label=<<b>Config</b><br/>t:77 (NamedType)> shape=box fillcolor=lightblue ]
78 [ label=<<b>'Value</b><br/>t:78 (VariableType)> shape=box fillcolor=lightyellow ]
79 [ label=<<b>Event</b><br/>t:79 (NamedType)> shape=box fillcolor=lightblue ]
80 [ label="'Value {'Value}\nt:80 (ParameterizedType)" shape=hexagon fillcolor=orange ]
81 [ label=<<b>Event::ProgramStarted</b><br/>t:81 (NamedType)> shape=box fillcolor=lightblue ]
82 [ label=<<b>Event::ProgramExiting</b><br/>t:82 (NamedType)> shape=box fillcolor=lightblue ]
83 [ label=<<b>'State</b><br/>t:83 (VariableType)> shape=box fillcolor=lightyellow ]
84 [ label=<<b>Event::StateEntered</b><br/>t:84 (NamedType)> shape=box fillcolor=lightblue ]
85 [ label="'State {'State}\nt:85 (ParameterizedType)" shape=hexagon fillcolor=orange ]
86 [ label=<<b>'State</b><br/>t:86 (VariableType)> shape=box fillcolor=lightyellow ]
87 [ label=<<b>Event::StateExited</b><br/>t:87 (NamedType)> shape=box fillcolor=lightblue ]
88 [ label="'State {'State}\nt:88 (ParameterizedType)" shape=hexagon fillcolor=orange ]
89 [ label=<<b>'State</b><br/>t:89 (VariableType)> shape=box fillcolor=lightyellow ]
90 [ label=<<b>Event::StateAborted</b><br/>t:90 (NamedType)> shape=box fillcolor=lightblue ]
91 [ label="'State {'State}\nt:91 (ParameterizedType)" shape=hexagon fillcolor=orange ]
92 [ label=<<b>'State</b><br/>t:92 (VariableType)> shape=box fillcolor=lightyellow ]
93 [ label=<<b>Event::StateEnabled</b><br/>t:93 (NamedType)> shape=box fillcolor=lightblue ]
94 [ label="'State {'State}\nt:94 (ParameterizedType)" shape=hexagon fillcolor=orange ]
95 [ label=<<b>'State</b><br/>t:95 (VariableType)> shape=box fillcolor=lightyellow ]
96 [ label=<<b>Event::StateDisabled</b><br/>t:96 (NamedType)> shape=box fillcolor=lightblue ]
97 [ label="'State {'State}\nt:97 (ParameterizedType)" shape=hexagon fillcolor=orange ]
98 [ label=<<b>Quality</b><br/>t:98 (NamedType)> shape=box fillcolor=lightblue ]
99 [ label=<<b>Classification</b><br/>t:99 (NamedType)> shape=box fillcolor=lightblue ]
100 [ label=<<b>Classification::Table</b><br/>t:100 (NamedType)> shape=box fillcolor=lightblue ]
101 [ label=<<b>Classification::Chair</b><br/>t:101 (NamedType)> shape=box fillcolor=lightblue ]
102 [ label=<<b>Classification::Lamp</b><br/>t:102 (NamedType)> shape=box fillcolor=lightblue ]
103 [ label=<<b>Classification::Wall</b><br/>t:103 (NamedType)> shape=box fillcolor=lightblue ]
104 [ label=<<b>Classification::Floor</b><br/>t:104 (NamedType)> shape=box fillcolor=lightblue ]
105 [ label=<<b>Classification::Window</b><br/>t:105 (NamedType)> shape=box fillcolor=lightblue ]
106 [ label=<<b>Classification::Door</b><br/>t:106 (NamedType)> shape=box fillcolor=lightblue ]
107 [ label=<<b>Classification::Organic</b><br/>t:107 (NamedType)> shape=box fillcolor=lightblue ]
108 [ label=<<b>Classification::Animal</b><br/>t:108 (NamedType)> shape=box fillcolor=lightblue ]
109 [ label=<<b>Classification::Cat</b><br/>t:109 (NamedType)> shape=box fillcolor=lightblue ]
110 [ label=<<b>Classification::Dog</b><br/>t:110 (NamedType)> shape=box fillcolor=lightblue ]
111 [ label=<<b>Classification::Bird</b><br/>t:111 (NamedType)> shape=box fillcolor=lightblue ]
112 [ label=<<b>Classification::Plant</b><br/>t:112 (NamedType)> shape=box fillcolor=lightblue ]
113 [ label=<<b>Classification::Tree</b><br/>t:113 (NamedType)> shape=box fillcolor=lightblue ]
114 [ label=<<b>Classification::Human</b><br/>t:114 (NamedType)> shape=box fillcolor=lightblue ]
115 [ label=<<b>Surface</b><br/>t:115 (NamedType)> shape=box fillcolor=lightblue ]
116 [ label=<<b>Surface::Stone</b><br/>t:116 (NamedType)> shape=box fillcolor=lightblue ]
117 [ label=<<b>Surface::Brick</b><br/>t:117 (NamedType)> shape=box fillcolor=lightblue ]
118 [ label=<<b>Surface::Fabric</b><br/>t:118 (NamedType)> shape=box fillcolor=lightblue ]
119 [ label=<<b>Surface::Plastic</b><br/>t:119 (NamedType)> shape=box fillcolor=lightblue ]
120 [ label=<<b>Surface::Metal</b><br/>t:120 (NamedType)> shape=box fillcolor=lightblue ]
121 [ label=<<b>Surface::Organic</b><br/>t:121 (NamedType)> shape=box fillcolor=lightblue ]
122 [ label=<<b>Surface::Wood</b><br/>t:122 (NamedType)> shape=box fillcolor=lightblue ]
123 [ label=<<b>Weather</b><br/>t:123 (NamedType)> shape=box fillcolor=lightblue ]
124 [ label=<<b>Weather::Sunny</b><br/>t:124 (NamedType)> shape=box fillcolor=lightblue ]
125 [ label=<<b>Weather::Cloudy</b><br/>t:125 (NamedType)> shape=box fillcolor=lightblue ]
126 [ label=<<b>Weather::Rainy</b><br/>t:126 (NamedType)> shape=box fillcolor=lightblue ]
127 [ label=<<b>Weather::Stormy</b><br/>t:127 (NamedType)> shape=box fillcolor=lightblue ]
128 [ label=<<b>Setting</b><br/>t:128 (NamedType)> shape=box fillcolor=lightblue ]
129 [ label=<<b>Setting::Inside</b><br/>t:129 (NamedType)> shape=box fillcolor=lightblue ]
130 [ label=<<b>Setting::Home</b><br/>t:130 (NamedType)> shape=box fillcolor=lightblue ]
131 [ label=<<b>Setting::Bedroom</b><br/>t:131 (NamedType)> shape=box fillcolor=lightblue ]
132 [ label=<<b>Setting::LivingRoom</b><br/>t:132 (NamedType)> shape=box fillcolor=lightblue ]
133 [ label=<<b>Setting::Hallway</b><br/>t:133 (NamedType)> shape=box fillcolor=lightblue ]
134 [ label=<<b>Setting::Kitchen</b><br/>t:134 (NamedType)> shape=box fillcolor=lightblue ]
135 [ label=<<b>Setting::Bathroom</b><br/>t:135 (NamedType)> shape=box fillcolor=lightblue ]
136 [ label=<<b>Setting::Shop</b><br/>t:136 (NamedType)> shape=box fillcolor=lightblue ]
137 [ label=<<b>Setting::Outside</b><br/>t:137 (NamedType)> shape=box fillcolor=lightblue ]
138 [ label=<<b>Setting::City</b><br/>t:138 (NamedType)> shape=box fillcolor=lightblue ]
139 [ label=<<b>Setting::Nature</b><br/>t:139 (NamedType)> shape=box fillcolor=lightblue ]
140 [ label=<<b>Setting::Park</b><br/>t:140 (NamedType)> shape=box fillcolor=lightblue ]
141 [ label=<<b>Setting::Graveyard</b><br/>t:141 (NamedType)> shape=box fillcolor=lightblue ]
142 [ label=<<b>Setting::Playground</b><br/>t:142 (NamedType)> shape=box fillcolor=lightblue ]
143 [ label=<<b>Orientation</b><br/>t:143 (NamedType)> shape=box fillcolor=lightblue ]
144 [ label=<<b>Orientation::Vertical</b><br/>t:144 (NamedType)> shape=box fillcolor=lightblue ]
145 [ label=<<b>Orientation::Horizontal</b><br/>t:145 (NamedType)> shape=box fillcolor=lightblue ]
146 [ label=<<b>'Value</b><br/>t:146 (VariableType)> shape=box fillcolor=lightyellow ]
147 [ label=<<b>Optional</b><br/>t:147 (NamedType)> shape=box fillcolor=lightblue ]
148 [ label="'Value {'Value}\nt:148 (ParameterizedType)" shape=hexagon fillcolor=orange ]
149 [ label=<<b>Locator</b><br/>t:149 (NamedType)> shape=box fillcolor=lightblue ]
150 [ label=<<b>Locator::Spatial</b><br/>t:150 (NamedType)> shape=box fillcolor=lightblue ]
151 [ label=<<b>Locator::Absolute</b><br/>t:151 (NamedType)> shape=box fillcolor=lightblue ]
152 [ label=<<b>Locator::WorldTransform</b><br/>t:152 (NamedType)> shape=box fillcolor=lightblue ]
153 [ label=<<b>Locator::Relative</b><br/>t:153 (NamedType)> shape=box fillcolor=lightblue ]
154 [ label=<<b>Locator::LocalTransform</b><br/>t:154 (NamedType)> shape=box fillcolor=lightblue ]
155 [ label=<<b>Locator::BoneTransform</b><br/>t:155 (NamedType)> shape=box fillcolor=lightblue ]
156 [ label=<<b>Locator::Geospatial</b><br/>t:156 (NamedType)> shape=box fillcolor=lightblue ]
157 [ label=<<b>Locator::Anchor</b><br/>t:157 (NamedType)> shape=box fillcolor=lightblue ]
158 [ label=<<b>Lifetime</b><br/>t:158 (NamedType)> shape=box fillcolor=lightblue ]
159 [ label=<<b>Lifetime::Persistent</b><br/>t:159 (NamedType)> shape=box fillcolor=lightblue ]
160 [ label=<<b>Lifetime::Transient</b><br/>t:160 (NamedType)> shape=box fillcolor=lightblue ]
161 [ label=<<b>Lifetime::Timer</b><br/>t:161 (NamedType)> shape=box fillcolor=lightblue ]
162 [ label=<<b>'Entity</b><br/>t:162 (VariableType)> shape=box fillcolor=lightyellow ]
163 [ label=<<b>Template</b><br/>t:163 (NamedType)> shape=box fillcolor=lightblue ]
164 [ label="'Entity {'Entity}\nt:164 (ParameterizedType)" shape=hexagon fillcolor=orange ]
165 [ label=<<b>Program</b><br/>t:165 (NamedType)> shape=box fillcolor=lightblue ]
166 [ label=<<b>'Value</b><br/>t:166 (VariableType)> shape=box fillcolor=lightyellow ]
167 [ label=<<b>CanBeParentedTo</b><br/>t:167 (NamedType)> shape=box fillcolor=lightblue ]
168 [ label="'Value {'Value}\nt:168 (ParameterizedType)" shape=hexagon fillcolor=orange ]
169 [ label=<<b>'Value</b><br/>t:169 (VariableType)> shape=box fillcolor=lightyellow ]
170 [ label=<<b>CanBeOwnedBy</b><br/>t:170 (NamedType)> shape=box fillcolor=lightblue ]
171 [ label="'Value {'Value}\nt:171 (ParameterizedType)" shape=hexagon fillcolor=orange ]
172 [ label=<<b>CanBeDisabled</b><br/>t:172 (NamedType)> shape=box fillcolor=lightblue ]
173 [ label=<<b>'Config</b><br/>t:173 (VariableType)> shape=box fillcolor=lightyellow ]
174 [ label=<<b>CanBeConfiguredUsing</b><br/>t:174 (NamedType)> shape=box fillcolor=lightblue ]
175 [ label="'Config {'Config}\nt:175 (ParameterizedType)" shape=hexagon fillcolor=orange ]
176 [ label=<<b>'Value</b><br/>t:176 (VariableType)> shape=box fillcolor=lightyellow ]
177 [ label=<<b>Parentable</b><br/>t:177 (NamedType)> shape=box fillcolor=lightblue ]
178 [ label="'Value {'Value}\nt:178 (ParameterizedType)" shape=hexagon fillcolor=orange ]
179 [ label=<<b>'Value</b><br/>t:179 (VariableType)> shape=box fillcolor=lightyellow ]
180 [ label=<<b>Ownable</b><br/>t:180 (NamedType)> shape=box fillcolor=lightblue ]
181 [ label="'Value {'Value}\nt:181 (ParameterizedType)" shape=hexagon fillcolor=orange ]
182 [ label=<<b>Disableable</b><br/>t:182 (NamedType)> shape=box fillcolor=lightblue ]
183 [ label=<<b>'Config</b><br/>t:183 (VariableType)> shape=box fillcolor=lightyellow ]
184 [ label=<<b>Configurable</b><br/>t:184 (NamedType)> shape=box fillcolor=lightblue ]
185 [ label="'Config {'Config}\nt:185 (ParameterizedType)" shape=hexagon fillcolor=orange ]
186 [ label=<<b>'Value</b><br/>t:186 (VariableType)> shape=box fillcolor=lightyellow ]
187 [ label="True | False\nt:187 (IntersectionType)" shape=ellipse fillcolor=lightpink ]
188 [ label="Float4 & Color\nt:188 (UnionType)" shape=ellipse fillcolor=lightcoral ]
189 [ label="Float3 & Color\nt:189 (UnionType)" shape=ellipse fillcolor=lightcoral ]
190 [ label="Artifact & 'Config {'Config}\nt:190 (UnionType)" shape=ellipse fillcolor=lightcoral ]
191 [ label="Entity & Disableable\nt:191 (UnionType)" shape=ellipse fillcolor=lightcoral ]
192 [ label="[Port {'Value}] ('Value)\nt:192 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
193 [ label="[Port {'Value}] ('Value)\nt:193 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
194 [ label="['Value {'Value}] ('State)\nt:194 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
195 [ label="['Value {'Value}] ('State)\nt:195 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
196 [ label="['Value {'Value}] ('State)\nt:196 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
197 [ label="['Value {'Value}] ('State)\nt:197 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
198 [ label="['Value {'Value}] ('State)\nt:198 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
199 [ label="Nothing | 'Value\nt:199 (IntersectionType)" shape=ellipse fillcolor=lightpink ]
200 [ label="Value & 'Config {'Config}\nt:200 (UnionType)" shape=ellipse fillcolor=lightcoral ]
201 [ label="['Entity {'Entity}] (Process)\nt:201 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
202 [ label="['Value {'Value}] (Classification)\nt:202 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
203 [ label="Plane | DetectedObject\nt:203 (IntersectionType)" shape=ellipse fillcolor=lightpink ]
204 [ label="['Value {'Value}] (Surface)\nt:204 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
205 [ label="['Value {'Value}] (Color::RGBA)\nt:205 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
206 [ label="['Value {'Value}] (Orientation)\nt:206 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
207 [ label="Float3 | Float4\nt:207 (IntersectionType)" shape=ellipse fillcolor=lightpink ]
208 [ label="Float2 | Float3 | Float4\nt:208 (IntersectionType)" shape=ellipse fillcolor=lightpink ]
209 [ label="Integer3 | Integer4\nt:209 (IntersectionType)" shape=ellipse fillcolor=lightpink ]
210 [ label="Integer2 | Integer3 | Integer4\nt:210 (IntersectionType)" shape=ellipse fillcolor=lightpink ]
211 [ label="['Config {'Config}] (Config)\nt:211 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
212 [ label="['Value {'Value}] (Value)\nt:212 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
213 [ label="[Pipe {'Value}] ('Value)\nt:213 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
214 [ label="Component::PlaneData & Component::PlaneVertexData\nt:214 (UnionType)" shape=ellipse fillcolor=lightcoral ]
215 [ label="Component::LocalTransform & Component::PlaneData & Component::PlaneVertexData\nt:215 (UnionType)" shape=ellipse fillcolor=lightcoral ]
216 [ label="Component::LocalToWorld & Component::LocalTransform & Component::PlaneData & Component::PlaneVertexData\nt:216 (UnionType)" shape=ellipse fillcolor=lightcoral ]
217 [ label="[Pipe {'Value}] ('Value) -> 'Value\nt:217 (MethodType)" shape=diamond fillcolor=lightgreen ]
218 [ label="[Pipe {'Value}] ('Value) -> 'Value {'Value}\nt:218 (ParameterizedType)" shape=hexagon fillcolor=orange ]
219 [ label="Plane -> Locator\nt:219 (MethodType)" shape=diamond fillcolor=lightgreen ]
220 [ label=<<b>Child</b><br/>t:220 (NamedType)> shape=box fillcolor=lightblue ]
221 [ label="[Port {'Value}] (Plane)\nt:221 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
222 [ label="Child -> [Port {'Value}] (Plane)\nt:222 (MethodType)" shape=diamond fillcolor=lightgreen ]
223 [ label="[Pipe {'Value}] (Plane)\nt:223 (InstancedType)" shape=parallelogram fillcolor=lightsalmon ]
1 -> 1 [ label="extends" ]
11 -> 2 [ label="extends" ]
11 -> 3 [ label="extends" ]
11 -> 4 [ label="extends" ]
1 -> 5 [ label="extends" ]
11 -> 6 [ label="extends" ]
11 -> 7 [ label="extends" ]
1 -> 8 [ label="extends" ]
10 -> 9 [ label="extends" ]
1 -> 10 [ label="extends" ]
1 -> 11 [ label="extends" ]
190 -> 12 [ label="extends" ]
66 -> 13 [ label="param" ]
67 -> 13 [ label="paramTarget" ]
75 -> 14 [ label="param" ]
76 -> 14 [ label="paramTarget" ]
9 -> 15 [ label="extends" ]
9 -> 16 [ label="extends" ]
9 -> 17 [ label="extends" ]
9 -> 18 [ label="extends" ]
49 -> 20 [ label="extends" ]
9 -> 21 [ label="extends" ]
10 -> 26 [ label="extends" ]
9 -> 32 [ label="extends" ]
9 -> 33 [ label="extends" ]
12 -> 35 [ label="extends" ]
1 -> 36 [ label="extends" ]
10 -> 37 [ label="extends" ]
10 -> 38 [ label="extends" ]
10 -> 39 [ label="extends" ]
10 -> 40 [ label="extends" ]
10 -> 41 [ label="extends" ]
10 -> 42 [ label="extends" ]
1 -> 43 [ label="extends" ]
1 -> 44 [ label="extends" ]
10 -> 45 [ label="extends" ]
45 -> 46 [ label="extends" ]
10 -> 47 [ label="extends" ]
47 -> 48 [ label="extends" ]
9 -> 49 [ label="extends" ]
8 -> 50 [ label="extends" ]
50 -> 51 [ label="extends" ]
51 -> 52 [ label="extends" ]
51 -> 53 [ label="extends" ]
50 -> 54 [ label="extends" ]
50 -> 55 [ label="extends" ]
12 -> 56 [ label="extends" ]
12 -> 57 [ label="extends" ]
12 -> 58 [ label="extends" ]
191 -> 59 [ label="extends" ]
59 -> 60 [ label="extends" ]
59 -> 61 [ label="extends" ]
59 -> 62 [ label="extends" ]
8 -> 63 [ label="extends" ]
191 -> 64 [ label="extends" ]
191 -> 65 [ label="extends" ]
1 -> 66 [ label="constraint" ]
8 -> 67 [ label="extends" ]
1 -> 68 [ label="constraint" ]
192 -> 69 [ label="extends" ]
68 -> 70 [ label="param" ]
68 -> 70 [ label="paramTarget" ]
70 -> 71 [ label="extends" ]
1 -> 72 [ label="constraint" ]
193 -> 73 [ label="extends" ]
72 -> 74 [ label="param" ]
72 -> 74 [ label="paramTarget" ]
1 -> 75 [ label="constraint" ]
1 -> 76 [ label="extends" ]
1 -> 77 [ label="extends" ]
1 -> 78 [ label="constraint" ]
10 -> 79 [ label="extends" ]
78 -> 80 [ label="param" ]
78 -> 80 [ label="paramTarget" ]
80 -> 81 [ label="extends" ]
80 -> 82 [ label="extends" ]
65 -> 83 [ label="constraint" ]
194 -> 84 [ label="extends" ]
83 -> 85 [ label="param" ]
83 -> 85 [ label="paramTarget" ]
65 -> 86 [ label="constraint" ]
195 -> 87 [ label="extends" ]
86 -> 88 [ label="param" ]
86 -> 88 [ label="paramTarget" ]
65 -> 89 [ label="constraint" ]
196 -> 90 [ label="extends" ]
89 -> 91 [ label="param" ]
89 -> 91 [ label="paramTarget" ]
65 -> 92 [ label="constraint" ]
197 -> 93 [ label="extends" ]
92 -> 94 [ label="param" ]
92 -> 94 [ label="paramTarget" ]
65 -> 95 [ label="constraint" ]
198 -> 96 [ label="extends" ]
95 -> 97 [ label="param" ]
95 -> 97 [ label="paramTarget" ]
1 -> 98 [ label="extends" ]
98 -> 99 [ label="extends" ]
99 -> 100 [ label="extends" ]
99 -> 101 [ label="extends" ]
99 -> 102 [ label="extends" ]
99 -> 103 [ label="extends" ]
99 -> 104 [ label="extends" ]
99 -> 105 [ label="extends" ]
99 -> 106 [ label="extends" ]
99 -> 107 [ label="extends" ]
107 -> 108 [ label="extends" ]
108 -> 109 [ label="extends" ]
108 -> 110 [ label="extends" ]
108 -> 111 [ label="extends" ]
107 -> 112 [ label="extends" ]
112 -> 113 [ label="extends" ]
107 -> 114 [ label="extends" ]
98 -> 115 [ label="extends" ]
115 -> 116 [ label="extends" ]
115 -> 117 [ label="extends" ]
115 -> 118 [ label="extends" ]
115 -> 119 [ label="extends" ]
115 -> 120 [ label="extends" ]
115 -> 121 [ label="extends" ]
121 -> 122 [ label="extends" ]
98 -> 123 [ label="extends" ]
123 -> 124 [ label="extends" ]
123 -> 125 [ label="extends" ]
123 -> 126 [ label="extends" ]
123 -> 127 [ label="extends" ]
98 -> 128 [ label="extends" ]
128 -> 129 [ label="extends" ]
129 -> 130 [ label="extends" ]
130 -> 131 [ label="extends" ]
129 -> 132 [ label="extends" ]
130 -> 133 [ label="extends" ]
130 -> 134 [ label="extends" ]
130 -> 135 [ label="extends" ]
129 -> 136 [ label="extends" ]
128 -> 137 [ label="extends" ]
137 -> 138 [ label="extends" ]
137 -> 139 [ label="extends" ]
137 -> 140 [ label="extends" ]
137 -> 141 [ label="extends" ]
137 -> 142 [ label="extends" ]
98 -> 143 [ label="extends" ]
143 -> 144 [ label="extends" ]
143 -> 145 [ label="extends" ]
1 -> 146 [ label="constraint" ]
1 -> 147 [ label="extends" ]
146 -> 148 [ label="param" ]
146 -> 148 [ label="paramTarget" ]
10 -> 149 [ label="extends" ]
149 -> 150 [ label="extends" ]
150 -> 151 [ label="extends" ]
151 -> 152 [ label="extends" ]
150 -> 153 [ label="extends" ]
153 -> 154 [ label="extends" ]
153 -> 155 [ label="extends" ]
149 -> 156 [ label="extends" ]
149 -> 157 [ label="extends" ]
1 -> 158 [ label="extends" ]
158 -> 159 [ label="extends" ]
158 -> 160 [ label="extends" ]
158 -> 161 [ label="extends" ]
8 -> 162 [ label="constraint" ]
200 -> 163 [ label="extends" ]
162 -> 164 [ label="param" ]
162 -> 164 [ label="paramTarget" ]
201 -> 165 [ label="extends" ]
1 -> 166 [ label="constraint" ]
1 -> 167 [ label="extends" ]
166 -> 168 [ label="param" ]
166 -> 168 [ label="paramTarget" ]
1 -> 169 [ label="constraint" ]
1 -> 170 [ label="extends" ]
169 -> 171 [ label="param" ]
169 -> 171 [ label="paramTarget" ]
1 -> 172 [ label="extends" ]
77 -> 173 [ label="constraint" ]
1 -> 174 [ label="extends" ]
173 -> 175 [ label="param" ]
173 -> 175 [ label="paramTarget" ]
1 -> 176 [ label="constraint" ]
176 -> 177 [ label="extends" ]
176 -> 178 [ label="param" ]
176 -> 178 [ label="paramTarget" ]
1 -> 179 [ label="constraint" ]
179 -> 180 [ label="extends" ]
179 -> 181 [ label="param" ]
179 -> 181 [ label="paramTarget" ]
1 -> 182 [ label="extends" ]
77 -> 183 [ label="constraint" ]
1 -> 184 [ label="extends" ]
183 -> 185 [ label="param" ]
183 -> 185 [ label="paramTarget" ]
1 -> 186 [ label="constraint" ]
6 -> 187 [ label="left" ]
7 -> 187 [ label="right" ]
38 -> 188 [ label="left" ]
42 -> 188 [ label="right" ]
37 -> 189 [ label="left" ]
42 -> 189 [ label="right" ]
50 -> 190 [ label="left" ]
185 -> 190 [ label="right" ]
8 -> 191 [ label="left" ]
182 -> 191 [ label="right" ]
13 -> 192 [ label="argTarget" ]
68 -> 192 [ label="arg" ]
13 -> 193 [ label="argTarget" ]
72 -> 193 [ label="arg" ]
80 -> 194 [ label="argTarget" ]
83 -> 194 [ label="arg" ]
80 -> 195 [ label="argTarget" ]
86 -> 195 [ label="arg" ]
80 -> 196 [ label="argTarget" ]
89 -> 196 [ label="arg" ]
80 -> 197 [ label="argTarget" ]
92 -> 197 [ label="arg" ]
80 -> 198 [ label="argTarget" ]
95 -> 198 [ label="arg" ]
5 -> 199 [ label="left" ]
146 -> 199 [ label="right" ]
1 -> 200 [ label="left" ]
185 -> 200 [ label="right" ]
164 -> 201 [ label="argTarget" ]
12 -> 201 [ label="arg" ]
148 -> 202 [ label="argTarget" ]
99 -> 202 [ label="arg" ]
52 -> 203 [ label="left" ]
53 -> 203 [ label="right" ]
148 -> 204 [ label="argTarget" ]
115 -> 204 [ label="arg" ]
148 -> 205 [ label="argTarget" ]
43 -> 205 [ label="arg" ]
148 -> 206 [ label="argTarget" ]
143 -> 206 [ label="arg" ]
37 -> 207 [ label="left" ]
38 -> 207 [ label="right" ]
26 -> 208 [ label="left" ]
207 -> 208 [ label="right" ]
40 -> 209 [ label="left" ]
41 -> 209 [ label="right" ]
39 -> 210 [ label="left" ]
209 -> 210 [ label="right" ]
185 -> 211 [ label="argTarget" ]
77 -> 211 [ label="arg" ]
80 -> 212 [ label="argTarget" ]
1 -> 212 [ label="arg" ]
14 -> 213 [ label="argTarget" ]
186 -> 213 [ label="arg" ]
18 -> 214 [ label="left" ]
20 -> 214 [ label="right" ]
32 -> 215 [ label="left" ]
214 -> 215 [ label="right" ]
33 -> 216 [ label="left" ]
215 -> 216 [ label="right" ]
213 -> 217 [ label="arg" ]
186 -> 217 [ label="result" ]
186 -> 218 [ label="param" ]
217 -> 218 [ label="paramTarget" ]
52 -> 219 [ label="arg" ]
149 -> 219 [ label="result" ]
12 -> 220 [ label="extends" ]
13 -> 221 [ label="argTarget" ]
52 -> 221 [ label="arg" ]
220 -> 222 [ label="arg" ]
221 -> 222 [ label="result" ]
14 -> 223 [ label="argTarget" ]
52 -> 223 [ label="arg" ]
}`;
doRender();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment