Last active
February 23, 2026 21:33
-
-
Save Rene-Damm/63831646257f2348b83a861212fe061d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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>◆ GraphViz Viewer</h1> | |
| <div id="searchWrap"> | |
| <input id="searchBox" type="text" placeholder="🔍 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 — 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 — 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+=` | Selected: <span>"${nd?.label||selectedId}"</span> <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