figdown 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/figdown.js +153 -9
- package/dist/figdown.mjs +153 -9
- package/examples/evpn-fabric.svg +1 -1
- package/examples/showcase/arp-resolution.svg +1 -1
- package/examples/showcase/ethernet-frame.svg +1 -1
- package/examples/showcase/l2-forwarding-logic.svg +1 -1
- package/examples/showcase/tcp-handshake.svg +1 -1
- package/examples/showcase/tcp-header.svg +1 -1
- package/examples/showcase/tcp-state-machine.svg +1 -1
- package/package.json +1 -1
- package/skill/figdown/figdown.html +151 -7
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "figdown",
|
|
4
4
|
"displayName": "FigDown",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.6",
|
|
6
6
|
"description": "Figures as text: create and maintain documentation figures as FigDown .fd sources with deterministic SVG artifacts embedded in Markdown — block diagrams, topologies, flowcharts, bit-level layouts, tables, timing waveforms.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "FigDown",
|
package/dist/figdown.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// figdown.js — FigDown embeddable library (0.1.
|
|
1
|
+
// figdown.js — FigDown embeddable library (0.1.6)
|
|
2
2
|
// GENERATED FILE, DO NOT EDIT. Built from editor/figdown.html.
|
|
3
3
|
// Regenerate with: node tools/make-lib.js
|
|
4
4
|
(function (root, factory) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
12
12
|
'use strict';
|
|
13
|
-
var VERSION = "0.1.
|
|
13
|
+
var VERSION = "0.1.6";
|
|
14
14
|
|
|
15
15
|
// ---- engine (extracted verbatim from editor/figdown.html) ----
|
|
16
16
|
var __engine = (function () {
|
|
@@ -24,7 +24,7 @@ const SHAPES = ['box','rounded','circle','ellipse','diamond','cylinder'];
|
|
|
24
24
|
// input to that promise, and under core §13 a 0.x renderer may differ from
|
|
25
25
|
// the next — which makes the recorded version the only thing that can
|
|
26
26
|
// explain a diff between two renderings of one source.
|
|
27
|
-
const FIGDOWN_VERSION = '0.1.
|
|
27
|
+
const FIGDOWN_VERSION = '0.1.6';
|
|
28
28
|
// Retired shape VALUES keep a named diagnostic (PROCESS §5(d)), the same way
|
|
29
29
|
// retired option keys do: `cloud` was the one value that named a domain
|
|
30
30
|
// (the internet cloud) in an enum the language keeps purely geometric
|
|
@@ -3585,6 +3585,89 @@ function routeAround(p,q,obs){
|
|
|
3585
3585
|
return pts.length>2?pts:null;
|
|
3586
3586
|
}
|
|
3587
3587
|
|
|
3588
|
+
// ---- shared-edge grid (0.1.6) ----
|
|
3589
|
+
// A boundary between two cells belongs to BOTH of them. Drawing each cell as a
|
|
3590
|
+
// stroked rect drew that boundary twice, and the second painting won: a marked
|
|
3591
|
+
// cell's colour, or a conditional field's dash, was whatever the neighbour
|
|
3592
|
+
// happened to paint last. So the grid is now emitted edge-by-edge, each edge
|
|
3593
|
+
// exactly once, and its appearance is decided by its (at most two) owners.
|
|
3594
|
+
//
|
|
3595
|
+
// WHAT THE OLD DRAWING DESTROYED. A `present=` field's shared edge was
|
|
3596
|
+
// overwritten solid by a plain neighbour. `STYLE-KEY-SCOPE` rules that the dash IS
|
|
3597
|
+
// conditional presence and that nothing else can set or clear it — so the
|
|
3598
|
+
// model's flag was being silently erased at render time. This is a
|
|
3599
|
+
// correctness fix, not a cosmetic one.
|
|
3600
|
+
//
|
|
3601
|
+
// WHY NOT HALF-WIDTH. The obvious alternative gives each owner half the
|
|
3602
|
+
// boundary: two 0.5px bands, one per side. Point-sampled at 1× — the size a
|
|
3603
|
+
// figure is actually read at — a shared edge between an orange mark and a
|
|
3604
|
+
// green one reads #2E6E34: one muddy pixel with the orange unrecoverable,
|
|
3605
|
+
// because half-width strokes sit at quarter-pixel offsets and the rasteriser
|
|
3606
|
+
// averages them. The bands here land on integer pixels and every colour
|
|
3607
|
+
// survives exactly. A zoomed crop hides this entirely, which is why the
|
|
3608
|
+
// decision was made at 1× and must be re-checked at 1× if it is revisited.
|
|
3609
|
+
//
|
|
3610
|
+
// THE RESIDUAL AMBIGUITY, stated rather than hidden. A dashed edge between a
|
|
3611
|
+
// conditional field and a plain one can be read locally as if the plain one
|
|
3612
|
+
// were conditional too. Today's drawing has the same ambiguity whenever the
|
|
3613
|
+
// dash happens to win the overwrite; this is strictly better, not solved.
|
|
3614
|
+
//
|
|
3615
|
+
// WHAT IT COSTS. Two adjacent marked cells put 3px of ink at their shared
|
|
3616
|
+
// boundary (ring + line + ring) against 1px elsewhere: the grid stays even,
|
|
3617
|
+
// but that region reads heavier. Four differently marked cells meeting at a
|
|
3618
|
+
// point give four ring corners around one crossing — busy at 8×, invisible
|
|
3619
|
+
// at 1×.
|
|
3620
|
+
//
|
|
3621
|
+
// `edges` are runs produced by edgeRuns(); `def` is the block's grid colour.
|
|
3622
|
+
function edgeSvg(edges, def){
|
|
3623
|
+
const W=1, out=[];
|
|
3624
|
+
const L=(v,p,s,e,col,dash,w)=>'<line x1="'+(v?p:s)+'" y1="'+(v?s:p)+'" x2="'+(v?p:e)+'" y2="'+(v?e:p)
|
|
3625
|
+
+'" stroke="'+col+'"'+(w!==1?' stroke-width="'+w+'"':'')+(dash?' stroke-dasharray="5 3"':'')+'/>';
|
|
3626
|
+
for(const g of edges){
|
|
3627
|
+
const A=g.a, B=g.b;
|
|
3628
|
+
// The shared line: default colour, full weight, dashed iff EITHER owner
|
|
3629
|
+
// carries `present=`. A field with both `stroke=` and `present=` therefore
|
|
3630
|
+
// gets a dashed boundary AND a separate coloured ring, not a coloured
|
|
3631
|
+
// dash — the two marks answer different questions and must not merge.
|
|
3632
|
+
out.push(L(g.v, g.p, g.s, g.e, def, (A&&A.d)||(B&&B.d), W));
|
|
3633
|
+
// The class marks: solid, full weight, inset INSIDE their own cell. An end
|
|
3634
|
+
// pulls in by W/2 where the owner stops and the ring turns the corner;
|
|
3635
|
+
// it is left long where the owner continues past that end and the next run
|
|
3636
|
+
// carries on, so a run that ends only because the NEIGHBOUR changed joins
|
|
3637
|
+
// with no seam and no ring protrudes past its own cell. The test is the
|
|
3638
|
+
// ownership map (does this cell own the next unit too?), not the geometry.
|
|
3639
|
+
if(A&&A.c) out.push(L(g.v, g.p-W, g.s+(g.ca0?0:W/2), g.e-(g.ca1?0:W/2), A.c, false, W));
|
|
3640
|
+
if(B&&B.c) out.push(L(g.v, g.p+W, g.s+(g.cb0?0:W/2), g.e-(g.cb1?0:W/2), B.c, false, W));
|
|
3641
|
+
}
|
|
3642
|
+
return out.join('');
|
|
3643
|
+
}
|
|
3644
|
+
// Walk one grid line unit by unit and merge collinear units that have the same
|
|
3645
|
+
// pair of owners into a single run. `v` is 1 for a vertical line, `p` its fixed
|
|
3646
|
+
// coordinate; `n` units; `span(i)` -> [start,end]; `ownAt(i)` -> [before,after]
|
|
3647
|
+
// where an owner is {id, c:colour|null, d:dashed}. A unit with no owner, or
|
|
3648
|
+
// with the SAME owner on both sides (a merged/spanning cell's interior), draws
|
|
3649
|
+
// nothing at all — which is where the doubled interior lines went.
|
|
3650
|
+
function edgeRuns(v, p, n, span, ownAt){
|
|
3651
|
+
const out=[]; let run=null;
|
|
3652
|
+
const sid=(o)=>o?o.id:null;
|
|
3653
|
+
const cont=(i,k,o)=>!!o && sid((ownAt(i)||[])[k])===o.id;
|
|
3654
|
+
for(let i=0;i<n;i++){
|
|
3655
|
+
const o=ownAt(i)||[], a=o[0]||null, b=o[1]||null;
|
|
3656
|
+
const skip=(!a&&!b)||(a&&b&&a.id===b.id);
|
|
3657
|
+
const key=skip?null:JSON.stringify([a&&[a.c,a.d], b&&[b.c,b.d]]);
|
|
3658
|
+
const sp=span(i);
|
|
3659
|
+
if(run && run.key===key && run.e===sp[0]){ run.e=sp[1]; run.i1=i; continue; }
|
|
3660
|
+
if(run){ out.push(run); run=null; }
|
|
3661
|
+
if(!skip) run={v:v,p:p,s:sp[0],e:sp[1],key:key,a:a,b:b,i0:i,i1:i};
|
|
3662
|
+
}
|
|
3663
|
+
if(run) out.push(run);
|
|
3664
|
+
for(const r of out){
|
|
3665
|
+
r.ca0=cont(r.i0-1,0,r.a); r.ca1=cont(r.i1+1,0,r.a);
|
|
3666
|
+
r.cb0=cont(r.i0-1,1,r.b); r.cb1=cont(r.i1+1,1,r.b);
|
|
3667
|
+
}
|
|
3668
|
+
return out;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3588
3671
|
// ---- bitfield ----
|
|
3589
3672
|
function renderBitfield(b,y0){
|
|
3590
3673
|
const cell=Math.max(18,Math.min(28,Math.floor(760/b.word))), rh=30, ruler=16;
|
|
@@ -3777,9 +3860,11 @@ function renderBitfield(b,y0){
|
|
|
3777
3860
|
// one edge a per-row rect cannot leave out: a rect strokes four sides or
|
|
3778
3861
|
// none, and "none" would also lose the two verticals that must continue.
|
|
3779
3862
|
// Fill stays a single closed region, so `fill=` paints the field once with
|
|
3780
|
-
// no seam
|
|
3781
|
-
//
|
|
3782
|
-
//
|
|
3863
|
+
// no seam. Since 0.1.6 the path is unstroked and the boundary comes from the
|
|
3864
|
+
// shared-edge grid instead; the internal boundary is still never drawn,
|
|
3865
|
+
// because both of its sides are owned by the same field and edgeRuns() skips
|
|
3866
|
+
// a unit whose two owners are one (`STYLE-KEY-SCOPE`: the dash IS conditional presence,
|
|
3867
|
+
// and nothing else may set or clear it).
|
|
3783
3868
|
const boxOutline=(bx)=>{
|
|
3784
3869
|
const bands=[];
|
|
3785
3870
|
for(const s of bx){
|
|
@@ -3801,6 +3886,11 @@ function renderBitfield(b,y0){
|
|
|
3801
3886
|
}
|
|
3802
3887
|
return 'M'+pts.map(p=>p[0]+' '+p[1]).join(' L')+' Z';
|
|
3803
3888
|
};
|
|
3889
|
+
// Shared-edge grid (0.1.6): field fills carry no stroke of their own. Every
|
|
3890
|
+
// bit column of every row records which field owns it, and the grid is
|
|
3891
|
+
// emitted once, edge by edge, after the fields — see edgeSvg() above.
|
|
3892
|
+
const BOWN=new Map(), BDEF=b.stroke||'#555';
|
|
3893
|
+
const bkey=(rw,cl)=>rw+':'+cl; let BOXN=0;
|
|
3804
3894
|
let pos=0; // bit cursor
|
|
3805
3895
|
for(const f of b.fields){
|
|
3806
3896
|
if(f.wrap){ pos=Math.ceil((pos||1)/b.word)*b.word; continue; }
|
|
@@ -3826,9 +3916,11 @@ function renderBitfield(b,y0){
|
|
|
3826
3916
|
// `PRESENCE-CONDITION-EXPRESSION`: the carrier is now `present=`, and BOTH of its
|
|
3827
3917
|
// written forms dash — `present=""` claims conditional presence just as
|
|
3828
3918
|
// `present="C = 1"` does; only the caption below distinguishes them.
|
|
3829
|
-
|
|
3919
|
+
// 0.1.6: the dash and the class colour no longer ride on the box's own
|
|
3920
|
+
// stroke — they are properties of the shared boundary and of the ring
|
|
3921
|
+
// inside the cell respectively, recorded per bit below and drawn once.
|
|
3830
3922
|
const cfill=f.fill||b.fill||'#fff';
|
|
3831
|
-
const paint=' fill="'+cfill+'" stroke="
|
|
3923
|
+
const paint=' fill="'+cfill+'" stroke="none"';
|
|
3832
3924
|
// Draw one occurrence. `suffix` is the derived index label — '' for a
|
|
3833
3925
|
// field that is not one element of a run, ' [0]' / ' [n]' for the two
|
|
3834
3926
|
// occurrences `REPEATED-RUN-DRAWING` draws. It is APPENDED to the author's label, and the
|
|
@@ -3863,6 +3955,13 @@ function renderBitfield(b,y0){
|
|
|
3863
3955
|
? '<rect x="'+bx[0].x+'" y="'+bx[0].y+'" width="'+bx[0].w+'" height="'+(bx.length*rh)+'"'+paint
|
|
3864
3956
|
: '<path d="'+boxOutline(bx)+'"'+paint;
|
|
3865
3957
|
const tag=flat?'rect':'path';
|
|
3958
|
+
// Record ownership per BIT, one entry per (row, column) this occurrence
|
|
3959
|
+
// covers. Every occurrence of a repeated field is its own owner id, so
|
|
3960
|
+
// the boundary between two occurrences is a real boundary and is drawn.
|
|
3961
|
+
const bid=b.id+'_'+(BOXN++);
|
|
3962
|
+
const brec={id:bid, c:f.stroke||null, d:f.present!==undefined};
|
|
3963
|
+
bx.forEach(function(s){ const c0=Math.round(s.x/cell), n=Math.round(s.w/cell);
|
|
3964
|
+
for(let k=0;k<n;k++) BOWN.set(bkey(s.row,c0+k), brec); });
|
|
3866
3965
|
svg.push(desc?shape+'>'+desc+'</'+tag+'>':shape+'/>');
|
|
3867
3966
|
// ONE caption per box: the NAME on the first box, CONT on every later
|
|
3868
3967
|
// one. The first box, not the widest — reading order is the order the
|
|
@@ -3923,6 +4022,29 @@ function renderBitfield(b,y0){
|
|
|
3923
4022
|
}
|
|
3924
4023
|
}
|
|
3925
4024
|
}
|
|
4025
|
+
// The shared-edge grid for the bitfield: one pass over every vertical bit
|
|
4026
|
+
// boundary and every horizontal row boundary, each emitted once.
|
|
4027
|
+
let maxRow=-1;
|
|
4028
|
+
BOWN.forEach(function(v,k){ const r=+k.split(':')[0]; if(r>maxRow) maxRow=r; });
|
|
4029
|
+
const yrow=(r)=>y+r*rh+shiftFor(r);
|
|
4030
|
+
const at=(r,c)=>(r<0||r>maxRow||c<0||c>=b.word)?null:(BOWN.get(bkey(r,c))||null);
|
|
4031
|
+
const BE=[];
|
|
4032
|
+
for(let c=0;c<=b.word;c++)
|
|
4033
|
+
BE.push.apply(BE, edgeRuns(1, c*cell, maxRow+1,
|
|
4034
|
+
i=>[yrow(i), yrow(i)+rh], i=>[at(i,c-1), at(i,c)]));
|
|
4035
|
+
// A horizontal boundary is a row's TOP, shared with the row above when the
|
|
4036
|
+
// two are vertically adjacent. A row also needs a bottom of its own when the
|
|
4037
|
+
// row below is not adjacent — an elision strip has been inserted between
|
|
4038
|
+
// them, and shiftFor() has pushed it down.
|
|
4039
|
+
const hb=[];
|
|
4040
|
+
for(let r=0;r<=maxRow;r++){
|
|
4041
|
+
hb.push({y:yrow(r), ar:(r>0 && yrow(r-1)+rh===yrow(r))?r-1:-1, br:r});
|
|
4042
|
+
if(r===maxRow || yrow(r+1)!==yrow(r)+rh) hb.push({y:yrow(r)+rh, ar:r, br:-1});
|
|
4043
|
+
}
|
|
4044
|
+
for(const hbe of hb)
|
|
4045
|
+
BE.push.apply(BE, edgeRuns(0, hbe.y, b.word,
|
|
4046
|
+
i=>[i*cell, (i+1)*cell], i=>[hbe.ar<0?null:at(hbe.ar,i), hbe.br<0?null:at(hbe.br,i)]));
|
|
4047
|
+
svg.push(edgeSvg(BE, BDEF));
|
|
3926
4048
|
for(const e of elis){
|
|
3927
4049
|
const st=(b.stroke||'#555');
|
|
3928
4050
|
// `ELISION-MARK-EXTENT`: THE SIDE DOTS ARE ALWAYS DRAWN. This reverses `ELISION-MARK-EXTENT`,
|
|
@@ -4018,6 +4140,14 @@ function renderTable(t,y0){
|
|
|
4018
4140
|
// cell marks: h1..hN address header tiers top-down, r>=1 the data rows
|
|
4019
4141
|
const markOf=(r,c)=>(t.marks||[]).find(mk=>(mk.hdr?mk.r-1:H+mk.r-1)===r&&mk.c===c+1);
|
|
4020
4142
|
const yTop=y+4;
|
|
4143
|
+
// Shared-edge grid (0.1.6): cell fills carry no stroke of their own. Every
|
|
4144
|
+
// grid cell records its owning (anchor) cell, and the grid is emitted once,
|
|
4145
|
+
// edge by edge, after the cells — see edgeSvg() above. Table cells have no
|
|
4146
|
+
// conditional-presence mark, so `d` is always false here; the dash branch
|
|
4147
|
+
// exists for the bitfield and is kept common so both genres draw alike.
|
|
4148
|
+
const NC=t.cols.length, DEF=t.stroke||'#c9c7bf';
|
|
4149
|
+
const OWN=grid.map(()=>new Array(NC).fill(null));
|
|
4150
|
+
const xAt=[0]; for(let i=0;i<NC;i++) xAt.push(xAt[i]+widths[i]);
|
|
4021
4151
|
for(let r=0;r<grid.length;r++){
|
|
4022
4152
|
for(let c=0;c<grid[r].length;c++){
|
|
4023
4153
|
const cell=grid[r][c];
|
|
@@ -4033,7 +4163,11 @@ function renderTable(t,y0){
|
|
|
4033
4163
|
// addressable cells carry table-id:row:col (row 0 = bottom header tier)
|
|
4034
4164
|
const addrR = r>=H ? (r-H+1) : (r===H-1 ? 0 : null);
|
|
4035
4165
|
const addr = addrR===null ? '' : ' data-cell="'+t.id+':'+addrR+':'+(c+1)+'" style="cursor:pointer"';
|
|
4036
|
-
|
|
4166
|
+
// A merged cell owns every grid square it spans, so its internal
|
|
4167
|
+
// boundaries have the same owner on both sides and are never drawn.
|
|
4168
|
+
const rec={id:'c'+r+'_'+c, c:(mk&&mk.stroke)||null, d:false};
|
|
4169
|
+
for(let rr=r;rr<r+rs;rr++) for(let cc=c;cc<c+cs;cc++) if(OWN[rr]) OWN[rr][cc]=rec;
|
|
4170
|
+
svg.push('<rect x="'+x+'" y="'+yy+'" width="'+wsum+'" height="'+h+'" fill="'+fill+'" stroke="none"'+addr+'/>');
|
|
4037
4171
|
// alignment: headers centered; data follows GFM colon alignment (default left)
|
|
4038
4172
|
const al=cell.hdr?'center':(alignOf(c)||'left');
|
|
4039
4173
|
const tx=al==='center'?x+wsum/2:(al==='right'?x+wsum-7:x+7);
|
|
@@ -4049,6 +4183,16 @@ function renderTable(t,y0){
|
|
|
4049
4183
|
}
|
|
4050
4184
|
}
|
|
4051
4185
|
}
|
|
4186
|
+
// Each edge drawn exactly once, owned by the (at most two) cells it divides.
|
|
4187
|
+
const EDG=[];
|
|
4188
|
+
const cellAt=(r,c)=>(r<0||r>=grid.length||c<0||c>=NC)?null:OWN[r][c];
|
|
4189
|
+
for(let c=0;c<=NC;c++)
|
|
4190
|
+
EDG.push.apply(EDG, edgeRuns(1, xAt[c], grid.length,
|
|
4191
|
+
i=>[yTop+yAt[i], yTop+yAt[i+1]], i=>[cellAt(i,c-1), cellAt(i,c)]));
|
|
4192
|
+
for(let r=0;r<=grid.length;r++)
|
|
4193
|
+
EDG.push.apply(EDG, edgeRuns(0, yTop+yAt[r], NC,
|
|
4194
|
+
i=>[xAt[i], xAt[i+1]], i=>[cellAt(r-1,i), cellAt(r,i)]));
|
|
4195
|
+
svg.push(edgeSvg(EDG, DEF));
|
|
4052
4196
|
const yEnd=yTop+yAt[grid.length];
|
|
4053
4197
|
return {svg:svg.join(''), y:yEnd+6, w:totalW+2};
|
|
4054
4198
|
}
|
package/dist/figdown.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// figdown.mjs — FigDown embeddable library (0.1.
|
|
1
|
+
// figdown.mjs — FigDown embeddable library (0.1.6)
|
|
2
2
|
// GENERATED FILE, DO NOT EDIT. Built from editor/figdown.html.
|
|
3
3
|
// Regenerate with: node tools/make-lib.js
|
|
4
4
|
'use strict';
|
|
5
|
-
var VERSION = "0.1.
|
|
5
|
+
var VERSION = "0.1.6";
|
|
6
6
|
|
|
7
7
|
// ---- engine (extracted verbatim from editor/figdown.html) ----
|
|
8
8
|
var __engine = (function () {
|
|
@@ -16,7 +16,7 @@ const SHAPES = ['box','rounded','circle','ellipse','diamond','cylinder'];
|
|
|
16
16
|
// input to that promise, and under core §13 a 0.x renderer may differ from
|
|
17
17
|
// the next — which makes the recorded version the only thing that can
|
|
18
18
|
// explain a diff between two renderings of one source.
|
|
19
|
-
const FIGDOWN_VERSION = '0.1.
|
|
19
|
+
const FIGDOWN_VERSION = '0.1.6';
|
|
20
20
|
// Retired shape VALUES keep a named diagnostic (PROCESS §5(d)), the same way
|
|
21
21
|
// retired option keys do: `cloud` was the one value that named a domain
|
|
22
22
|
// (the internet cloud) in an enum the language keeps purely geometric
|
|
@@ -3577,6 +3577,89 @@ function routeAround(p,q,obs){
|
|
|
3577
3577
|
return pts.length>2?pts:null;
|
|
3578
3578
|
}
|
|
3579
3579
|
|
|
3580
|
+
// ---- shared-edge grid (0.1.6) ----
|
|
3581
|
+
// A boundary between two cells belongs to BOTH of them. Drawing each cell as a
|
|
3582
|
+
// stroked rect drew that boundary twice, and the second painting won: a marked
|
|
3583
|
+
// cell's colour, or a conditional field's dash, was whatever the neighbour
|
|
3584
|
+
// happened to paint last. So the grid is now emitted edge-by-edge, each edge
|
|
3585
|
+
// exactly once, and its appearance is decided by its (at most two) owners.
|
|
3586
|
+
//
|
|
3587
|
+
// WHAT THE OLD DRAWING DESTROYED. A `present=` field's shared edge was
|
|
3588
|
+
// overwritten solid by a plain neighbour. `STYLE-KEY-SCOPE` rules that the dash IS
|
|
3589
|
+
// conditional presence and that nothing else can set or clear it — so the
|
|
3590
|
+
// model's flag was being silently erased at render time. This is a
|
|
3591
|
+
// correctness fix, not a cosmetic one.
|
|
3592
|
+
//
|
|
3593
|
+
// WHY NOT HALF-WIDTH. The obvious alternative gives each owner half the
|
|
3594
|
+
// boundary: two 0.5px bands, one per side. Point-sampled at 1× — the size a
|
|
3595
|
+
// figure is actually read at — a shared edge between an orange mark and a
|
|
3596
|
+
// green one reads #2E6E34: one muddy pixel with the orange unrecoverable,
|
|
3597
|
+
// because half-width strokes sit at quarter-pixel offsets and the rasteriser
|
|
3598
|
+
// averages them. The bands here land on integer pixels and every colour
|
|
3599
|
+
// survives exactly. A zoomed crop hides this entirely, which is why the
|
|
3600
|
+
// decision was made at 1× and must be re-checked at 1× if it is revisited.
|
|
3601
|
+
//
|
|
3602
|
+
// THE RESIDUAL AMBIGUITY, stated rather than hidden. A dashed edge between a
|
|
3603
|
+
// conditional field and a plain one can be read locally as if the plain one
|
|
3604
|
+
// were conditional too. Today's drawing has the same ambiguity whenever the
|
|
3605
|
+
// dash happens to win the overwrite; this is strictly better, not solved.
|
|
3606
|
+
//
|
|
3607
|
+
// WHAT IT COSTS. Two adjacent marked cells put 3px of ink at their shared
|
|
3608
|
+
// boundary (ring + line + ring) against 1px elsewhere: the grid stays even,
|
|
3609
|
+
// but that region reads heavier. Four differently marked cells meeting at a
|
|
3610
|
+
// point give four ring corners around one crossing — busy at 8×, invisible
|
|
3611
|
+
// at 1×.
|
|
3612
|
+
//
|
|
3613
|
+
// `edges` are runs produced by edgeRuns(); `def` is the block's grid colour.
|
|
3614
|
+
function edgeSvg(edges, def){
|
|
3615
|
+
const W=1, out=[];
|
|
3616
|
+
const L=(v,p,s,e,col,dash,w)=>'<line x1="'+(v?p:s)+'" y1="'+(v?s:p)+'" x2="'+(v?p:e)+'" y2="'+(v?e:p)
|
|
3617
|
+
+'" stroke="'+col+'"'+(w!==1?' stroke-width="'+w+'"':'')+(dash?' stroke-dasharray="5 3"':'')+'/>';
|
|
3618
|
+
for(const g of edges){
|
|
3619
|
+
const A=g.a, B=g.b;
|
|
3620
|
+
// The shared line: default colour, full weight, dashed iff EITHER owner
|
|
3621
|
+
// carries `present=`. A field with both `stroke=` and `present=` therefore
|
|
3622
|
+
// gets a dashed boundary AND a separate coloured ring, not a coloured
|
|
3623
|
+
// dash — the two marks answer different questions and must not merge.
|
|
3624
|
+
out.push(L(g.v, g.p, g.s, g.e, def, (A&&A.d)||(B&&B.d), W));
|
|
3625
|
+
// The class marks: solid, full weight, inset INSIDE their own cell. An end
|
|
3626
|
+
// pulls in by W/2 where the owner stops and the ring turns the corner;
|
|
3627
|
+
// it is left long where the owner continues past that end and the next run
|
|
3628
|
+
// carries on, so a run that ends only because the NEIGHBOUR changed joins
|
|
3629
|
+
// with no seam and no ring protrudes past its own cell. The test is the
|
|
3630
|
+
// ownership map (does this cell own the next unit too?), not the geometry.
|
|
3631
|
+
if(A&&A.c) out.push(L(g.v, g.p-W, g.s+(g.ca0?0:W/2), g.e-(g.ca1?0:W/2), A.c, false, W));
|
|
3632
|
+
if(B&&B.c) out.push(L(g.v, g.p+W, g.s+(g.cb0?0:W/2), g.e-(g.cb1?0:W/2), B.c, false, W));
|
|
3633
|
+
}
|
|
3634
|
+
return out.join('');
|
|
3635
|
+
}
|
|
3636
|
+
// Walk one grid line unit by unit and merge collinear units that have the same
|
|
3637
|
+
// pair of owners into a single run. `v` is 1 for a vertical line, `p` its fixed
|
|
3638
|
+
// coordinate; `n` units; `span(i)` -> [start,end]; `ownAt(i)` -> [before,after]
|
|
3639
|
+
// where an owner is {id, c:colour|null, d:dashed}. A unit with no owner, or
|
|
3640
|
+
// with the SAME owner on both sides (a merged/spanning cell's interior), draws
|
|
3641
|
+
// nothing at all — which is where the doubled interior lines went.
|
|
3642
|
+
function edgeRuns(v, p, n, span, ownAt){
|
|
3643
|
+
const out=[]; let run=null;
|
|
3644
|
+
const sid=(o)=>o?o.id:null;
|
|
3645
|
+
const cont=(i,k,o)=>!!o && sid((ownAt(i)||[])[k])===o.id;
|
|
3646
|
+
for(let i=0;i<n;i++){
|
|
3647
|
+
const o=ownAt(i)||[], a=o[0]||null, b=o[1]||null;
|
|
3648
|
+
const skip=(!a&&!b)||(a&&b&&a.id===b.id);
|
|
3649
|
+
const key=skip?null:JSON.stringify([a&&[a.c,a.d], b&&[b.c,b.d]]);
|
|
3650
|
+
const sp=span(i);
|
|
3651
|
+
if(run && run.key===key && run.e===sp[0]){ run.e=sp[1]; run.i1=i; continue; }
|
|
3652
|
+
if(run){ out.push(run); run=null; }
|
|
3653
|
+
if(!skip) run={v:v,p:p,s:sp[0],e:sp[1],key:key,a:a,b:b,i0:i,i1:i};
|
|
3654
|
+
}
|
|
3655
|
+
if(run) out.push(run);
|
|
3656
|
+
for(const r of out){
|
|
3657
|
+
r.ca0=cont(r.i0-1,0,r.a); r.ca1=cont(r.i1+1,0,r.a);
|
|
3658
|
+
r.cb0=cont(r.i0-1,1,r.b); r.cb1=cont(r.i1+1,1,r.b);
|
|
3659
|
+
}
|
|
3660
|
+
return out;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3580
3663
|
// ---- bitfield ----
|
|
3581
3664
|
function renderBitfield(b,y0){
|
|
3582
3665
|
const cell=Math.max(18,Math.min(28,Math.floor(760/b.word))), rh=30, ruler=16;
|
|
@@ -3769,9 +3852,11 @@ function renderBitfield(b,y0){
|
|
|
3769
3852
|
// one edge a per-row rect cannot leave out: a rect strokes four sides or
|
|
3770
3853
|
// none, and "none" would also lose the two verticals that must continue.
|
|
3771
3854
|
// Fill stays a single closed region, so `fill=` paints the field once with
|
|
3772
|
-
// no seam
|
|
3773
|
-
//
|
|
3774
|
-
//
|
|
3855
|
+
// no seam. Since 0.1.6 the path is unstroked and the boundary comes from the
|
|
3856
|
+
// shared-edge grid instead; the internal boundary is still never drawn,
|
|
3857
|
+
// because both of its sides are owned by the same field and edgeRuns() skips
|
|
3858
|
+
// a unit whose two owners are one (`STYLE-KEY-SCOPE`: the dash IS conditional presence,
|
|
3859
|
+
// and nothing else may set or clear it).
|
|
3775
3860
|
const boxOutline=(bx)=>{
|
|
3776
3861
|
const bands=[];
|
|
3777
3862
|
for(const s of bx){
|
|
@@ -3793,6 +3878,11 @@ function renderBitfield(b,y0){
|
|
|
3793
3878
|
}
|
|
3794
3879
|
return 'M'+pts.map(p=>p[0]+' '+p[1]).join(' L')+' Z';
|
|
3795
3880
|
};
|
|
3881
|
+
// Shared-edge grid (0.1.6): field fills carry no stroke of their own. Every
|
|
3882
|
+
// bit column of every row records which field owns it, and the grid is
|
|
3883
|
+
// emitted once, edge by edge, after the fields — see edgeSvg() above.
|
|
3884
|
+
const BOWN=new Map(), BDEF=b.stroke||'#555';
|
|
3885
|
+
const bkey=(rw,cl)=>rw+':'+cl; let BOXN=0;
|
|
3796
3886
|
let pos=0; // bit cursor
|
|
3797
3887
|
for(const f of b.fields){
|
|
3798
3888
|
if(f.wrap){ pos=Math.ceil((pos||1)/b.word)*b.word; continue; }
|
|
@@ -3818,9 +3908,11 @@ function renderBitfield(b,y0){
|
|
|
3818
3908
|
// `PRESENCE-CONDITION-EXPRESSION`: the carrier is now `present=`, and BOTH of its
|
|
3819
3909
|
// written forms dash — `present=""` claims conditional presence just as
|
|
3820
3910
|
// `present="C = 1"` does; only the caption below distinguishes them.
|
|
3821
|
-
|
|
3911
|
+
// 0.1.6: the dash and the class colour no longer ride on the box's own
|
|
3912
|
+
// stroke — they are properties of the shared boundary and of the ring
|
|
3913
|
+
// inside the cell respectively, recorded per bit below and drawn once.
|
|
3822
3914
|
const cfill=f.fill||b.fill||'#fff';
|
|
3823
|
-
const paint=' fill="'+cfill+'" stroke="
|
|
3915
|
+
const paint=' fill="'+cfill+'" stroke="none"';
|
|
3824
3916
|
// Draw one occurrence. `suffix` is the derived index label — '' for a
|
|
3825
3917
|
// field that is not one element of a run, ' [0]' / ' [n]' for the two
|
|
3826
3918
|
// occurrences `REPEATED-RUN-DRAWING` draws. It is APPENDED to the author's label, and the
|
|
@@ -3855,6 +3947,13 @@ function renderBitfield(b,y0){
|
|
|
3855
3947
|
? '<rect x="'+bx[0].x+'" y="'+bx[0].y+'" width="'+bx[0].w+'" height="'+(bx.length*rh)+'"'+paint
|
|
3856
3948
|
: '<path d="'+boxOutline(bx)+'"'+paint;
|
|
3857
3949
|
const tag=flat?'rect':'path';
|
|
3950
|
+
// Record ownership per BIT, one entry per (row, column) this occurrence
|
|
3951
|
+
// covers. Every occurrence of a repeated field is its own owner id, so
|
|
3952
|
+
// the boundary between two occurrences is a real boundary and is drawn.
|
|
3953
|
+
const bid=b.id+'_'+(BOXN++);
|
|
3954
|
+
const brec={id:bid, c:f.stroke||null, d:f.present!==undefined};
|
|
3955
|
+
bx.forEach(function(s){ const c0=Math.round(s.x/cell), n=Math.round(s.w/cell);
|
|
3956
|
+
for(let k=0;k<n;k++) BOWN.set(bkey(s.row,c0+k), brec); });
|
|
3858
3957
|
svg.push(desc?shape+'>'+desc+'</'+tag+'>':shape+'/>');
|
|
3859
3958
|
// ONE caption per box: the NAME on the first box, CONT on every later
|
|
3860
3959
|
// one. The first box, not the widest — reading order is the order the
|
|
@@ -3915,6 +4014,29 @@ function renderBitfield(b,y0){
|
|
|
3915
4014
|
}
|
|
3916
4015
|
}
|
|
3917
4016
|
}
|
|
4017
|
+
// The shared-edge grid for the bitfield: one pass over every vertical bit
|
|
4018
|
+
// boundary and every horizontal row boundary, each emitted once.
|
|
4019
|
+
let maxRow=-1;
|
|
4020
|
+
BOWN.forEach(function(v,k){ const r=+k.split(':')[0]; if(r>maxRow) maxRow=r; });
|
|
4021
|
+
const yrow=(r)=>y+r*rh+shiftFor(r);
|
|
4022
|
+
const at=(r,c)=>(r<0||r>maxRow||c<0||c>=b.word)?null:(BOWN.get(bkey(r,c))||null);
|
|
4023
|
+
const BE=[];
|
|
4024
|
+
for(let c=0;c<=b.word;c++)
|
|
4025
|
+
BE.push.apply(BE, edgeRuns(1, c*cell, maxRow+1,
|
|
4026
|
+
i=>[yrow(i), yrow(i)+rh], i=>[at(i,c-1), at(i,c)]));
|
|
4027
|
+
// A horizontal boundary is a row's TOP, shared with the row above when the
|
|
4028
|
+
// two are vertically adjacent. A row also needs a bottom of its own when the
|
|
4029
|
+
// row below is not adjacent — an elision strip has been inserted between
|
|
4030
|
+
// them, and shiftFor() has pushed it down.
|
|
4031
|
+
const hb=[];
|
|
4032
|
+
for(let r=0;r<=maxRow;r++){
|
|
4033
|
+
hb.push({y:yrow(r), ar:(r>0 && yrow(r-1)+rh===yrow(r))?r-1:-1, br:r});
|
|
4034
|
+
if(r===maxRow || yrow(r+1)!==yrow(r)+rh) hb.push({y:yrow(r)+rh, ar:r, br:-1});
|
|
4035
|
+
}
|
|
4036
|
+
for(const hbe of hb)
|
|
4037
|
+
BE.push.apply(BE, edgeRuns(0, hbe.y, b.word,
|
|
4038
|
+
i=>[i*cell, (i+1)*cell], i=>[hbe.ar<0?null:at(hbe.ar,i), hbe.br<0?null:at(hbe.br,i)]));
|
|
4039
|
+
svg.push(edgeSvg(BE, BDEF));
|
|
3918
4040
|
for(const e of elis){
|
|
3919
4041
|
const st=(b.stroke||'#555');
|
|
3920
4042
|
// `ELISION-MARK-EXTENT`: THE SIDE DOTS ARE ALWAYS DRAWN. This reverses `ELISION-MARK-EXTENT`,
|
|
@@ -4010,6 +4132,14 @@ function renderTable(t,y0){
|
|
|
4010
4132
|
// cell marks: h1..hN address header tiers top-down, r>=1 the data rows
|
|
4011
4133
|
const markOf=(r,c)=>(t.marks||[]).find(mk=>(mk.hdr?mk.r-1:H+mk.r-1)===r&&mk.c===c+1);
|
|
4012
4134
|
const yTop=y+4;
|
|
4135
|
+
// Shared-edge grid (0.1.6): cell fills carry no stroke of their own. Every
|
|
4136
|
+
// grid cell records its owning (anchor) cell, and the grid is emitted once,
|
|
4137
|
+
// edge by edge, after the cells — see edgeSvg() above. Table cells have no
|
|
4138
|
+
// conditional-presence mark, so `d` is always false here; the dash branch
|
|
4139
|
+
// exists for the bitfield and is kept common so both genres draw alike.
|
|
4140
|
+
const NC=t.cols.length, DEF=t.stroke||'#c9c7bf';
|
|
4141
|
+
const OWN=grid.map(()=>new Array(NC).fill(null));
|
|
4142
|
+
const xAt=[0]; for(let i=0;i<NC;i++) xAt.push(xAt[i]+widths[i]);
|
|
4013
4143
|
for(let r=0;r<grid.length;r++){
|
|
4014
4144
|
for(let c=0;c<grid[r].length;c++){
|
|
4015
4145
|
const cell=grid[r][c];
|
|
@@ -4025,7 +4155,11 @@ function renderTable(t,y0){
|
|
|
4025
4155
|
// addressable cells carry table-id:row:col (row 0 = bottom header tier)
|
|
4026
4156
|
const addrR = r>=H ? (r-H+1) : (r===H-1 ? 0 : null);
|
|
4027
4157
|
const addr = addrR===null ? '' : ' data-cell="'+t.id+':'+addrR+':'+(c+1)+'" style="cursor:pointer"';
|
|
4028
|
-
|
|
4158
|
+
// A merged cell owns every grid square it spans, so its internal
|
|
4159
|
+
// boundaries have the same owner on both sides and are never drawn.
|
|
4160
|
+
const rec={id:'c'+r+'_'+c, c:(mk&&mk.stroke)||null, d:false};
|
|
4161
|
+
for(let rr=r;rr<r+rs;rr++) for(let cc=c;cc<c+cs;cc++) if(OWN[rr]) OWN[rr][cc]=rec;
|
|
4162
|
+
svg.push('<rect x="'+x+'" y="'+yy+'" width="'+wsum+'" height="'+h+'" fill="'+fill+'" stroke="none"'+addr+'/>');
|
|
4029
4163
|
// alignment: headers centered; data follows GFM colon alignment (default left)
|
|
4030
4164
|
const al=cell.hdr?'center':(alignOf(c)||'left');
|
|
4031
4165
|
const tx=al==='center'?x+wsum/2:(al==='right'?x+wsum-7:x+7);
|
|
@@ -4041,6 +4175,16 @@ function renderTable(t,y0){
|
|
|
4041
4175
|
}
|
|
4042
4176
|
}
|
|
4043
4177
|
}
|
|
4178
|
+
// Each edge drawn exactly once, owned by the (at most two) cells it divides.
|
|
4179
|
+
const EDG=[];
|
|
4180
|
+
const cellAt=(r,c)=>(r<0||r>=grid.length||c<0||c>=NC)?null:OWN[r][c];
|
|
4181
|
+
for(let c=0;c<=NC;c++)
|
|
4182
|
+
EDG.push.apply(EDG, edgeRuns(1, xAt[c], grid.length,
|
|
4183
|
+
i=>[yTop+yAt[i], yTop+yAt[i+1]], i=>[cellAt(i,c-1), cellAt(i,c)]));
|
|
4184
|
+
for(let r=0;r<=grid.length;r++)
|
|
4185
|
+
EDG.push.apply(EDG, edgeRuns(0, yTop+yAt[r], NC,
|
|
4186
|
+
i=>[xAt[i], xAt[i+1]], i=>[cellAt(r-1,i), cellAt(r,i)]));
|
|
4187
|
+
svg.push(edgeSvg(EDG, DEF));
|
|
4044
4188
|
const yEnd=yTop+yAt[grid.length];
|
|
4045
4189
|
return {svg:svg.join(''), y:yEnd+6, w:totalW+2};
|
|
4046
4190
|
}
|
package/examples/evpn-fabric.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 527 634" width="527" height="634" font-family="system-ui,sans-serif"><g transform="translate(0,0)"><defs><marker id="s0_arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker><pattern id="s0_hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#bbb" stroke-width="2"/></pattern></defs><g transform="translate(18,6)"><line data-edge="24" x1="157.16" y1="56" x2="96.44000000000003" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="25" x1="197.12" y1="56" x2="234.08000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="26" x1="221.2" y1="50.810457516339866" x2="371.72" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="27" x1="277.20000000000005" y1="50.810457516339866" x2="126.68000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="28" x1="301.28000000000003" y1="56" x2="264.32000000000005" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="29" x1="341.24000000000007" y1="56" x2="401.96000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="30" x1="69.98000000000002" y1="136" x2="66.02000000000001" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="31" x1="249.20000000000005" y1="136" x2="249.20000000000002" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="32" x1="428.4200000000001" y1="136" x2="432.38" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="36" x1="132.40000000000003" y1="118" x2="188.40000000000003" y2="118" stroke="#dc2626" stroke-width="1.6" stroke-dasharray="6 4"/><g data-node="sp1" data-x="142.8" data-y="20" style="cursor:move"><rect x="142.8" y="20" width="78.4" height="36" rx="14" fill="#e0e7ff" stroke="#8a8880" stroke-width="1.8"/><text x="182" y="42.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Spine-1</text></g><g data-node="sp2" data-x="277.20000000000005" data-y="20" style="cursor:move"><rect x="277.20000000000005" y="20" width="78.4" height="36" rx="14" fill="#e0e7ff" stroke="#8a8880" stroke-width="1.8"/><text x="316.40000000000003" y="42.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Spine-2</text></g><g data-node="lf1" data-x="10.800000000000011" data-y="100" style="cursor:move"><rect x="10.800000000000011" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="71.60000000000002" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-1 (VTEP)</text></g><g data-node="lf2" data-x="188.40000000000003" data-y="100" style="cursor:move"><rect x="188.40000000000003" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="249.20000000000005" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-2 (VTEP)</text></g><g data-node="lf3" data-x="366.00000000000006" data-y="100" style="cursor:move"><rect x="366.00000000000006" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="426.80000000000007" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-3 (VTEP)</text></g><g data-node="h1" data-x="0" data-y="180" style="cursor:move"><rect x="0" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="64.4" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-A (VLAN 10)</text></g><g data-node="h2" data-x="184.8" data-y="180" style="cursor:move"><rect x="184.8" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="249.20000000000002" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-B (VLAN 10)</text></g><g data-node="h3" data-x="369.6" data-y="180" style="cursor:move"><rect x="369.6" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="434" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-C (VLAN 20)</text></g><text x="126.80000000000001" y="72.25" font-size="11" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round">eBGP</text><text x="126.80000000000001" y="72.25" font-size="11" text-anchor="middle" fill="#555">eBGP</text><text x="160.40000000000003" y="112.25" font-size="11" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round">VXLAN VNI 10010</text><text x="160.40000000000003" y="112.25" font-size="11" text-anchor="middle" fill="#dc2626">VXLAN VNI 10010</text><path d="M132.40000000000003 118 L142.48000000000005 112.4 L142.48000000000005 123.6 z" fill="#dc2626" stroke="none"/><path d="M188.40000000000003 118 L178.32000000000002 123.6 L178.32000000000002 112.4 z" fill="#dc2626" stroke="none"/><rect x="0" y="237" width="16" height="11" fill="#fff" stroke="#555"/><text x="21" y="246.5" font-size="11" fill="#1d1d1b">Physical underlay link — eBGP/L3 transport between leaf and spine</text><rect x="0" y="257" width="16" height="11" fill="#fff" stroke="#dc2626" stroke-dasharray="6 4"/><text x="21" y="266.5" font-size="11" fill="#1d1d1b">Logical VXLAN overlay tunnel — rides on the physical underlay</text></g></g><g transform="translate(0,312)"><defs><marker id="s1_arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker><pattern id="s1_hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#bbb" stroke-width="2"/></pattern></defs><g transform="translate(18,6)"><text x="0" y="14" font-size="13" font-weight="600">VNI mapping</text><rect x="0" y="22" width="71.2" height="26" fill="#eeede6" stroke="#c9c7bf" data-cell="vni:0:1" style="cursor:pointer"/><text x="35.6" y="39.3" font-size="12" text-anchor="middle" font-weight="600">Leaf</text><rect x="71.2" y="22" width="56.8" height="26" fill="#eeede6" stroke="#c9c7bf" data-cell="vni:0:2" style="cursor:pointer"/><text x="99.6" y="39.3" font-size="12" text-anchor="middle" font-weight="600">VLAN</text><rect x="128" y="22" width="64" height="26" fill="#eeede6" stroke="#c9c7bf" data-cell="vni:0:3" style="cursor:pointer"/><text x="160" y="39.3" font-size="12" text-anchor="middle" font-weight="600">VNI</text><rect x="192" y="22" width="56.8" height="26" fill="#eeede6" stroke="#c9c7bf" data-cell="vni:0:4" style="cursor:pointer"/><text x="220.4" y="39.3" font-size="12" text-anchor="middle" font-weight="600">Type</text><rect x="0" y="48" width="71.2" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:1:1" style="cursor:pointer"/><text x="7" y="65.3" font-size="12" text-anchor="start">Leaf-1</text><rect x="71.2" y="48" width="56.8" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:1:2" style="cursor:pointer"/><text x="99.6" y="65.3" font-size="12" text-anchor="middle">10</text><rect x="128" y="48" width="64" height="26" fill="#fee2e2" stroke="#c9c7bf" data-cell="vni:1:3" style="cursor:pointer"/><text x="135" y="65.3" font-size="12" text-anchor="start">10010</text><rect x="192" y="48" width="56.8" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:1:4" style="cursor:pointer"/><text x="199" y="65.3" font-size="12" text-anchor="start">L2</text><rect x="0" y="74" width="71.2" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:2:1" style="cursor:pointer"/><text x="7" y="91.3" font-size="12" text-anchor="start">Leaf-2</text><rect x="71.2" y="74" width="56.8" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:2:2" style="cursor:pointer"/><text x="99.6" y="91.3" font-size="12" text-anchor="middle">10</text><rect x="128" y="74" width="64" height="26" fill="#fee2e2" stroke="#c9c7bf" data-cell="vni:2:3" style="cursor:pointer"/><text x="135" y="91.3" font-size="12" text-anchor="start">10010</text><rect x="192" y="74" width="56.8" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:2:4" style="cursor:pointer"/><text x="199" y="91.3" font-size="12" text-anchor="start">L2</text><rect x="0" y="100" width="71.2" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:3:1" style="cursor:pointer"/><text x="7" y="117.3" font-size="12" text-anchor="start">Leaf-3</text><rect x="71.2" y="100" width="56.8" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:3:2" style="cursor:pointer"/><text x="99.6" y="117.3" font-size="12" text-anchor="middle">20</text><rect x="128" y="100" width="64" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:3:3" style="cursor:pointer"/><text x="135" y="117.3" font-size="12" text-anchor="start">10020</text><rect x="192" y="100" width="56.8" height="26" fill="#fff" stroke="#c9c7bf" data-cell="vni:3:4" style="cursor:pointer"/><text x="199" y="117.3" font-size="12" text-anchor="start">L2</text><text x="0" y="170" font-size="13" font-weight="600">Fabric planes</text><rect x="0" y="178" width="136" height="26" fill="#eeede6" stroke="#c9c7bf" data-cell="planes:0:1" style="cursor:pointer"/><text x="68" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Plane</text><rect x="136" y="178" width="143.2" height="26" fill="#eeede6" stroke="#c9c7bf" data-cell="planes:0:2" style="cursor:pointer"/><text x="207.6" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Protocol</text><rect x="279.2" y="178" width="208" height="26" fill="#eeede6" stroke="#c9c7bf" data-cell="planes:0:3" style="cursor:pointer"/><text x="383.2" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Role</text><rect x="0" y="204" width="136" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:1:1" style="cursor:pointer"/><text x="7" y="221.3" font-size="12" text-anchor="start">Underlay</text><rect x="136" y="204" width="143.2" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:1:2" style="cursor:pointer"/><text x="143" y="221.3" font-size="12" text-anchor="start">eBGP</text><rect x="279.2" y="204" width="208" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:1:3" style="cursor:pointer"/><text x="286.2" y="221.3" font-size="12" text-anchor="start">loopback reachability</text><rect x="0" y="230" width="136" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:2:1" style="cursor:pointer"/><text x="7" y="247.3" font-size="12" text-anchor="start">Overlay control</text><rect x="136" y="230" width="143.2" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:2:2" style="cursor:pointer"/><text x="143" y="247.3" font-size="12" text-anchor="start">MP-BGP EVPN</text><rect x="279.2" y="230" width="208" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:2:3" style="cursor:pointer"/><text x="286.2" y="247.3" font-size="12" text-anchor="start">MAC/IP route distribution</text><rect x="0" y="256" width="136" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:3:1" style="cursor:pointer"/><text x="7" y="273.3" font-size="12" text-anchor="start">Overlay data</text><rect x="136" y="256" width="143.2" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:3:2" style="cursor:pointer"/><text x="143" y="273.3" font-size="12" text-anchor="start">VXLAN (UDP 4789)</text><rect x="279.2" y="256" width="208" height="26" fill="#fff" stroke="#c9c7bf" data-cell="planes:3:3" style="cursor:pointer"/><text x="286.2" y="273.3" font-size="12" text-anchor="start">L2-in-L3 encapsulation</text></g></g><metadata id="figdown-source" data-sha256="bc3fb7fb66c7628738359b5135f8da14572e5fb24c4f6efb18a6b8365ab602ea" data-engine-version="0.1.5"><![CDATA[
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 527 634" width="527" height="634" font-family="system-ui,sans-serif"><g transform="translate(0,0)"><defs><marker id="s0_arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker><pattern id="s0_hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#bbb" stroke-width="2"/></pattern></defs><g transform="translate(18,6)"><line data-edge="24" x1="157.16" y1="56" x2="96.44000000000003" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="25" x1="197.12" y1="56" x2="234.08000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="26" x1="221.2" y1="50.810457516339866" x2="371.72" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="27" x1="277.20000000000005" y1="50.810457516339866" x2="126.68000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="28" x1="301.28000000000003" y1="56" x2="264.32000000000005" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="29" x1="341.24000000000007" y1="56" x2="401.96000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="30" x1="69.98000000000002" y1="136" x2="66.02000000000001" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="31" x1="249.20000000000005" y1="136" x2="249.20000000000002" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="32" x1="428.4200000000001" y1="136" x2="432.38" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="36" x1="132.40000000000003" y1="118" x2="188.40000000000003" y2="118" stroke="#dc2626" stroke-width="1.6" stroke-dasharray="6 4"/><g data-node="sp1" data-x="142.8" data-y="20" style="cursor:move"><rect x="142.8" y="20" width="78.4" height="36" rx="14" fill="#e0e7ff" stroke="#8a8880" stroke-width="1.8"/><text x="182" y="42.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Spine-1</text></g><g data-node="sp2" data-x="277.20000000000005" data-y="20" style="cursor:move"><rect x="277.20000000000005" y="20" width="78.4" height="36" rx="14" fill="#e0e7ff" stroke="#8a8880" stroke-width="1.8"/><text x="316.40000000000003" y="42.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Spine-2</text></g><g data-node="lf1" data-x="10.800000000000011" data-y="100" style="cursor:move"><rect x="10.800000000000011" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="71.60000000000002" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-1 (VTEP)</text></g><g data-node="lf2" data-x="188.40000000000003" data-y="100" style="cursor:move"><rect x="188.40000000000003" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="249.20000000000005" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-2 (VTEP)</text></g><g data-node="lf3" data-x="366.00000000000006" data-y="100" style="cursor:move"><rect x="366.00000000000006" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="426.80000000000007" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-3 (VTEP)</text></g><g data-node="h1" data-x="0" data-y="180" style="cursor:move"><rect x="0" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="64.4" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-A (VLAN 10)</text></g><g data-node="h2" data-x="184.8" data-y="180" style="cursor:move"><rect x="184.8" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="249.20000000000002" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-B (VLAN 10)</text></g><g data-node="h3" data-x="369.6" data-y="180" style="cursor:move"><rect x="369.6" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="434" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-C (VLAN 20)</text></g><text x="126.80000000000001" y="72.25" font-size="11" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round">eBGP</text><text x="126.80000000000001" y="72.25" font-size="11" text-anchor="middle" fill="#555">eBGP</text><text x="160.40000000000003" y="112.25" font-size="11" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round">VXLAN VNI 10010</text><text x="160.40000000000003" y="112.25" font-size="11" text-anchor="middle" fill="#dc2626">VXLAN VNI 10010</text><path d="M132.40000000000003 118 L142.48000000000005 112.4 L142.48000000000005 123.6 z" fill="#dc2626" stroke="none"/><path d="M188.40000000000003 118 L178.32000000000002 123.6 L178.32000000000002 112.4 z" fill="#dc2626" stroke="none"/><rect x="0" y="237" width="16" height="11" fill="#fff" stroke="#555"/><text x="21" y="246.5" font-size="11" fill="#1d1d1b">Physical underlay link — eBGP/L3 transport between leaf and spine</text><rect x="0" y="257" width="16" height="11" fill="#fff" stroke="#dc2626" stroke-dasharray="6 4"/><text x="21" y="266.5" font-size="11" fill="#1d1d1b">Logical VXLAN overlay tunnel — rides on the physical underlay</text></g></g><g transform="translate(0,312)"><defs><marker id="s1_arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker><pattern id="s1_hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#bbb" stroke-width="2"/></pattern></defs><g transform="translate(18,6)"><text x="0" y="14" font-size="13" font-weight="600">VNI mapping</text><rect x="0" y="22" width="71.2" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:1" style="cursor:pointer"/><text x="35.6" y="39.3" font-size="12" text-anchor="middle" font-weight="600">Leaf</text><rect x="71.2" y="22" width="56.8" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:2" style="cursor:pointer"/><text x="99.6" y="39.3" font-size="12" text-anchor="middle" font-weight="600">VLAN</text><rect x="128" y="22" width="64" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:3" style="cursor:pointer"/><text x="160" y="39.3" font-size="12" text-anchor="middle" font-weight="600">VNI</text><rect x="192" y="22" width="56.8" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:4" style="cursor:pointer"/><text x="220.4" y="39.3" font-size="12" text-anchor="middle" font-weight="600">Type</text><rect x="0" y="48" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:1:1" style="cursor:pointer"/><text x="7" y="65.3" font-size="12" text-anchor="start">Leaf-1</text><rect x="71.2" y="48" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:1:2" style="cursor:pointer"/><text x="99.6" y="65.3" font-size="12" text-anchor="middle">10</text><rect x="128" y="48" width="64" height="26" fill="#fee2e2" stroke="none" data-cell="vni:1:3" style="cursor:pointer"/><text x="135" y="65.3" font-size="12" text-anchor="start">10010</text><rect x="192" y="48" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:1:4" style="cursor:pointer"/><text x="199" y="65.3" font-size="12" text-anchor="start">L2</text><rect x="0" y="74" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:2:1" style="cursor:pointer"/><text x="7" y="91.3" font-size="12" text-anchor="start">Leaf-2</text><rect x="71.2" y="74" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:2:2" style="cursor:pointer"/><text x="99.6" y="91.3" font-size="12" text-anchor="middle">10</text><rect x="128" y="74" width="64" height="26" fill="#fee2e2" stroke="none" data-cell="vni:2:3" style="cursor:pointer"/><text x="135" y="91.3" font-size="12" text-anchor="start">10010</text><rect x="192" y="74" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:2:4" style="cursor:pointer"/><text x="199" y="91.3" font-size="12" text-anchor="start">L2</text><rect x="0" y="100" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:3:1" style="cursor:pointer"/><text x="7" y="117.3" font-size="12" text-anchor="start">Leaf-3</text><rect x="71.2" y="100" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:3:2" style="cursor:pointer"/><text x="99.6" y="117.3" font-size="12" text-anchor="middle">20</text><rect x="128" y="100" width="64" height="26" fill="#fff" stroke="none" data-cell="vni:3:3" style="cursor:pointer"/><text x="135" y="117.3" font-size="12" text-anchor="start">10020</text><rect x="192" y="100" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:3:4" style="cursor:pointer"/><text x="199" y="117.3" font-size="12" text-anchor="start">L2</text><line x1="0" y1="22" x2="0" y2="126" stroke="#c9c7bf"/><line x1="71.2" y1="22" x2="71.2" y2="126" stroke="#c9c7bf"/><line x1="128" y1="22" x2="128" y2="126" stroke="#c9c7bf"/><line x1="192" y1="22" x2="192" y2="126" stroke="#c9c7bf"/><line x1="248.8" y1="22" x2="248.8" y2="126" stroke="#c9c7bf"/><line x1="0" y1="22" x2="248.8" y2="22" stroke="#c9c7bf"/><line x1="0" y1="48" x2="248.8" y2="48" stroke="#c9c7bf"/><line x1="0" y1="74" x2="248.8" y2="74" stroke="#c9c7bf"/><line x1="0" y1="100" x2="248.8" y2="100" stroke="#c9c7bf"/><line x1="0" y1="126" x2="248.8" y2="126" stroke="#c9c7bf"/><text x="0" y="170" font-size="13" font-weight="600">Fabric planes</text><rect x="0" y="178" width="136" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:1" style="cursor:pointer"/><text x="68" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Plane</text><rect x="136" y="178" width="143.2" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:2" style="cursor:pointer"/><text x="207.6" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Protocol</text><rect x="279.2" y="178" width="208" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:3" style="cursor:pointer"/><text x="383.2" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Role</text><rect x="0" y="204" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:1:1" style="cursor:pointer"/><text x="7" y="221.3" font-size="12" text-anchor="start">Underlay</text><rect x="136" y="204" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:1:2" style="cursor:pointer"/><text x="143" y="221.3" font-size="12" text-anchor="start">eBGP</text><rect x="279.2" y="204" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:1:3" style="cursor:pointer"/><text x="286.2" y="221.3" font-size="12" text-anchor="start">loopback reachability</text><rect x="0" y="230" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:2:1" style="cursor:pointer"/><text x="7" y="247.3" font-size="12" text-anchor="start">Overlay control</text><rect x="136" y="230" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:2:2" style="cursor:pointer"/><text x="143" y="247.3" font-size="12" text-anchor="start">MP-BGP EVPN</text><rect x="279.2" y="230" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:2:3" style="cursor:pointer"/><text x="286.2" y="247.3" font-size="12" text-anchor="start">MAC/IP route distribution</text><rect x="0" y="256" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:3:1" style="cursor:pointer"/><text x="7" y="273.3" font-size="12" text-anchor="start">Overlay data</text><rect x="136" y="256" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:3:2" style="cursor:pointer"/><text x="143" y="273.3" font-size="12" text-anchor="start">VXLAN (UDP 4789)</text><rect x="279.2" y="256" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:3:3" style="cursor:pointer"/><text x="286.2" y="273.3" font-size="12" text-anchor="start">L2-in-L3 encapsulation</text><line x1="0" y1="178" x2="0" y2="282" stroke="#c9c7bf"/><line x1="136" y1="178" x2="136" y2="282" stroke="#c9c7bf"/><line x1="279.2" y1="178" x2="279.2" y2="282" stroke="#c9c7bf"/><line x1="487.2" y1="178" x2="487.2" y2="282" stroke="#c9c7bf"/><line x1="0" y1="178" x2="487.2" y2="178" stroke="#c9c7bf"/><line x1="0" y1="204" x2="487.2" y2="204" stroke="#c9c7bf"/><line x1="0" y1="230" x2="487.2" y2="230" stroke="#c9c7bf"/><line x1="0" y1="256" x2="487.2" y2="256" stroke="#c9c7bf"/><line x1="0" y1="282" x2="487.2" y2="282" stroke="#c9c7bf"/></g></g><metadata id="figdown-source" data-sha256="bc3fb7fb66c7628738359b5135f8da14572e5fb24c4f6efb18a6b8365ab602ea" data-engine-version="0.1.6"><![CDATA[
|
|
2
2
|
# FigDown — figures as text. Spec: https://github.com/FigDown/figdown
|
|
3
3
|
|
|
4
4
|
figdown 0.1 topology
|