figdown 0.1.6 → 0.1.8
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 +170 -13
- package/dist/figdown.mjs +170 -13
- 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 +168 -11
|
@@ -121,7 +121,7 @@ const SHAPES = ['box','rounded','circle','ellipse','diamond','cylinder'];
|
|
|
121
121
|
// input to that promise, and under core §13 a 0.x renderer may differ from
|
|
122
122
|
// the next — which makes the recorded version the only thing that can
|
|
123
123
|
// explain a diff between two renderings of one source.
|
|
124
|
-
const FIGDOWN_VERSION = '0.1.
|
|
124
|
+
const FIGDOWN_VERSION = '0.1.8';
|
|
125
125
|
// Retired shape VALUES keep a named diagnostic (PROCESS §5(d)), the same way
|
|
126
126
|
// retired option keys do: `cloud` was the one value that named a domain
|
|
127
127
|
// (the internet cloud) in an enum the language keeps purely geometric
|
|
@@ -2455,6 +2455,80 @@ function findComment(s){
|
|
|
2455
2455
|
// 2. LAYOUT + RENDER (deterministic; no randomness, no Date)
|
|
2456
2456
|
// ============================================================
|
|
2457
2457
|
const FONT=13, CH=7.2, PADX=14, NH=36, GAPX=56, GAPY=44;
|
|
2458
|
+
|
|
2459
|
+
// ---- cw(): script-aware advance width --------------------------------
|
|
2460
|
+
// Every px-per-character constant in this file — CH here, and its siblings at
|
|
2461
|
+
// the other font sizes (8.6 title, 6.6 legend, 6.5 edge label, 6.3/6.2 bitfield
|
|
2462
|
+
// caption) — is calibrated on LATIN, where one character advances ~0.554 em.
|
|
2463
|
+
// That is a claim about SCRIPT, not about font: a CJK ideograph advances a full
|
|
2464
|
+
// em, so counting characters undersizes a Chinese label by 45% and the engine
|
|
2465
|
+
// draws the text outside the box it has just sized for it. cw() is the measure
|
|
2466
|
+
// those constants multiply — a string's advance in Latin units. Every codepoint
|
|
2467
|
+
// below U+0300 weighs exactly 1, so an all-ASCII string returns exactly its
|
|
2468
|
+
// .length and every figure that was already right stays byte-identical.
|
|
2469
|
+
//
|
|
2470
|
+
// The classes are Unicode East Asian Width (UAX #11), a fixed character
|
|
2471
|
+
// property: a lookup by codepoint range, deterministic, no font-metrics library
|
|
2472
|
+
// and no canvas measurement at render time. The WEIGHTS are measured, not
|
|
2473
|
+
// chosen — probe strings rendered through librsvg at font-size 13 in the
|
|
2474
|
+
// engine's own stack (font-family="system-ui,sans-serif" → Noto Sans + Noto
|
|
2475
|
+
// Sans CJK), advance recovered as the slope of ink extent over repeat count:
|
|
2476
|
+
// ideograph 封 一 㐀, kana あ ア, bopomofo ㄅ, fullwidth A ,, U+3000 13.000px = 1.000em
|
|
2477
|
+
// emoji U+1F600 U+1F680 (Noto Color Emoji) 16.188px = 1.245em
|
|
2478
|
+
// combining U+0301 standalone, U+200B, U+FEFF 0.000px = 0.000em
|
|
2479
|
+
// Latin — the calibration these constants already carry 7.200px = 0.554em
|
|
2480
|
+
// hence WIDE = 1.000/0.554 = 1.806, rounded UP to 1.81 so a wide glyph is never
|
|
2481
|
+
// UNDER-measured at the node-label site, and EMOJI = 1.245/0.554 = 2.25.
|
|
2482
|
+
// Halfwidth katakana (U+FF61..FF9F) measured 0.500 em: it stays in the default
|
|
2483
|
+
// class, which over-measures it by 11% — the safe direction.
|
|
2484
|
+
//
|
|
2485
|
+
// EAW *Ambiguous* — §, —, –, →, ←, ✓, ≤, •, the only non-ASCII this
|
|
2486
|
+
// repository's figures actually draw — is deliberately NARROW, the
|
|
2487
|
+
// wcwidth/POSIX reading for a non-East-Asian context. Measured, an em dash is
|
|
2488
|
+
// a full em in this font, so a label carrying one is still under-measured by
|
|
2489
|
+
// 0.45 unit; that is a different question from this defect (it needs a
|
|
2490
|
+
// context rule, not a table) and is not decided here.
|
|
2491
|
+
const CW_ZERO=[[0x0300,0x036F],[0x0483,0x0489],[0x0591,0x05BD],[0x0610,0x061A],
|
|
2492
|
+
[0x064B,0x065F],[0x0670,0x0670],[0x06D6,0x06DC],[0x0711,0x0711],[0x0730,0x074A],
|
|
2493
|
+
[0x07A6,0x07B0],[0x0816,0x0819],[0x093C,0x093C],[0x0951,0x0954],[0x0E31,0x0E31],
|
|
2494
|
+
[0x0E34,0x0E3A],[0x0E47,0x0E4E],[0x1AB0,0x1AFF],[0x1DC0,0x1DFF],[0x200B,0x200F],
|
|
2495
|
+
[0x2060,0x2064],[0x20D0,0x20F0],[0xFE00,0xFE0F],[0xFE20,0xFE2F],[0xFEFF,0xFEFF]];
|
|
2496
|
+
const CW_EMOJI=[[0x1F004,0x1F0CF],[0x1F18E,0x1F19A],[0x1F300,0x1F5FF],
|
|
2497
|
+
[0x1F600,0x1F64F],[0x1F680,0x1F6FF],[0x1F7E0,0x1F7EB],[0x1F900,0x1F9FF],
|
|
2498
|
+
[0x1FA70,0x1FAFF]];
|
|
2499
|
+
const CW_WIDE=[[0x1100,0x115F],[0x2E80,0x303E],[0x3041,0x33FF],[0x3400,0x4DBF],
|
|
2500
|
+
[0x4E00,0x9FFF],[0xA000,0xA4CF],[0xA960,0xA97F],[0xAC00,0xD7A3],[0xF900,0xFAFF],
|
|
2501
|
+
[0xFE10,0xFE19],[0xFE30,0xFE6F],[0xFF01,0xFF60],[0xFFE0,0xFFE6],
|
|
2502
|
+
[0x17000,0x18AFF],[0x1B000,0x1B2FF],[0x20000,0x3FFFD]];
|
|
2503
|
+
// ranges are sorted and disjoint, so the scan stops at the first range that
|
|
2504
|
+
// starts above the codepoint
|
|
2505
|
+
function cwIn(cp,rs){
|
|
2506
|
+
for(let i=0;i<rs.length;i++){ if(cp<rs[i][0]) return false; if(cp<=rs[i][1]) return true; }
|
|
2507
|
+
return false;
|
|
2508
|
+
}
|
|
2509
|
+
function cwOf(cp){
|
|
2510
|
+
if(cp<0x0300) return 1; // ASCII and Latin-1: the calibrated case
|
|
2511
|
+
if(cwIn(cp,CW_ZERO)) return 0;
|
|
2512
|
+
if(cwIn(cp,CW_EMOJI)) return 2.25;
|
|
2513
|
+
if(cwIn(cp,CW_WIDE)) return 1.81;
|
|
2514
|
+
return 1;
|
|
2515
|
+
}
|
|
2516
|
+
// Iterate CODEPOINTS, not UTF-16 units: an astral character is one advance,
|
|
2517
|
+
// not two. A ZWJ emoji sequence still counts each codepoint, so it is
|
|
2518
|
+
// over-measured — the safe direction, and the only way to avoid it is a
|
|
2519
|
+
// grapheme segmenter this engine will not carry.
|
|
2520
|
+
function cw(s){
|
|
2521
|
+
s=String(s); let w=0;
|
|
2522
|
+
for(let i=0;i<s.length;i++){
|
|
2523
|
+
const cp=s.codePointAt(i);
|
|
2524
|
+
if(cp>0xFFFF) i++;
|
|
2525
|
+
w+=cwOf(cp);
|
|
2526
|
+
}
|
|
2527
|
+
return w;
|
|
2528
|
+
}
|
|
2529
|
+
// the widest line of a multi-line string, in Latin units
|
|
2530
|
+
function cwMax(s){ return Math.max(...String(s).split('\n').map(cw)); }
|
|
2531
|
+
|
|
2458
2532
|
// §5 style= → SVG dash pattern. `def` is the construct's conventional
|
|
2459
2533
|
// default (the bundle ring and the threshold line are dashed by convention);
|
|
2460
2534
|
// an explicit style= always wins.
|
|
@@ -2464,7 +2538,7 @@ function dashOf(style,def){
|
|
|
2464
2538
|
}
|
|
2465
2539
|
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
2466
2540
|
function tw(s){
|
|
2467
|
-
const longest=
|
|
2541
|
+
const longest=cwMax(s);
|
|
2468
2542
|
return Math.max(30,longest*CH+2*PADX);
|
|
2469
2543
|
}
|
|
2470
2544
|
// ---- shape geometry (one source of truth for size and for clipping) ----
|
|
@@ -2583,7 +2657,7 @@ function inscribedHalfW(g,v){
|
|
|
2583
2657
|
const LBLPADX=6, LBLPADY=4;
|
|
2584
2658
|
function labelBox(label){
|
|
2585
2659
|
const ls=String(label).split('\n');
|
|
2586
|
-
return [Math.max(...ls.map(
|
|
2660
|
+
return [Math.max(...ls.map(cw))*CH+2*LBLPADX,
|
|
2587
2661
|
((ls.length-1)*1.3+1.2)*FONT+2*LBLPADY];
|
|
2588
2662
|
}
|
|
2589
2663
|
// fitOutline: grow the box by the smallest factor that pulls an iw x ih box,
|
|
@@ -2675,7 +2749,7 @@ function render(doc,ropts){
|
|
|
2675
2749
|
}
|
|
2676
2750
|
const parts=[]; let y=0, maxW=0;
|
|
2677
2751
|
if(doc.title && RO.title===true){ parts.push('<text x="0" y="16" font-size="15" font-weight="600">'+esc(doc.title)+'</text>'); y=30;
|
|
2678
|
-
maxW=Math.max(maxW, doc.title
|
|
2752
|
+
maxW=Math.max(maxW, cw(doc.title)*8.6); } // canvas must fit the title
|
|
2679
2753
|
let sceneMeta=null;
|
|
2680
2754
|
if(doc.nodes.length||doc.edges.length||(doc.boundaries||[]).length){
|
|
2681
2755
|
const s=renderScene(doc,y); parts.push(s.svg); y=s.y; maxW=Math.max(maxW,s.w);
|
|
@@ -2713,7 +2787,7 @@ function render(doc,ropts){
|
|
|
2713
2787
|
// `style`) is drawn, so "declared but not shown" is now unreachable.
|
|
2714
2788
|
for(const c of legendCls){
|
|
2715
2789
|
const paints=c.fill!==undefined||c.stroke!==undefined||c.style!==undefined;
|
|
2716
|
-
const tw=
|
|
2790
|
+
const tw=cw(c.label)*6.6+(paints?30:9);
|
|
2717
2791
|
if(lx>0 && lx+tw>wrapW){ lx=0; ly+=rowH; }
|
|
2718
2792
|
if(paints){
|
|
2719
2793
|
const dash=c.style==='dashed'?' stroke-dasharray="6 4"':(c.style==='dotted'?' stroke-dasharray="2 4"':'');
|
|
@@ -2826,7 +2900,7 @@ function renderScene(doc,y0){
|
|
|
2826
2900
|
// ALL nodes participate in auto layout (pinned ones hold their slot so
|
|
2827
2901
|
// that pinning X never reflows Y); pins override coordinates afterwards.
|
|
2828
2902
|
const horiz=(doc.flow==='right'||doc.flow==='left');
|
|
2829
|
-
const lblPx=s=>
|
|
2903
|
+
const lblPx=s=>cwMax(s)*6.5;
|
|
2830
2904
|
const lay=[...nodes]; // layout participants
|
|
2831
2905
|
const chains=new Map(); // edge -> [A, ...waypoints, B]
|
|
2832
2906
|
for(const e of doc.edges){
|
|
@@ -3209,6 +3283,45 @@ function renderScene(doc,y0){
|
|
|
3209
3283
|
const halo=' paint-order="stroke" stroke="#fff" stroke-width="3"';
|
|
3210
3284
|
const seg=(p,q,t,lbl,fs)=>reqLabel({p,q,t0:t,text:lbl,fs,col:ecol,halo,e,A,B,kind:'end'});
|
|
3211
3285
|
if(isBack.has(e)&&!pinned(e.a)&&!pinned(e.b)){
|
|
3286
|
+
// ── ROUTING-CHANGE ARCHITECTURE NOTE ──────────
|
|
3287
|
+
// Edge labels are DEFERRED: every label is registered against its
|
|
3288
|
+
// FINAL segment geometry (reqLabel/lblReq above) and placed by ONE
|
|
3289
|
+
// greedy pass after all edges are drawn; arrowheads are computed from
|
|
3290
|
+
// final terminal points (arrowTri). Therefore a routing change made
|
|
3291
|
+
// HERE — upstream of reqLabel/noteSegs/arrowTri — gets label and
|
|
3292
|
+
// arrowhead re-placement for free. A change spliced into the emitted
|
|
3293
|
+
// SVG downstream, or made outside the engine, will strand both.
|
|
3294
|
+
// Two prototype rounds re-derived this the hard way (floating
|
|
3295
|
+
// arrowheads, orphaned labels): that was external splicing, not an
|
|
3296
|
+
// engine gap. Patch routing here; do not "fix" the label machinery.
|
|
3297
|
+
if(A===B){
|
|
3298
|
+
// Self-transition: a small side loop on the node, the
|
|
3299
|
+
// convention of every drawing tool — never a lap of the figure
|
|
3300
|
+
// through the back-edge channel. Side order r,l,b,t; first side
|
|
3301
|
+
// whose loop box overlaps no other node wins (deterministic).
|
|
3302
|
+
const scy=A.y+A.h/2, scx=A.x+A.w/2;
|
|
3303
|
+
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]]
|
|
3304
|
+
:sd==='l'?[[A.x,scy-8],[A.x-20,scy-8],[A.x-20,scy+8],[A.x,scy+8]]
|
|
3305
|
+
: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]]
|
|
3306
|
+
:[[scx-8,A.y],[scx-8,A.y-20],[scx+8,A.y-20],[scx+8,A.y]];
|
|
3307
|
+
const loopHit=pp=>{
|
|
3308
|
+
const x0=Math.min(...pp.map(p=>p[0]))-2, x1=Math.max(...pp.map(p=>p[0]))+2;
|
|
3309
|
+
const z0=Math.min(...pp.map(p=>p[1]))-2, z1=Math.max(...pp.map(p=>p[1]))+2;
|
|
3310
|
+
return nodes.some(n=>n!==A&&!n.boundary&&n.x<x1&&n.x+n.w>x0&&n.y<z1&&n.y+n.h>z0);
|
|
3311
|
+
};
|
|
3312
|
+
let sp=null;
|
|
3313
|
+
for(const sd of ['r','l','b','t']){ const pp=mkLoop(sd); if(!loopHit(pp)){ sp=pp; break; } }
|
|
3314
|
+
if(!sp) sp=mkLoop('r');
|
|
3315
|
+
for(const p of sp){ W=Math.max(W,p[0]+4); Hh=Math.max(Hh,p[1]+16-y0-20); }
|
|
3316
|
+
esvg.push('<path data-edge="'+e.line+'" d="'+roundPath(sp)+'" fill="none" stroke="'+col+'" stroke-width="1.6"'+dash+'/>');
|
|
3317
|
+
noteSegs(e,sp);
|
|
3318
|
+
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});
|
|
3319
|
+
if(e.tail) seg(sp[0],sp[1],0.5,e.tail,10);
|
|
3320
|
+
if(e.head) seg(sp[3],sp[2],0.5,e.head,10);
|
|
3321
|
+
if(wantsStart) arrowTri(sp[0],sp[1],col);
|
|
3322
|
+
if(wantsEnd) arrowTri(sp[sp.length-1],sp[sp.length-2],col);
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3212
3325
|
// back-edge (retry loop): polyline through a side channel beyond the
|
|
3213
3326
|
// occupied lanes instead of a straight line hidden under the spine,
|
|
3214
3327
|
// using the nested slot from the channel plan. Ring-eligible loops
|
|
@@ -3335,6 +3448,50 @@ function renderScene(doc,y0){
|
|
|
3335
3448
|
// their neighbours turns a 40-point staircase into the 2–4 bends a
|
|
3336
3449
|
// dummy-vertex chain should have, without moving the drawn line.
|
|
3337
3450
|
simplifyPts(pts);
|
|
3451
|
+
// Waypoint prune: after collinear simplification a
|
|
3452
|
+
// chain can still carry a staircase of near-collinear jogs — the drift
|
|
3453
|
+
// clamp allows only a few px of sideways movement per rank, so a run
|
|
3454
|
+
// that wants to move 35px sideways alternates short diagonals and
|
|
3455
|
+
// verticals that Douglas-Peucker cannot collapse (each angle differs).
|
|
3456
|
+
// The rule: an interior waypoint SURVIVES only if removing it would
|
|
3457
|
+
// make the edge pierce something. Obstacles are the same set the
|
|
3458
|
+
// straight-edge router uses below: non-incident nodes, plus group
|
|
3459
|
+
// boxes when NEITHER endpoint lies inside (a run through a group
|
|
3460
|
+
// interior asserts membership the figure does not declare). Fixed
|
|
3461
|
+
// left-to-right scan to fixpoint — deterministic. Endpoints are then
|
|
3462
|
+
// re-derived so the edge leaves its node facing the first bend, and
|
|
3463
|
+
// each re-derived terminal segment is re-checked: if it would hit an
|
|
3464
|
+
// obstacle the old endpoint stays.
|
|
3465
|
+
{
|
|
3466
|
+
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}));
|
|
3467
|
+
const ep0=pts[0], ep1=pts[pts.length-1];
|
|
3468
|
+
for(const k in gBox){
|
|
3469
|
+
const b=gBox[k];
|
|
3470
|
+
const inG=(px,py)=>px>b.x0&&px<b.x1&&py>b.yA&&py<b.yB;
|
|
3471
|
+
if(!inG(ep0[0],ep0[1])&&!inG(ep1[0],ep1[1]))
|
|
3472
|
+
pobs.push({x:b.x0,y:b.yA,w:b.x1-b.x0,h:b.yB-b.yA});
|
|
3473
|
+
}
|
|
3474
|
+
let ch=true, pruned=false;
|
|
3475
|
+
while(ch){ ch=false;
|
|
3476
|
+
for(let i=1;i+1<pts.length;i++)
|
|
3477
|
+
if(!segHitsObs(pts[i-1],pts[i+1],pobs)){ pts.splice(i,1); ch=true; pruned=true; i--; }
|
|
3478
|
+
}
|
|
3479
|
+
// untouched chains stay byte-identical: endpoints and the label
|
|
3480
|
+
// segment are only re-derived when the prune removed a waypoint
|
|
3481
|
+
if(pruned){
|
|
3482
|
+
const nb0=borderPoint(A,pts[1][0],pts[1][1]);
|
|
3483
|
+
if(!segHitsObs(nb0,pts[1],pobs)) pts[0]=nb0;
|
|
3484
|
+
const nb1=borderPoint(B,pts[pts.length-2][0],pts[pts.length-2][1]);
|
|
3485
|
+
if(!segHitsObs(nb1,pts[pts.length-2],pobs)) pts[pts.length-1]=nb1;
|
|
3486
|
+
if(midSeg){ let bi=0,bl=-1;
|
|
3487
|
+
for(let i=0;i+1<pts.length;i++){
|
|
3488
|
+
const l=Math.hypot(pts[i+1][0]-pts[i][0],pts[i+1][1]-pts[i][1]);
|
|
3489
|
+
if(l>bl){ bl=l; bi=i; }
|
|
3490
|
+
}
|
|
3491
|
+
midSeg=[pts[bi],pts[bi+1]];
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3338
3495
|
esvg.push('<path data-edge="'+e.line+'" d="'+roundPath(pts)+'" fill="none" stroke="'+col+'" stroke-width="1.6"'+dash+'/>');
|
|
3339
3496
|
noteSegs(e,pts);
|
|
3340
3497
|
if(midSeg) reqLabel({p:midSeg[0],q:midSeg[1],text:e.mid,fs:11,col:lcol,halo,e,A,B,kind:'mid',first:false});
|
|
@@ -3427,7 +3584,7 @@ function renderScene(doc,y0){
|
|
|
3427
3584
|
const CLAMP=t=>Math.max(0.06,Math.min(0.94,t));
|
|
3428
3585
|
const cand=(r,t,side)=>{
|
|
3429
3586
|
const lines=String(r.text).split('\n'), n=lines.length;
|
|
3430
|
-
const w=Math.max(...lines.map(
|
|
3587
|
+
const w=Math.max(...lines.map(cw))*6.5*r.fs/11;
|
|
3431
3588
|
const lh=r.fs*1.3, h=(n-1)*lh+r.fs*1.1;
|
|
3432
3589
|
const up=(n-1)*lh/2+r.fs*0.85; // baseline y = box top + up
|
|
3433
3590
|
const mx=r.p[0]+(r.q[0]-r.p[0])*t, my=r.p[1]+(r.q[1]-r.p[1])*t;
|
|
@@ -3533,7 +3690,7 @@ function renderScene(doc,y0){
|
|
|
3533
3690
|
// label with shrink-to-fit when size is rigid (`UNDECLARED-ATTRIBUTE-BEHAVIOUR`); multi-line via "\n"
|
|
3534
3691
|
let fs=FONT;
|
|
3535
3692
|
const nl=String(n.label).split('\n');
|
|
3536
|
-
const need=Math.max(...nl.map(
|
|
3693
|
+
const need=Math.max(...nl.map(cw))*CH;
|
|
3537
3694
|
// budget = the width the OUTLINE offers at the label's own height, minus
|
|
3538
3695
|
// 8 px clearance each side. For a box that is n.w-16, exactly as before;
|
|
3539
3696
|
// for a rhombus or an ellipse it is the inscribed width, so a rigid
|
|
@@ -4077,7 +4234,7 @@ function renderBitfield(b,y0){
|
|
|
4077
4234
|
const cap=bi===0?String(f.name)+suffix:CONT;
|
|
4078
4235
|
let lead=bx[0]; for(const s of bx) if(s.w>lead.w) lead=s;
|
|
4079
4236
|
const ly=flat ? bx[0].y+bx.length*rh/2 : lead.y+rh/2;
|
|
4080
|
-
let fs=11; const need=cap
|
|
4237
|
+
let fs=11; const need=cw(cap)*6.2;
|
|
4081
4238
|
// The font shrinks to fit the segment that carries it, floor 7px — the
|
|
4082
4239
|
// rule a too-narrow one-row field has always had. Below the floor the
|
|
4083
4240
|
// caption overflows its box rather than vanishing: a name half outside
|
|
@@ -4171,7 +4328,7 @@ function renderBitfield(b,y0){
|
|
|
4171
4328
|
// job.
|
|
4172
4329
|
svg.push('<line x1="'+e.x+'" y1="'+e.y+'" x2="'+e.x+'" y2="'+(e.y+EL_H)+'" stroke="'+st+'" stroke-dasharray="2 3"/>');
|
|
4173
4330
|
svg.push('<line x1="'+(e.x+e.w)+'" y1="'+e.y+'" x2="'+(e.x+e.w)+'" y2="'+(e.y+EL_H)+'" stroke="'+st+'" stroke-dasharray="2 3"/>');
|
|
4174
|
-
let fs=10.5; const need=e.text
|
|
4331
|
+
let fs=10.5; const need=cw(e.text)*6.2;
|
|
4175
4332
|
if(need>e.w-6) fs=Math.max(7,10.5*(e.w-6)/need);
|
|
4176
4333
|
svg.push('<text x="'+(e.x+e.w/2)+'" y="'+(e.y+EL_H/2+fs*0.35)+'" font-size="'+fs+'" text-anchor="middle" fill="#6f6e69">'+esc(e.text)+'</text>');
|
|
4177
4334
|
}
|
|
@@ -4191,7 +4348,7 @@ function renderBitfield(b,y0){
|
|
|
4191
4348
|
for(const f of capt){
|
|
4192
4349
|
const s=f.name+' — present: '+f.present;
|
|
4193
4350
|
svg.push('<text x="0" y="'+(yb+9)+'" font-size="10.5" fill="#6f6e69">'+esc(s)+'</text>');
|
|
4194
|
-
wb=Math.max(wb, s
|
|
4351
|
+
wb=Math.max(wb, cw(s)*6.3+2); yb+=14;
|
|
4195
4352
|
}
|
|
4196
4353
|
yb+=2;
|
|
4197
4354
|
}
|