figdown 0.1.5 → 0.1.7
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 +236 -9
- package/dist/figdown.mjs +236 -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/guide/layout.md +4 -3
- package/package.json +1 -1
- package/skill/figdown/figdown.html +234 -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.7",
|
|
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.7)
|
|
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.7";
|
|
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.7';
|
|
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
|
|
@@ -3112,6 +3112,45 @@ function renderScene(doc,y0){
|
|
|
3112
3112
|
const halo=' paint-order="stroke" stroke="#fff" stroke-width="3"';
|
|
3113
3113
|
const seg=(p,q,t,lbl,fs)=>reqLabel({p,q,t0:t,text:lbl,fs,col:ecol,halo,e,A,B,kind:'end'});
|
|
3114
3114
|
if(isBack.has(e)&&!pinned(e.a)&&!pinned(e.b)){
|
|
3115
|
+
// ── ROUTING-CHANGE ARCHITECTURE NOTE ──────────
|
|
3116
|
+
// Edge labels are DEFERRED: every label is registered against its
|
|
3117
|
+
// FINAL segment geometry (reqLabel/lblReq above) and placed by ONE
|
|
3118
|
+
// greedy pass after all edges are drawn; arrowheads are computed from
|
|
3119
|
+
// final terminal points (arrowTri). Therefore a routing change made
|
|
3120
|
+
// HERE — upstream of reqLabel/noteSegs/arrowTri — gets label and
|
|
3121
|
+
// arrowhead re-placement for free. A change spliced into the emitted
|
|
3122
|
+
// SVG downstream, or made outside the engine, will strand both.
|
|
3123
|
+
// Two prototype rounds re-derived this the hard way (floating
|
|
3124
|
+
// arrowheads, orphaned labels): that was external splicing, not an
|
|
3125
|
+
// engine gap. Patch routing here; do not "fix" the label machinery.
|
|
3126
|
+
if(A===B){
|
|
3127
|
+
// Self-transition: a small side loop on the node, the
|
|
3128
|
+
// convention of every drawing tool — never a lap of the figure
|
|
3129
|
+
// through the back-edge channel. Side order r,l,b,t; first side
|
|
3130
|
+
// whose loop box overlaps no other node wins (deterministic).
|
|
3131
|
+
const scy=A.y+A.h/2, scx=A.x+A.w/2;
|
|
3132
|
+
const mkLoop=sd=>sd==='r'?[[A.x+A.w,scy-8],[A.x+A.w+20,scy-8],[A.x+A.w+20,scy+8],[A.x+A.w,scy+8]]
|
|
3133
|
+
:sd==='l'?[[A.x,scy-8],[A.x-20,scy-8],[A.x-20,scy+8],[A.x,scy+8]]
|
|
3134
|
+
:sd==='b'?[[scx-8,A.y+A.h],[scx-8,A.y+A.h+20],[scx+8,A.y+A.h+20],[scx+8,A.y+A.h]]
|
|
3135
|
+
:[[scx-8,A.y],[scx-8,A.y-20],[scx+8,A.y-20],[scx+8,A.y]];
|
|
3136
|
+
const loopHit=pp=>{
|
|
3137
|
+
const x0=Math.min(...pp.map(p=>p[0]))-2, x1=Math.max(...pp.map(p=>p[0]))+2;
|
|
3138
|
+
const z0=Math.min(...pp.map(p=>p[1]))-2, z1=Math.max(...pp.map(p=>p[1]))+2;
|
|
3139
|
+
return nodes.some(n=>n!==A&&!n.boundary&&n.x<x1&&n.x+n.w>x0&&n.y<z1&&n.y+n.h>z0);
|
|
3140
|
+
};
|
|
3141
|
+
let sp=null;
|
|
3142
|
+
for(const sd of ['r','l','b','t']){ const pp=mkLoop(sd); if(!loopHit(pp)){ sp=pp; break; } }
|
|
3143
|
+
if(!sp) sp=mkLoop('r');
|
|
3144
|
+
for(const p of sp){ W=Math.max(W,p[0]+4); Hh=Math.max(Hh,p[1]+16-y0-20); }
|
|
3145
|
+
esvg.push('<path data-edge="'+e.line+'" d="'+roundPath(sp)+'" fill="none" stroke="'+col+'" stroke-width="1.6"'+dash+'/>');
|
|
3146
|
+
noteSegs(e,sp);
|
|
3147
|
+
if(e.mid) reqLabel({p:sp[1],q:sp[2],text:e.mid,fs:11,col:lcol,halo,e,A,B,kind:'mid',first:false});
|
|
3148
|
+
if(e.tail) seg(sp[0],sp[1],0.5,e.tail,10);
|
|
3149
|
+
if(e.head) seg(sp[3],sp[2],0.5,e.head,10);
|
|
3150
|
+
if(wantsStart) arrowTri(sp[0],sp[1],col);
|
|
3151
|
+
if(wantsEnd) arrowTri(sp[sp.length-1],sp[sp.length-2],col);
|
|
3152
|
+
continue;
|
|
3153
|
+
}
|
|
3115
3154
|
// back-edge (retry loop): polyline through a side channel beyond the
|
|
3116
3155
|
// occupied lanes instead of a straight line hidden under the spine,
|
|
3117
3156
|
// using the nested slot from the channel plan. Ring-eligible loops
|
|
@@ -3238,6 +3277,50 @@ function renderScene(doc,y0){
|
|
|
3238
3277
|
// their neighbours turns a 40-point staircase into the 2–4 bends a
|
|
3239
3278
|
// dummy-vertex chain should have, without moving the drawn line.
|
|
3240
3279
|
simplifyPts(pts);
|
|
3280
|
+
// Waypoint prune: after collinear simplification a
|
|
3281
|
+
// chain can still carry a staircase of near-collinear jogs — the drift
|
|
3282
|
+
// clamp allows only a few px of sideways movement per rank, so a run
|
|
3283
|
+
// that wants to move 35px sideways alternates short diagonals and
|
|
3284
|
+
// verticals that Douglas-Peucker cannot collapse (each angle differs).
|
|
3285
|
+
// The rule: an interior waypoint SURVIVES only if removing it would
|
|
3286
|
+
// make the edge pierce something. Obstacles are the same set the
|
|
3287
|
+
// straight-edge router uses below: non-incident nodes, plus group
|
|
3288
|
+
// boxes when NEITHER endpoint lies inside (a run through a group
|
|
3289
|
+
// interior asserts membership the figure does not declare). Fixed
|
|
3290
|
+
// left-to-right scan to fixpoint — deterministic. Endpoints are then
|
|
3291
|
+
// re-derived so the edge leaves its node facing the first bend, and
|
|
3292
|
+
// each re-derived terminal segment is re-checked: if it would hit an
|
|
3293
|
+
// obstacle the old endpoint stays.
|
|
3294
|
+
{
|
|
3295
|
+
const pobs=nodes.filter(n=>n!==A&&n!==B&&!n.boundary).map(n=>({x:n.x,y:n.y,w:n.w,h:n.h}));
|
|
3296
|
+
const ep0=pts[0], ep1=pts[pts.length-1];
|
|
3297
|
+
for(const k in gBox){
|
|
3298
|
+
const b=gBox[k];
|
|
3299
|
+
const inG=(px,py)=>px>b.x0&&px<b.x1&&py>b.yA&&py<b.yB;
|
|
3300
|
+
if(!inG(ep0[0],ep0[1])&&!inG(ep1[0],ep1[1]))
|
|
3301
|
+
pobs.push({x:b.x0,y:b.yA,w:b.x1-b.x0,h:b.yB-b.yA});
|
|
3302
|
+
}
|
|
3303
|
+
let ch=true, pruned=false;
|
|
3304
|
+
while(ch){ ch=false;
|
|
3305
|
+
for(let i=1;i+1<pts.length;i++)
|
|
3306
|
+
if(!segHitsObs(pts[i-1],pts[i+1],pobs)){ pts.splice(i,1); ch=true; pruned=true; i--; }
|
|
3307
|
+
}
|
|
3308
|
+
// untouched chains stay byte-identical: endpoints and the label
|
|
3309
|
+
// segment are only re-derived when the prune removed a waypoint
|
|
3310
|
+
if(pruned){
|
|
3311
|
+
const nb0=borderPoint(A,pts[1][0],pts[1][1]);
|
|
3312
|
+
if(!segHitsObs(nb0,pts[1],pobs)) pts[0]=nb0;
|
|
3313
|
+
const nb1=borderPoint(B,pts[pts.length-2][0],pts[pts.length-2][1]);
|
|
3314
|
+
if(!segHitsObs(nb1,pts[pts.length-2],pobs)) pts[pts.length-1]=nb1;
|
|
3315
|
+
if(midSeg){ let bi=0,bl=-1;
|
|
3316
|
+
for(let i=0;i+1<pts.length;i++){
|
|
3317
|
+
const l=Math.hypot(pts[i+1][0]-pts[i][0],pts[i+1][1]-pts[i][1]);
|
|
3318
|
+
if(l>bl){ bl=l; bi=i; }
|
|
3319
|
+
}
|
|
3320
|
+
midSeg=[pts[bi],pts[bi+1]];
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3241
3324
|
esvg.push('<path data-edge="'+e.line+'" d="'+roundPath(pts)+'" fill="none" stroke="'+col+'" stroke-width="1.6"'+dash+'/>');
|
|
3242
3325
|
noteSegs(e,pts);
|
|
3243
3326
|
if(midSeg) reqLabel({p:midSeg[0],q:midSeg[1],text:e.mid,fs:11,col:lcol,halo,e,A,B,kind:'mid',first:false});
|
|
@@ -3585,6 +3668,89 @@ function routeAround(p,q,obs){
|
|
|
3585
3668
|
return pts.length>2?pts:null;
|
|
3586
3669
|
}
|
|
3587
3670
|
|
|
3671
|
+
// ---- shared-edge grid (0.1.6) ----
|
|
3672
|
+
// A boundary between two cells belongs to BOTH of them. Drawing each cell as a
|
|
3673
|
+
// stroked rect drew that boundary twice, and the second painting won: a marked
|
|
3674
|
+
// cell's colour, or a conditional field's dash, was whatever the neighbour
|
|
3675
|
+
// happened to paint last. So the grid is now emitted edge-by-edge, each edge
|
|
3676
|
+
// exactly once, and its appearance is decided by its (at most two) owners.
|
|
3677
|
+
//
|
|
3678
|
+
// WHAT THE OLD DRAWING DESTROYED. A `present=` field's shared edge was
|
|
3679
|
+
// overwritten solid by a plain neighbour. `STYLE-KEY-SCOPE` rules that the dash IS
|
|
3680
|
+
// conditional presence and that nothing else can set or clear it — so the
|
|
3681
|
+
// model's flag was being silently erased at render time. This is a
|
|
3682
|
+
// correctness fix, not a cosmetic one.
|
|
3683
|
+
//
|
|
3684
|
+
// WHY NOT HALF-WIDTH. The obvious alternative gives each owner half the
|
|
3685
|
+
// boundary: two 0.5px bands, one per side. Point-sampled at 1× — the size a
|
|
3686
|
+
// figure is actually read at — a shared edge between an orange mark and a
|
|
3687
|
+
// green one reads #2E6E34: one muddy pixel with the orange unrecoverable,
|
|
3688
|
+
// because half-width strokes sit at quarter-pixel offsets and the rasteriser
|
|
3689
|
+
// averages them. The bands here land on integer pixels and every colour
|
|
3690
|
+
// survives exactly. A zoomed crop hides this entirely, which is why the
|
|
3691
|
+
// decision was made at 1× and must be re-checked at 1× if it is revisited.
|
|
3692
|
+
//
|
|
3693
|
+
// THE RESIDUAL AMBIGUITY, stated rather than hidden. A dashed edge between a
|
|
3694
|
+
// conditional field and a plain one can be read locally as if the plain one
|
|
3695
|
+
// were conditional too. Today's drawing has the same ambiguity whenever the
|
|
3696
|
+
// dash happens to win the overwrite; this is strictly better, not solved.
|
|
3697
|
+
//
|
|
3698
|
+
// WHAT IT COSTS. Two adjacent marked cells put 3px of ink at their shared
|
|
3699
|
+
// boundary (ring + line + ring) against 1px elsewhere: the grid stays even,
|
|
3700
|
+
// but that region reads heavier. Four differently marked cells meeting at a
|
|
3701
|
+
// point give four ring corners around one crossing — busy at 8×, invisible
|
|
3702
|
+
// at 1×.
|
|
3703
|
+
//
|
|
3704
|
+
// `edges` are runs produced by edgeRuns(); `def` is the block's grid colour.
|
|
3705
|
+
function edgeSvg(edges, def){
|
|
3706
|
+
const W=1, out=[];
|
|
3707
|
+
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)
|
|
3708
|
+
+'" stroke="'+col+'"'+(w!==1?' stroke-width="'+w+'"':'')+(dash?' stroke-dasharray="5 3"':'')+'/>';
|
|
3709
|
+
for(const g of edges){
|
|
3710
|
+
const A=g.a, B=g.b;
|
|
3711
|
+
// The shared line: default colour, full weight, dashed iff EITHER owner
|
|
3712
|
+
// carries `present=`. A field with both `stroke=` and `present=` therefore
|
|
3713
|
+
// gets a dashed boundary AND a separate coloured ring, not a coloured
|
|
3714
|
+
// dash — the two marks answer different questions and must not merge.
|
|
3715
|
+
out.push(L(g.v, g.p, g.s, g.e, def, (A&&A.d)||(B&&B.d), W));
|
|
3716
|
+
// The class marks: solid, full weight, inset INSIDE their own cell. An end
|
|
3717
|
+
// pulls in by W/2 where the owner stops and the ring turns the corner;
|
|
3718
|
+
// it is left long where the owner continues past that end and the next run
|
|
3719
|
+
// carries on, so a run that ends only because the NEIGHBOUR changed joins
|
|
3720
|
+
// with no seam and no ring protrudes past its own cell. The test is the
|
|
3721
|
+
// ownership map (does this cell own the next unit too?), not the geometry.
|
|
3722
|
+
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));
|
|
3723
|
+
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));
|
|
3724
|
+
}
|
|
3725
|
+
return out.join('');
|
|
3726
|
+
}
|
|
3727
|
+
// Walk one grid line unit by unit and merge collinear units that have the same
|
|
3728
|
+
// pair of owners into a single run. `v` is 1 for a vertical line, `p` its fixed
|
|
3729
|
+
// coordinate; `n` units; `span(i)` -> [start,end]; `ownAt(i)` -> [before,after]
|
|
3730
|
+
// where an owner is {id, c:colour|null, d:dashed}. A unit with no owner, or
|
|
3731
|
+
// with the SAME owner on both sides (a merged/spanning cell's interior), draws
|
|
3732
|
+
// nothing at all — which is where the doubled interior lines went.
|
|
3733
|
+
function edgeRuns(v, p, n, span, ownAt){
|
|
3734
|
+
const out=[]; let run=null;
|
|
3735
|
+
const sid=(o)=>o?o.id:null;
|
|
3736
|
+
const cont=(i,k,o)=>!!o && sid((ownAt(i)||[])[k])===o.id;
|
|
3737
|
+
for(let i=0;i<n;i++){
|
|
3738
|
+
const o=ownAt(i)||[], a=o[0]||null, b=o[1]||null;
|
|
3739
|
+
const skip=(!a&&!b)||(a&&b&&a.id===b.id);
|
|
3740
|
+
const key=skip?null:JSON.stringify([a&&[a.c,a.d], b&&[b.c,b.d]]);
|
|
3741
|
+
const sp=span(i);
|
|
3742
|
+
if(run && run.key===key && run.e===sp[0]){ run.e=sp[1]; run.i1=i; continue; }
|
|
3743
|
+
if(run){ out.push(run); run=null; }
|
|
3744
|
+
if(!skip) run={v:v,p:p,s:sp[0],e:sp[1],key:key,a:a,b:b,i0:i,i1:i};
|
|
3745
|
+
}
|
|
3746
|
+
if(run) out.push(run);
|
|
3747
|
+
for(const r of out){
|
|
3748
|
+
r.ca0=cont(r.i0-1,0,r.a); r.ca1=cont(r.i1+1,0,r.a);
|
|
3749
|
+
r.cb0=cont(r.i0-1,1,r.b); r.cb1=cont(r.i1+1,1,r.b);
|
|
3750
|
+
}
|
|
3751
|
+
return out;
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3588
3754
|
// ---- bitfield ----
|
|
3589
3755
|
function renderBitfield(b,y0){
|
|
3590
3756
|
const cell=Math.max(18,Math.min(28,Math.floor(760/b.word))), rh=30, ruler=16;
|
|
@@ -3777,9 +3943,11 @@ function renderBitfield(b,y0){
|
|
|
3777
3943
|
// one edge a per-row rect cannot leave out: a rect strokes four sides or
|
|
3778
3944
|
// none, and "none" would also lose the two verticals that must continue.
|
|
3779
3945
|
// Fill stays a single closed region, so `fill=` paints the field once with
|
|
3780
|
-
// no seam
|
|
3781
|
-
//
|
|
3782
|
-
//
|
|
3946
|
+
// no seam. Since 0.1.6 the path is unstroked and the boundary comes from the
|
|
3947
|
+
// shared-edge grid instead; the internal boundary is still never drawn,
|
|
3948
|
+
// because both of its sides are owned by the same field and edgeRuns() skips
|
|
3949
|
+
// a unit whose two owners are one (`STYLE-KEY-SCOPE`: the dash IS conditional presence,
|
|
3950
|
+
// and nothing else may set or clear it).
|
|
3783
3951
|
const boxOutline=(bx)=>{
|
|
3784
3952
|
const bands=[];
|
|
3785
3953
|
for(const s of bx){
|
|
@@ -3801,6 +3969,11 @@ function renderBitfield(b,y0){
|
|
|
3801
3969
|
}
|
|
3802
3970
|
return 'M'+pts.map(p=>p[0]+' '+p[1]).join(' L')+' Z';
|
|
3803
3971
|
};
|
|
3972
|
+
// Shared-edge grid (0.1.6): field fills carry no stroke of their own. Every
|
|
3973
|
+
// bit column of every row records which field owns it, and the grid is
|
|
3974
|
+
// emitted once, edge by edge, after the fields — see edgeSvg() above.
|
|
3975
|
+
const BOWN=new Map(), BDEF=b.stroke||'#555';
|
|
3976
|
+
const bkey=(rw,cl)=>rw+':'+cl; let BOXN=0;
|
|
3804
3977
|
let pos=0; // bit cursor
|
|
3805
3978
|
for(const f of b.fields){
|
|
3806
3979
|
if(f.wrap){ pos=Math.ceil((pos||1)/b.word)*b.word; continue; }
|
|
@@ -3826,9 +3999,11 @@ function renderBitfield(b,y0){
|
|
|
3826
3999
|
// `PRESENCE-CONDITION-EXPRESSION`: the carrier is now `present=`, and BOTH of its
|
|
3827
4000
|
// written forms dash — `present=""` claims conditional presence just as
|
|
3828
4001
|
// `present="C = 1"` does; only the caption below distinguishes them.
|
|
3829
|
-
|
|
4002
|
+
// 0.1.6: the dash and the class colour no longer ride on the box's own
|
|
4003
|
+
// stroke — they are properties of the shared boundary and of the ring
|
|
4004
|
+
// inside the cell respectively, recorded per bit below and drawn once.
|
|
3830
4005
|
const cfill=f.fill||b.fill||'#fff';
|
|
3831
|
-
const paint=' fill="'+cfill+'" stroke="
|
|
4006
|
+
const paint=' fill="'+cfill+'" stroke="none"';
|
|
3832
4007
|
// Draw one occurrence. `suffix` is the derived index label — '' for a
|
|
3833
4008
|
// field that is not one element of a run, ' [0]' / ' [n]' for the two
|
|
3834
4009
|
// occurrences `REPEATED-RUN-DRAWING` draws. It is APPENDED to the author's label, and the
|
|
@@ -3863,6 +4038,13 @@ function renderBitfield(b,y0){
|
|
|
3863
4038
|
? '<rect x="'+bx[0].x+'" y="'+bx[0].y+'" width="'+bx[0].w+'" height="'+(bx.length*rh)+'"'+paint
|
|
3864
4039
|
: '<path d="'+boxOutline(bx)+'"'+paint;
|
|
3865
4040
|
const tag=flat?'rect':'path';
|
|
4041
|
+
// Record ownership per BIT, one entry per (row, column) this occurrence
|
|
4042
|
+
// covers. Every occurrence of a repeated field is its own owner id, so
|
|
4043
|
+
// the boundary between two occurrences is a real boundary and is drawn.
|
|
4044
|
+
const bid=b.id+'_'+(BOXN++);
|
|
4045
|
+
const brec={id:bid, c:f.stroke||null, d:f.present!==undefined};
|
|
4046
|
+
bx.forEach(function(s){ const c0=Math.round(s.x/cell), n=Math.round(s.w/cell);
|
|
4047
|
+
for(let k=0;k<n;k++) BOWN.set(bkey(s.row,c0+k), brec); });
|
|
3866
4048
|
svg.push(desc?shape+'>'+desc+'</'+tag+'>':shape+'/>');
|
|
3867
4049
|
// ONE caption per box: the NAME on the first box, CONT on every later
|
|
3868
4050
|
// one. The first box, not the widest — reading order is the order the
|
|
@@ -3923,6 +4105,29 @@ function renderBitfield(b,y0){
|
|
|
3923
4105
|
}
|
|
3924
4106
|
}
|
|
3925
4107
|
}
|
|
4108
|
+
// The shared-edge grid for the bitfield: one pass over every vertical bit
|
|
4109
|
+
// boundary and every horizontal row boundary, each emitted once.
|
|
4110
|
+
let maxRow=-1;
|
|
4111
|
+
BOWN.forEach(function(v,k){ const r=+k.split(':')[0]; if(r>maxRow) maxRow=r; });
|
|
4112
|
+
const yrow=(r)=>y+r*rh+shiftFor(r);
|
|
4113
|
+
const at=(r,c)=>(r<0||r>maxRow||c<0||c>=b.word)?null:(BOWN.get(bkey(r,c))||null);
|
|
4114
|
+
const BE=[];
|
|
4115
|
+
for(let c=0;c<=b.word;c++)
|
|
4116
|
+
BE.push.apply(BE, edgeRuns(1, c*cell, maxRow+1,
|
|
4117
|
+
i=>[yrow(i), yrow(i)+rh], i=>[at(i,c-1), at(i,c)]));
|
|
4118
|
+
// A horizontal boundary is a row's TOP, shared with the row above when the
|
|
4119
|
+
// two are vertically adjacent. A row also needs a bottom of its own when the
|
|
4120
|
+
// row below is not adjacent — an elision strip has been inserted between
|
|
4121
|
+
// them, and shiftFor() has pushed it down.
|
|
4122
|
+
const hb=[];
|
|
4123
|
+
for(let r=0;r<=maxRow;r++){
|
|
4124
|
+
hb.push({y:yrow(r), ar:(r>0 && yrow(r-1)+rh===yrow(r))?r-1:-1, br:r});
|
|
4125
|
+
if(r===maxRow || yrow(r+1)!==yrow(r)+rh) hb.push({y:yrow(r)+rh, ar:r, br:-1});
|
|
4126
|
+
}
|
|
4127
|
+
for(const hbe of hb)
|
|
4128
|
+
BE.push.apply(BE, edgeRuns(0, hbe.y, b.word,
|
|
4129
|
+
i=>[i*cell, (i+1)*cell], i=>[hbe.ar<0?null:at(hbe.ar,i), hbe.br<0?null:at(hbe.br,i)]));
|
|
4130
|
+
svg.push(edgeSvg(BE, BDEF));
|
|
3926
4131
|
for(const e of elis){
|
|
3927
4132
|
const st=(b.stroke||'#555');
|
|
3928
4133
|
// `ELISION-MARK-EXTENT`: THE SIDE DOTS ARE ALWAYS DRAWN. This reverses `ELISION-MARK-EXTENT`,
|
|
@@ -4018,6 +4223,14 @@ function renderTable(t,y0){
|
|
|
4018
4223
|
// cell marks: h1..hN address header tiers top-down, r>=1 the data rows
|
|
4019
4224
|
const markOf=(r,c)=>(t.marks||[]).find(mk=>(mk.hdr?mk.r-1:H+mk.r-1)===r&&mk.c===c+1);
|
|
4020
4225
|
const yTop=y+4;
|
|
4226
|
+
// Shared-edge grid (0.1.6): cell fills carry no stroke of their own. Every
|
|
4227
|
+
// grid cell records its owning (anchor) cell, and the grid is emitted once,
|
|
4228
|
+
// edge by edge, after the cells — see edgeSvg() above. Table cells have no
|
|
4229
|
+
// conditional-presence mark, so `d` is always false here; the dash branch
|
|
4230
|
+
// exists for the bitfield and is kept common so both genres draw alike.
|
|
4231
|
+
const NC=t.cols.length, DEF=t.stroke||'#c9c7bf';
|
|
4232
|
+
const OWN=grid.map(()=>new Array(NC).fill(null));
|
|
4233
|
+
const xAt=[0]; for(let i=0;i<NC;i++) xAt.push(xAt[i]+widths[i]);
|
|
4021
4234
|
for(let r=0;r<grid.length;r++){
|
|
4022
4235
|
for(let c=0;c<grid[r].length;c++){
|
|
4023
4236
|
const cell=grid[r][c];
|
|
@@ -4033,7 +4246,11 @@ function renderTable(t,y0){
|
|
|
4033
4246
|
// addressable cells carry table-id:row:col (row 0 = bottom header tier)
|
|
4034
4247
|
const addrR = r>=H ? (r-H+1) : (r===H-1 ? 0 : null);
|
|
4035
4248
|
const addr = addrR===null ? '' : ' data-cell="'+t.id+':'+addrR+':'+(c+1)+'" style="cursor:pointer"';
|
|
4036
|
-
|
|
4249
|
+
// A merged cell owns every grid square it spans, so its internal
|
|
4250
|
+
// boundaries have the same owner on both sides and are never drawn.
|
|
4251
|
+
const rec={id:'c'+r+'_'+c, c:(mk&&mk.stroke)||null, d:false};
|
|
4252
|
+
for(let rr=r;rr<r+rs;rr++) for(let cc=c;cc<c+cs;cc++) if(OWN[rr]) OWN[rr][cc]=rec;
|
|
4253
|
+
svg.push('<rect x="'+x+'" y="'+yy+'" width="'+wsum+'" height="'+h+'" fill="'+fill+'" stroke="none"'+addr+'/>');
|
|
4037
4254
|
// alignment: headers centered; data follows GFM colon alignment (default left)
|
|
4038
4255
|
const al=cell.hdr?'center':(alignOf(c)||'left');
|
|
4039
4256
|
const tx=al==='center'?x+wsum/2:(al==='right'?x+wsum-7:x+7);
|
|
@@ -4049,6 +4266,16 @@ function renderTable(t,y0){
|
|
|
4049
4266
|
}
|
|
4050
4267
|
}
|
|
4051
4268
|
}
|
|
4269
|
+
// Each edge drawn exactly once, owned by the (at most two) cells it divides.
|
|
4270
|
+
const EDG=[];
|
|
4271
|
+
const cellAt=(r,c)=>(r<0||r>=grid.length||c<0||c>=NC)?null:OWN[r][c];
|
|
4272
|
+
for(let c=0;c<=NC;c++)
|
|
4273
|
+
EDG.push.apply(EDG, edgeRuns(1, xAt[c], grid.length,
|
|
4274
|
+
i=>[yTop+yAt[i], yTop+yAt[i+1]], i=>[cellAt(i,c-1), cellAt(i,c)]));
|
|
4275
|
+
for(let r=0;r<=grid.length;r++)
|
|
4276
|
+
EDG.push.apply(EDG, edgeRuns(0, yTop+yAt[r], NC,
|
|
4277
|
+
i=>[xAt[i], xAt[i+1]], i=>[cellAt(r-1,i), cellAt(r,i)]));
|
|
4278
|
+
svg.push(edgeSvg(EDG, DEF));
|
|
4052
4279
|
const yEnd=yTop+yAt[grid.length];
|
|
4053
4280
|
return {svg:svg.join(''), y:yEnd+6, w:totalW+2};
|
|
4054
4281
|
}
|