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
|
@@ -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.8",
|
|
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.8)
|
|
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.8";
|
|
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.8';
|
|
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
|
|
@@ -2358,6 +2358,80 @@ function findComment(s){
|
|
|
2358
2358
|
// 2. LAYOUT + RENDER (deterministic; no randomness, no Date)
|
|
2359
2359
|
// ============================================================
|
|
2360
2360
|
const FONT=13, CH=7.2, PADX=14, NH=36, GAPX=56, GAPY=44;
|
|
2361
|
+
|
|
2362
|
+
// ---- cw(): script-aware advance width --------------------------------
|
|
2363
|
+
// Every px-per-character constant in this file — CH here, and its siblings at
|
|
2364
|
+
// the other font sizes (8.6 title, 6.6 legend, 6.5 edge label, 6.3/6.2 bitfield
|
|
2365
|
+
// caption) — is calibrated on LATIN, where one character advances ~0.554 em.
|
|
2366
|
+
// That is a claim about SCRIPT, not about font: a CJK ideograph advances a full
|
|
2367
|
+
// em, so counting characters undersizes a Chinese label by 45% and the engine
|
|
2368
|
+
// draws the text outside the box it has just sized for it. cw() is the measure
|
|
2369
|
+
// those constants multiply — a string's advance in Latin units. Every codepoint
|
|
2370
|
+
// below U+0300 weighs exactly 1, so an all-ASCII string returns exactly its
|
|
2371
|
+
// .length and every figure that was already right stays byte-identical.
|
|
2372
|
+
//
|
|
2373
|
+
// The classes are Unicode East Asian Width (UAX #11), a fixed character
|
|
2374
|
+
// property: a lookup by codepoint range, deterministic, no font-metrics library
|
|
2375
|
+
// and no canvas measurement at render time. The WEIGHTS are measured, not
|
|
2376
|
+
// chosen — probe strings rendered through librsvg at font-size 13 in the
|
|
2377
|
+
// engine's own stack (font-family="system-ui,sans-serif" → Noto Sans + Noto
|
|
2378
|
+
// Sans CJK), advance recovered as the slope of ink extent over repeat count:
|
|
2379
|
+
// ideograph 封 一 㐀, kana あ ア, bopomofo ㄅ, fullwidth A ,, U+3000 13.000px = 1.000em
|
|
2380
|
+
// emoji U+1F600 U+1F680 (Noto Color Emoji) 16.188px = 1.245em
|
|
2381
|
+
// combining U+0301 standalone, U+200B, U+FEFF 0.000px = 0.000em
|
|
2382
|
+
// Latin — the calibration these constants already carry 7.200px = 0.554em
|
|
2383
|
+
// hence WIDE = 1.000/0.554 = 1.806, rounded UP to 1.81 so a wide glyph is never
|
|
2384
|
+
// UNDER-measured at the node-label site, and EMOJI = 1.245/0.554 = 2.25.
|
|
2385
|
+
// Halfwidth katakana (U+FF61..FF9F) measured 0.500 em: it stays in the default
|
|
2386
|
+
// class, which over-measures it by 11% — the safe direction.
|
|
2387
|
+
//
|
|
2388
|
+
// EAW *Ambiguous* — §, —, –, →, ←, ✓, ≤, •, the only non-ASCII this
|
|
2389
|
+
// repository's figures actually draw — is deliberately NARROW, the
|
|
2390
|
+
// wcwidth/POSIX reading for a non-East-Asian context. Measured, an em dash is
|
|
2391
|
+
// a full em in this font, so a label carrying one is still under-measured by
|
|
2392
|
+
// 0.45 unit; that is a different question from this defect (it needs a
|
|
2393
|
+
// context rule, not a table) and is not decided here.
|
|
2394
|
+
const CW_ZERO=[[0x0300,0x036F],[0x0483,0x0489],[0x0591,0x05BD],[0x0610,0x061A],
|
|
2395
|
+
[0x064B,0x065F],[0x0670,0x0670],[0x06D6,0x06DC],[0x0711,0x0711],[0x0730,0x074A],
|
|
2396
|
+
[0x07A6,0x07B0],[0x0816,0x0819],[0x093C,0x093C],[0x0951,0x0954],[0x0E31,0x0E31],
|
|
2397
|
+
[0x0E34,0x0E3A],[0x0E47,0x0E4E],[0x1AB0,0x1AFF],[0x1DC0,0x1DFF],[0x200B,0x200F],
|
|
2398
|
+
[0x2060,0x2064],[0x20D0,0x20F0],[0xFE00,0xFE0F],[0xFE20,0xFE2F],[0xFEFF,0xFEFF]];
|
|
2399
|
+
const CW_EMOJI=[[0x1F004,0x1F0CF],[0x1F18E,0x1F19A],[0x1F300,0x1F5FF],
|
|
2400
|
+
[0x1F600,0x1F64F],[0x1F680,0x1F6FF],[0x1F7E0,0x1F7EB],[0x1F900,0x1F9FF],
|
|
2401
|
+
[0x1FA70,0x1FAFF]];
|
|
2402
|
+
const CW_WIDE=[[0x1100,0x115F],[0x2E80,0x303E],[0x3041,0x33FF],[0x3400,0x4DBF],
|
|
2403
|
+
[0x4E00,0x9FFF],[0xA000,0xA4CF],[0xA960,0xA97F],[0xAC00,0xD7A3],[0xF900,0xFAFF],
|
|
2404
|
+
[0xFE10,0xFE19],[0xFE30,0xFE6F],[0xFF01,0xFF60],[0xFFE0,0xFFE6],
|
|
2405
|
+
[0x17000,0x18AFF],[0x1B000,0x1B2FF],[0x20000,0x3FFFD]];
|
|
2406
|
+
// ranges are sorted and disjoint, so the scan stops at the first range that
|
|
2407
|
+
// starts above the codepoint
|
|
2408
|
+
function cwIn(cp,rs){
|
|
2409
|
+
for(let i=0;i<rs.length;i++){ if(cp<rs[i][0]) return false; if(cp<=rs[i][1]) return true; }
|
|
2410
|
+
return false;
|
|
2411
|
+
}
|
|
2412
|
+
function cwOf(cp){
|
|
2413
|
+
if(cp<0x0300) return 1; // ASCII and Latin-1: the calibrated case
|
|
2414
|
+
if(cwIn(cp,CW_ZERO)) return 0;
|
|
2415
|
+
if(cwIn(cp,CW_EMOJI)) return 2.25;
|
|
2416
|
+
if(cwIn(cp,CW_WIDE)) return 1.81;
|
|
2417
|
+
return 1;
|
|
2418
|
+
}
|
|
2419
|
+
// Iterate CODEPOINTS, not UTF-16 units: an astral character is one advance,
|
|
2420
|
+
// not two. A ZWJ emoji sequence still counts each codepoint, so it is
|
|
2421
|
+
// over-measured — the safe direction, and the only way to avoid it is a
|
|
2422
|
+
// grapheme segmenter this engine will not carry.
|
|
2423
|
+
function cw(s){
|
|
2424
|
+
s=String(s); let w=0;
|
|
2425
|
+
for(let i=0;i<s.length;i++){
|
|
2426
|
+
const cp=s.codePointAt(i);
|
|
2427
|
+
if(cp>0xFFFF) i++;
|
|
2428
|
+
w+=cwOf(cp);
|
|
2429
|
+
}
|
|
2430
|
+
return w;
|
|
2431
|
+
}
|
|
2432
|
+
// the widest line of a multi-line string, in Latin units
|
|
2433
|
+
function cwMax(s){ return Math.max(...String(s).split('\n').map(cw)); }
|
|
2434
|
+
|
|
2361
2435
|
// §5 style= → SVG dash pattern. `def` is the construct's conventional
|
|
2362
2436
|
// default (the bundle ring and the threshold line are dashed by convention);
|
|
2363
2437
|
// an explicit style= always wins.
|
|
@@ -2367,7 +2441,7 @@ function dashOf(style,def){
|
|
|
2367
2441
|
}
|
|
2368
2442
|
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
2369
2443
|
function tw(s){
|
|
2370
|
-
const longest=
|
|
2444
|
+
const longest=cwMax(s);
|
|
2371
2445
|
return Math.max(30,longest*CH+2*PADX);
|
|
2372
2446
|
}
|
|
2373
2447
|
// ---- shape geometry (one source of truth for size and for clipping) ----
|
|
@@ -2486,7 +2560,7 @@ function inscribedHalfW(g,v){
|
|
|
2486
2560
|
const LBLPADX=6, LBLPADY=4;
|
|
2487
2561
|
function labelBox(label){
|
|
2488
2562
|
const ls=String(label).split('\n');
|
|
2489
|
-
return [Math.max(...ls.map(
|
|
2563
|
+
return [Math.max(...ls.map(cw))*CH+2*LBLPADX,
|
|
2490
2564
|
((ls.length-1)*1.3+1.2)*FONT+2*LBLPADY];
|
|
2491
2565
|
}
|
|
2492
2566
|
// fitOutline: grow the box by the smallest factor that pulls an iw x ih box,
|
|
@@ -2578,7 +2652,7 @@ function render(doc,ropts){
|
|
|
2578
2652
|
}
|
|
2579
2653
|
const parts=[]; let y=0, maxW=0;
|
|
2580
2654
|
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;
|
|
2581
|
-
maxW=Math.max(maxW, doc.title
|
|
2655
|
+
maxW=Math.max(maxW, cw(doc.title)*8.6); } // canvas must fit the title
|
|
2582
2656
|
let sceneMeta=null;
|
|
2583
2657
|
if(doc.nodes.length||doc.edges.length||(doc.boundaries||[]).length){
|
|
2584
2658
|
const s=renderScene(doc,y); parts.push(s.svg); y=s.y; maxW=Math.max(maxW,s.w);
|
|
@@ -2616,7 +2690,7 @@ function render(doc,ropts){
|
|
|
2616
2690
|
// `style`) is drawn, so "declared but not shown" is now unreachable.
|
|
2617
2691
|
for(const c of legendCls){
|
|
2618
2692
|
const paints=c.fill!==undefined||c.stroke!==undefined||c.style!==undefined;
|
|
2619
|
-
const tw=
|
|
2693
|
+
const tw=cw(c.label)*6.6+(paints?30:9);
|
|
2620
2694
|
if(lx>0 && lx+tw>wrapW){ lx=0; ly+=rowH; }
|
|
2621
2695
|
if(paints){
|
|
2622
2696
|
const dash=c.style==='dashed'?' stroke-dasharray="6 4"':(c.style==='dotted'?' stroke-dasharray="2 4"':'');
|
|
@@ -2729,7 +2803,7 @@ function renderScene(doc,y0){
|
|
|
2729
2803
|
// ALL nodes participate in auto layout (pinned ones hold their slot so
|
|
2730
2804
|
// that pinning X never reflows Y); pins override coordinates afterwards.
|
|
2731
2805
|
const horiz=(doc.flow==='right'||doc.flow==='left');
|
|
2732
|
-
const lblPx=s=>
|
|
2806
|
+
const lblPx=s=>cwMax(s)*6.5;
|
|
2733
2807
|
const lay=[...nodes]; // layout participants
|
|
2734
2808
|
const chains=new Map(); // edge -> [A, ...waypoints, B]
|
|
2735
2809
|
for(const e of doc.edges){
|
|
@@ -3112,6 +3186,45 @@ function renderScene(doc,y0){
|
|
|
3112
3186
|
const halo=' paint-order="stroke" stroke="#fff" stroke-width="3"';
|
|
3113
3187
|
const seg=(p,q,t,lbl,fs)=>reqLabel({p,q,t0:t,text:lbl,fs,col:ecol,halo,e,A,B,kind:'end'});
|
|
3114
3188
|
if(isBack.has(e)&&!pinned(e.a)&&!pinned(e.b)){
|
|
3189
|
+
// ── ROUTING-CHANGE ARCHITECTURE NOTE ──────────
|
|
3190
|
+
// Edge labels are DEFERRED: every label is registered against its
|
|
3191
|
+
// FINAL segment geometry (reqLabel/lblReq above) and placed by ONE
|
|
3192
|
+
// greedy pass after all edges are drawn; arrowheads are computed from
|
|
3193
|
+
// final terminal points (arrowTri). Therefore a routing change made
|
|
3194
|
+
// HERE — upstream of reqLabel/noteSegs/arrowTri — gets label and
|
|
3195
|
+
// arrowhead re-placement for free. A change spliced into the emitted
|
|
3196
|
+
// SVG downstream, or made outside the engine, will strand both.
|
|
3197
|
+
// Two prototype rounds re-derived this the hard way (floating
|
|
3198
|
+
// arrowheads, orphaned labels): that was external splicing, not an
|
|
3199
|
+
// engine gap. Patch routing here; do not "fix" the label machinery.
|
|
3200
|
+
if(A===B){
|
|
3201
|
+
// Self-transition: a small side loop on the node, the
|
|
3202
|
+
// convention of every drawing tool — never a lap of the figure
|
|
3203
|
+
// through the back-edge channel. Side order r,l,b,t; first side
|
|
3204
|
+
// whose loop box overlaps no other node wins (deterministic).
|
|
3205
|
+
const scy=A.y+A.h/2, scx=A.x+A.w/2;
|
|
3206
|
+
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]]
|
|
3207
|
+
:sd==='l'?[[A.x,scy-8],[A.x-20,scy-8],[A.x-20,scy+8],[A.x,scy+8]]
|
|
3208
|
+
: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]]
|
|
3209
|
+
:[[scx-8,A.y],[scx-8,A.y-20],[scx+8,A.y-20],[scx+8,A.y]];
|
|
3210
|
+
const loopHit=pp=>{
|
|
3211
|
+
const x0=Math.min(...pp.map(p=>p[0]))-2, x1=Math.max(...pp.map(p=>p[0]))+2;
|
|
3212
|
+
const z0=Math.min(...pp.map(p=>p[1]))-2, z1=Math.max(...pp.map(p=>p[1]))+2;
|
|
3213
|
+
return nodes.some(n=>n!==A&&!n.boundary&&n.x<x1&&n.x+n.w>x0&&n.y<z1&&n.y+n.h>z0);
|
|
3214
|
+
};
|
|
3215
|
+
let sp=null;
|
|
3216
|
+
for(const sd of ['r','l','b','t']){ const pp=mkLoop(sd); if(!loopHit(pp)){ sp=pp; break; } }
|
|
3217
|
+
if(!sp) sp=mkLoop('r');
|
|
3218
|
+
for(const p of sp){ W=Math.max(W,p[0]+4); Hh=Math.max(Hh,p[1]+16-y0-20); }
|
|
3219
|
+
esvg.push('<path data-edge="'+e.line+'" d="'+roundPath(sp)+'" fill="none" stroke="'+col+'" stroke-width="1.6"'+dash+'/>');
|
|
3220
|
+
noteSegs(e,sp);
|
|
3221
|
+
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});
|
|
3222
|
+
if(e.tail) seg(sp[0],sp[1],0.5,e.tail,10);
|
|
3223
|
+
if(e.head) seg(sp[3],sp[2],0.5,e.head,10);
|
|
3224
|
+
if(wantsStart) arrowTri(sp[0],sp[1],col);
|
|
3225
|
+
if(wantsEnd) arrowTri(sp[sp.length-1],sp[sp.length-2],col);
|
|
3226
|
+
continue;
|
|
3227
|
+
}
|
|
3115
3228
|
// back-edge (retry loop): polyline through a side channel beyond the
|
|
3116
3229
|
// occupied lanes instead of a straight line hidden under the spine,
|
|
3117
3230
|
// using the nested slot from the channel plan. Ring-eligible loops
|
|
@@ -3238,6 +3351,50 @@ function renderScene(doc,y0){
|
|
|
3238
3351
|
// their neighbours turns a 40-point staircase into the 2–4 bends a
|
|
3239
3352
|
// dummy-vertex chain should have, without moving the drawn line.
|
|
3240
3353
|
simplifyPts(pts);
|
|
3354
|
+
// Waypoint prune: after collinear simplification a
|
|
3355
|
+
// chain can still carry a staircase of near-collinear jogs — the drift
|
|
3356
|
+
// clamp allows only a few px of sideways movement per rank, so a run
|
|
3357
|
+
// that wants to move 35px sideways alternates short diagonals and
|
|
3358
|
+
// verticals that Douglas-Peucker cannot collapse (each angle differs).
|
|
3359
|
+
// The rule: an interior waypoint SURVIVES only if removing it would
|
|
3360
|
+
// make the edge pierce something. Obstacles are the same set the
|
|
3361
|
+
// straight-edge router uses below: non-incident nodes, plus group
|
|
3362
|
+
// boxes when NEITHER endpoint lies inside (a run through a group
|
|
3363
|
+
// interior asserts membership the figure does not declare). Fixed
|
|
3364
|
+
// left-to-right scan to fixpoint — deterministic. Endpoints are then
|
|
3365
|
+
// re-derived so the edge leaves its node facing the first bend, and
|
|
3366
|
+
// each re-derived terminal segment is re-checked: if it would hit an
|
|
3367
|
+
// obstacle the old endpoint stays.
|
|
3368
|
+
{
|
|
3369
|
+
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}));
|
|
3370
|
+
const ep0=pts[0], ep1=pts[pts.length-1];
|
|
3371
|
+
for(const k in gBox){
|
|
3372
|
+
const b=gBox[k];
|
|
3373
|
+
const inG=(px,py)=>px>b.x0&&px<b.x1&&py>b.yA&&py<b.yB;
|
|
3374
|
+
if(!inG(ep0[0],ep0[1])&&!inG(ep1[0],ep1[1]))
|
|
3375
|
+
pobs.push({x:b.x0,y:b.yA,w:b.x1-b.x0,h:b.yB-b.yA});
|
|
3376
|
+
}
|
|
3377
|
+
let ch=true, pruned=false;
|
|
3378
|
+
while(ch){ ch=false;
|
|
3379
|
+
for(let i=1;i+1<pts.length;i++)
|
|
3380
|
+
if(!segHitsObs(pts[i-1],pts[i+1],pobs)){ pts.splice(i,1); ch=true; pruned=true; i--; }
|
|
3381
|
+
}
|
|
3382
|
+
// untouched chains stay byte-identical: endpoints and the label
|
|
3383
|
+
// segment are only re-derived when the prune removed a waypoint
|
|
3384
|
+
if(pruned){
|
|
3385
|
+
const nb0=borderPoint(A,pts[1][0],pts[1][1]);
|
|
3386
|
+
if(!segHitsObs(nb0,pts[1],pobs)) pts[0]=nb0;
|
|
3387
|
+
const nb1=borderPoint(B,pts[pts.length-2][0],pts[pts.length-2][1]);
|
|
3388
|
+
if(!segHitsObs(nb1,pts[pts.length-2],pobs)) pts[pts.length-1]=nb1;
|
|
3389
|
+
if(midSeg){ let bi=0,bl=-1;
|
|
3390
|
+
for(let i=0;i+1<pts.length;i++){
|
|
3391
|
+
const l=Math.hypot(pts[i+1][0]-pts[i][0],pts[i+1][1]-pts[i][1]);
|
|
3392
|
+
if(l>bl){ bl=l; bi=i; }
|
|
3393
|
+
}
|
|
3394
|
+
midSeg=[pts[bi],pts[bi+1]];
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3241
3398
|
esvg.push('<path data-edge="'+e.line+'" d="'+roundPath(pts)+'" fill="none" stroke="'+col+'" stroke-width="1.6"'+dash+'/>');
|
|
3242
3399
|
noteSegs(e,pts);
|
|
3243
3400
|
if(midSeg) reqLabel({p:midSeg[0],q:midSeg[1],text:e.mid,fs:11,col:lcol,halo,e,A,B,kind:'mid',first:false});
|
|
@@ -3330,7 +3487,7 @@ function renderScene(doc,y0){
|
|
|
3330
3487
|
const CLAMP=t=>Math.max(0.06,Math.min(0.94,t));
|
|
3331
3488
|
const cand=(r,t,side)=>{
|
|
3332
3489
|
const lines=String(r.text).split('\n'), n=lines.length;
|
|
3333
|
-
const w=Math.max(...lines.map(
|
|
3490
|
+
const w=Math.max(...lines.map(cw))*6.5*r.fs/11;
|
|
3334
3491
|
const lh=r.fs*1.3, h=(n-1)*lh+r.fs*1.1;
|
|
3335
3492
|
const up=(n-1)*lh/2+r.fs*0.85; // baseline y = box top + up
|
|
3336
3493
|
const mx=r.p[0]+(r.q[0]-r.p[0])*t, my=r.p[1]+(r.q[1]-r.p[1])*t;
|
|
@@ -3436,7 +3593,7 @@ function renderScene(doc,y0){
|
|
|
3436
3593
|
// label with shrink-to-fit when size is rigid (`UNDECLARED-ATTRIBUTE-BEHAVIOUR`); multi-line via "\n"
|
|
3437
3594
|
let fs=FONT;
|
|
3438
3595
|
const nl=String(n.label).split('\n');
|
|
3439
|
-
const need=Math.max(...nl.map(
|
|
3596
|
+
const need=Math.max(...nl.map(cw))*CH;
|
|
3440
3597
|
// budget = the width the OUTLINE offers at the label's own height, minus
|
|
3441
3598
|
// 8 px clearance each side. For a box that is n.w-16, exactly as before;
|
|
3442
3599
|
// for a rhombus or an ellipse it is the inscribed width, so a rigid
|
|
@@ -3980,7 +4137,7 @@ function renderBitfield(b,y0){
|
|
|
3980
4137
|
const cap=bi===0?String(f.name)+suffix:CONT;
|
|
3981
4138
|
let lead=bx[0]; for(const s of bx) if(s.w>lead.w) lead=s;
|
|
3982
4139
|
const ly=flat ? bx[0].y+bx.length*rh/2 : lead.y+rh/2;
|
|
3983
|
-
let fs=11; const need=cap
|
|
4140
|
+
let fs=11; const need=cw(cap)*6.2;
|
|
3984
4141
|
// The font shrinks to fit the segment that carries it, floor 7px — the
|
|
3985
4142
|
// rule a too-narrow one-row field has always had. Below the floor the
|
|
3986
4143
|
// caption overflows its box rather than vanishing: a name half outside
|
|
@@ -4074,7 +4231,7 @@ function renderBitfield(b,y0){
|
|
|
4074
4231
|
// job.
|
|
4075
4232
|
svg.push('<line x1="'+e.x+'" y1="'+e.y+'" x2="'+e.x+'" y2="'+(e.y+EL_H)+'" stroke="'+st+'" stroke-dasharray="2 3"/>');
|
|
4076
4233
|
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"/>');
|
|
4077
|
-
let fs=10.5; const need=e.text
|
|
4234
|
+
let fs=10.5; const need=cw(e.text)*6.2;
|
|
4078
4235
|
if(need>e.w-6) fs=Math.max(7,10.5*(e.w-6)/need);
|
|
4079
4236
|
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>');
|
|
4080
4237
|
}
|
|
@@ -4094,7 +4251,7 @@ function renderBitfield(b,y0){
|
|
|
4094
4251
|
for(const f of capt){
|
|
4095
4252
|
const s=f.name+' — present: '+f.present;
|
|
4096
4253
|
svg.push('<text x="0" y="'+(yb+9)+'" font-size="10.5" fill="#6f6e69">'+esc(s)+'</text>');
|
|
4097
|
-
wb=Math.max(wb, s
|
|
4254
|
+
wb=Math.max(wb, cw(s)*6.3+2); yb+=14;
|
|
4098
4255
|
}
|
|
4099
4256
|
yb+=2;
|
|
4100
4257
|
}
|
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.8)
|
|
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.8";
|
|
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.8';
|
|
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
|
|
@@ -2350,6 +2350,80 @@ function findComment(s){
|
|
|
2350
2350
|
// 2. LAYOUT + RENDER (deterministic; no randomness, no Date)
|
|
2351
2351
|
// ============================================================
|
|
2352
2352
|
const FONT=13, CH=7.2, PADX=14, NH=36, GAPX=56, GAPY=44;
|
|
2353
|
+
|
|
2354
|
+
// ---- cw(): script-aware advance width --------------------------------
|
|
2355
|
+
// Every px-per-character constant in this file — CH here, and its siblings at
|
|
2356
|
+
// the other font sizes (8.6 title, 6.6 legend, 6.5 edge label, 6.3/6.2 bitfield
|
|
2357
|
+
// caption) — is calibrated on LATIN, where one character advances ~0.554 em.
|
|
2358
|
+
// That is a claim about SCRIPT, not about font: a CJK ideograph advances a full
|
|
2359
|
+
// em, so counting characters undersizes a Chinese label by 45% and the engine
|
|
2360
|
+
// draws the text outside the box it has just sized for it. cw() is the measure
|
|
2361
|
+
// those constants multiply — a string's advance in Latin units. Every codepoint
|
|
2362
|
+
// below U+0300 weighs exactly 1, so an all-ASCII string returns exactly its
|
|
2363
|
+
// .length and every figure that was already right stays byte-identical.
|
|
2364
|
+
//
|
|
2365
|
+
// The classes are Unicode East Asian Width (UAX #11), a fixed character
|
|
2366
|
+
// property: a lookup by codepoint range, deterministic, no font-metrics library
|
|
2367
|
+
// and no canvas measurement at render time. The WEIGHTS are measured, not
|
|
2368
|
+
// chosen — probe strings rendered through librsvg at font-size 13 in the
|
|
2369
|
+
// engine's own stack (font-family="system-ui,sans-serif" → Noto Sans + Noto
|
|
2370
|
+
// Sans CJK), advance recovered as the slope of ink extent over repeat count:
|
|
2371
|
+
// ideograph 封 一 㐀, kana あ ア, bopomofo ㄅ, fullwidth A ,, U+3000 13.000px = 1.000em
|
|
2372
|
+
// emoji U+1F600 U+1F680 (Noto Color Emoji) 16.188px = 1.245em
|
|
2373
|
+
// combining U+0301 standalone, U+200B, U+FEFF 0.000px = 0.000em
|
|
2374
|
+
// Latin — the calibration these constants already carry 7.200px = 0.554em
|
|
2375
|
+
// hence WIDE = 1.000/0.554 = 1.806, rounded UP to 1.81 so a wide glyph is never
|
|
2376
|
+
// UNDER-measured at the node-label site, and EMOJI = 1.245/0.554 = 2.25.
|
|
2377
|
+
// Halfwidth katakana (U+FF61..FF9F) measured 0.500 em: it stays in the default
|
|
2378
|
+
// class, which over-measures it by 11% — the safe direction.
|
|
2379
|
+
//
|
|
2380
|
+
// EAW *Ambiguous* — §, —, –, →, ←, ✓, ≤, •, the only non-ASCII this
|
|
2381
|
+
// repository's figures actually draw — is deliberately NARROW, the
|
|
2382
|
+
// wcwidth/POSIX reading for a non-East-Asian context. Measured, an em dash is
|
|
2383
|
+
// a full em in this font, so a label carrying one is still under-measured by
|
|
2384
|
+
// 0.45 unit; that is a different question from this defect (it needs a
|
|
2385
|
+
// context rule, not a table) and is not decided here.
|
|
2386
|
+
const CW_ZERO=[[0x0300,0x036F],[0x0483,0x0489],[0x0591,0x05BD],[0x0610,0x061A],
|
|
2387
|
+
[0x064B,0x065F],[0x0670,0x0670],[0x06D6,0x06DC],[0x0711,0x0711],[0x0730,0x074A],
|
|
2388
|
+
[0x07A6,0x07B0],[0x0816,0x0819],[0x093C,0x093C],[0x0951,0x0954],[0x0E31,0x0E31],
|
|
2389
|
+
[0x0E34,0x0E3A],[0x0E47,0x0E4E],[0x1AB0,0x1AFF],[0x1DC0,0x1DFF],[0x200B,0x200F],
|
|
2390
|
+
[0x2060,0x2064],[0x20D0,0x20F0],[0xFE00,0xFE0F],[0xFE20,0xFE2F],[0xFEFF,0xFEFF]];
|
|
2391
|
+
const CW_EMOJI=[[0x1F004,0x1F0CF],[0x1F18E,0x1F19A],[0x1F300,0x1F5FF],
|
|
2392
|
+
[0x1F600,0x1F64F],[0x1F680,0x1F6FF],[0x1F7E0,0x1F7EB],[0x1F900,0x1F9FF],
|
|
2393
|
+
[0x1FA70,0x1FAFF]];
|
|
2394
|
+
const CW_WIDE=[[0x1100,0x115F],[0x2E80,0x303E],[0x3041,0x33FF],[0x3400,0x4DBF],
|
|
2395
|
+
[0x4E00,0x9FFF],[0xA000,0xA4CF],[0xA960,0xA97F],[0xAC00,0xD7A3],[0xF900,0xFAFF],
|
|
2396
|
+
[0xFE10,0xFE19],[0xFE30,0xFE6F],[0xFF01,0xFF60],[0xFFE0,0xFFE6],
|
|
2397
|
+
[0x17000,0x18AFF],[0x1B000,0x1B2FF],[0x20000,0x3FFFD]];
|
|
2398
|
+
// ranges are sorted and disjoint, so the scan stops at the first range that
|
|
2399
|
+
// starts above the codepoint
|
|
2400
|
+
function cwIn(cp,rs){
|
|
2401
|
+
for(let i=0;i<rs.length;i++){ if(cp<rs[i][0]) return false; if(cp<=rs[i][1]) return true; }
|
|
2402
|
+
return false;
|
|
2403
|
+
}
|
|
2404
|
+
function cwOf(cp){
|
|
2405
|
+
if(cp<0x0300) return 1; // ASCII and Latin-1: the calibrated case
|
|
2406
|
+
if(cwIn(cp,CW_ZERO)) return 0;
|
|
2407
|
+
if(cwIn(cp,CW_EMOJI)) return 2.25;
|
|
2408
|
+
if(cwIn(cp,CW_WIDE)) return 1.81;
|
|
2409
|
+
return 1;
|
|
2410
|
+
}
|
|
2411
|
+
// Iterate CODEPOINTS, not UTF-16 units: an astral character is one advance,
|
|
2412
|
+
// not two. A ZWJ emoji sequence still counts each codepoint, so it is
|
|
2413
|
+
// over-measured — the safe direction, and the only way to avoid it is a
|
|
2414
|
+
// grapheme segmenter this engine will not carry.
|
|
2415
|
+
function cw(s){
|
|
2416
|
+
s=String(s); let w=0;
|
|
2417
|
+
for(let i=0;i<s.length;i++){
|
|
2418
|
+
const cp=s.codePointAt(i);
|
|
2419
|
+
if(cp>0xFFFF) i++;
|
|
2420
|
+
w+=cwOf(cp);
|
|
2421
|
+
}
|
|
2422
|
+
return w;
|
|
2423
|
+
}
|
|
2424
|
+
// the widest line of a multi-line string, in Latin units
|
|
2425
|
+
function cwMax(s){ return Math.max(...String(s).split('\n').map(cw)); }
|
|
2426
|
+
|
|
2353
2427
|
// §5 style= → SVG dash pattern. `def` is the construct's conventional
|
|
2354
2428
|
// default (the bundle ring and the threshold line are dashed by convention);
|
|
2355
2429
|
// an explicit style= always wins.
|
|
@@ -2359,7 +2433,7 @@ function dashOf(style,def){
|
|
|
2359
2433
|
}
|
|
2360
2434
|
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
2361
2435
|
function tw(s){
|
|
2362
|
-
const longest=
|
|
2436
|
+
const longest=cwMax(s);
|
|
2363
2437
|
return Math.max(30,longest*CH+2*PADX);
|
|
2364
2438
|
}
|
|
2365
2439
|
// ---- shape geometry (one source of truth for size and for clipping) ----
|
|
@@ -2478,7 +2552,7 @@ function inscribedHalfW(g,v){
|
|
|
2478
2552
|
const LBLPADX=6, LBLPADY=4;
|
|
2479
2553
|
function labelBox(label){
|
|
2480
2554
|
const ls=String(label).split('\n');
|
|
2481
|
-
return [Math.max(...ls.map(
|
|
2555
|
+
return [Math.max(...ls.map(cw))*CH+2*LBLPADX,
|
|
2482
2556
|
((ls.length-1)*1.3+1.2)*FONT+2*LBLPADY];
|
|
2483
2557
|
}
|
|
2484
2558
|
// fitOutline: grow the box by the smallest factor that pulls an iw x ih box,
|
|
@@ -2570,7 +2644,7 @@ function render(doc,ropts){
|
|
|
2570
2644
|
}
|
|
2571
2645
|
const parts=[]; let y=0, maxW=0;
|
|
2572
2646
|
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;
|
|
2573
|
-
maxW=Math.max(maxW, doc.title
|
|
2647
|
+
maxW=Math.max(maxW, cw(doc.title)*8.6); } // canvas must fit the title
|
|
2574
2648
|
let sceneMeta=null;
|
|
2575
2649
|
if(doc.nodes.length||doc.edges.length||(doc.boundaries||[]).length){
|
|
2576
2650
|
const s=renderScene(doc,y); parts.push(s.svg); y=s.y; maxW=Math.max(maxW,s.w);
|
|
@@ -2608,7 +2682,7 @@ function render(doc,ropts){
|
|
|
2608
2682
|
// `style`) is drawn, so "declared but not shown" is now unreachable.
|
|
2609
2683
|
for(const c of legendCls){
|
|
2610
2684
|
const paints=c.fill!==undefined||c.stroke!==undefined||c.style!==undefined;
|
|
2611
|
-
const tw=
|
|
2685
|
+
const tw=cw(c.label)*6.6+(paints?30:9);
|
|
2612
2686
|
if(lx>0 && lx+tw>wrapW){ lx=0; ly+=rowH; }
|
|
2613
2687
|
if(paints){
|
|
2614
2688
|
const dash=c.style==='dashed'?' stroke-dasharray="6 4"':(c.style==='dotted'?' stroke-dasharray="2 4"':'');
|
|
@@ -2721,7 +2795,7 @@ function renderScene(doc,y0){
|
|
|
2721
2795
|
// ALL nodes participate in auto layout (pinned ones hold their slot so
|
|
2722
2796
|
// that pinning X never reflows Y); pins override coordinates afterwards.
|
|
2723
2797
|
const horiz=(doc.flow==='right'||doc.flow==='left');
|
|
2724
|
-
const lblPx=s=>
|
|
2798
|
+
const lblPx=s=>cwMax(s)*6.5;
|
|
2725
2799
|
const lay=[...nodes]; // layout participants
|
|
2726
2800
|
const chains=new Map(); // edge -> [A, ...waypoints, B]
|
|
2727
2801
|
for(const e of doc.edges){
|
|
@@ -3104,6 +3178,45 @@ function renderScene(doc,y0){
|
|
|
3104
3178
|
const halo=' paint-order="stroke" stroke="#fff" stroke-width="3"';
|
|
3105
3179
|
const seg=(p,q,t,lbl,fs)=>reqLabel({p,q,t0:t,text:lbl,fs,col:ecol,halo,e,A,B,kind:'end'});
|
|
3106
3180
|
if(isBack.has(e)&&!pinned(e.a)&&!pinned(e.b)){
|
|
3181
|
+
// ── ROUTING-CHANGE ARCHITECTURE NOTE ──────────
|
|
3182
|
+
// Edge labels are DEFERRED: every label is registered against its
|
|
3183
|
+
// FINAL segment geometry (reqLabel/lblReq above) and placed by ONE
|
|
3184
|
+
// greedy pass after all edges are drawn; arrowheads are computed from
|
|
3185
|
+
// final terminal points (arrowTri). Therefore a routing change made
|
|
3186
|
+
// HERE — upstream of reqLabel/noteSegs/arrowTri — gets label and
|
|
3187
|
+
// arrowhead re-placement for free. A change spliced into the emitted
|
|
3188
|
+
// SVG downstream, or made outside the engine, will strand both.
|
|
3189
|
+
// Two prototype rounds re-derived this the hard way (floating
|
|
3190
|
+
// arrowheads, orphaned labels): that was external splicing, not an
|
|
3191
|
+
// engine gap. Patch routing here; do not "fix" the label machinery.
|
|
3192
|
+
if(A===B){
|
|
3193
|
+
// Self-transition: a small side loop on the node, the
|
|
3194
|
+
// convention of every drawing tool — never a lap of the figure
|
|
3195
|
+
// through the back-edge channel. Side order r,l,b,t; first side
|
|
3196
|
+
// whose loop box overlaps no other node wins (deterministic).
|
|
3197
|
+
const scy=A.y+A.h/2, scx=A.x+A.w/2;
|
|
3198
|
+
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]]
|
|
3199
|
+
:sd==='l'?[[A.x,scy-8],[A.x-20,scy-8],[A.x-20,scy+8],[A.x,scy+8]]
|
|
3200
|
+
: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]]
|
|
3201
|
+
:[[scx-8,A.y],[scx-8,A.y-20],[scx+8,A.y-20],[scx+8,A.y]];
|
|
3202
|
+
const loopHit=pp=>{
|
|
3203
|
+
const x0=Math.min(...pp.map(p=>p[0]))-2, x1=Math.max(...pp.map(p=>p[0]))+2;
|
|
3204
|
+
const z0=Math.min(...pp.map(p=>p[1]))-2, z1=Math.max(...pp.map(p=>p[1]))+2;
|
|
3205
|
+
return nodes.some(n=>n!==A&&!n.boundary&&n.x<x1&&n.x+n.w>x0&&n.y<z1&&n.y+n.h>z0);
|
|
3206
|
+
};
|
|
3207
|
+
let sp=null;
|
|
3208
|
+
for(const sd of ['r','l','b','t']){ const pp=mkLoop(sd); if(!loopHit(pp)){ sp=pp; break; } }
|
|
3209
|
+
if(!sp) sp=mkLoop('r');
|
|
3210
|
+
for(const p of sp){ W=Math.max(W,p[0]+4); Hh=Math.max(Hh,p[1]+16-y0-20); }
|
|
3211
|
+
esvg.push('<path data-edge="'+e.line+'" d="'+roundPath(sp)+'" fill="none" stroke="'+col+'" stroke-width="1.6"'+dash+'/>');
|
|
3212
|
+
noteSegs(e,sp);
|
|
3213
|
+
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});
|
|
3214
|
+
if(e.tail) seg(sp[0],sp[1],0.5,e.tail,10);
|
|
3215
|
+
if(e.head) seg(sp[3],sp[2],0.5,e.head,10);
|
|
3216
|
+
if(wantsStart) arrowTri(sp[0],sp[1],col);
|
|
3217
|
+
if(wantsEnd) arrowTri(sp[sp.length-1],sp[sp.length-2],col);
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3107
3220
|
// back-edge (retry loop): polyline through a side channel beyond the
|
|
3108
3221
|
// occupied lanes instead of a straight line hidden under the spine,
|
|
3109
3222
|
// using the nested slot from the channel plan. Ring-eligible loops
|
|
@@ -3230,6 +3343,50 @@ function renderScene(doc,y0){
|
|
|
3230
3343
|
// their neighbours turns a 40-point staircase into the 2–4 bends a
|
|
3231
3344
|
// dummy-vertex chain should have, without moving the drawn line.
|
|
3232
3345
|
simplifyPts(pts);
|
|
3346
|
+
// Waypoint prune: after collinear simplification a
|
|
3347
|
+
// chain can still carry a staircase of near-collinear jogs — the drift
|
|
3348
|
+
// clamp allows only a few px of sideways movement per rank, so a run
|
|
3349
|
+
// that wants to move 35px sideways alternates short diagonals and
|
|
3350
|
+
// verticals that Douglas-Peucker cannot collapse (each angle differs).
|
|
3351
|
+
// The rule: an interior waypoint SURVIVES only if removing it would
|
|
3352
|
+
// make the edge pierce something. Obstacles are the same set the
|
|
3353
|
+
// straight-edge router uses below: non-incident nodes, plus group
|
|
3354
|
+
// boxes when NEITHER endpoint lies inside (a run through a group
|
|
3355
|
+
// interior asserts membership the figure does not declare). Fixed
|
|
3356
|
+
// left-to-right scan to fixpoint — deterministic. Endpoints are then
|
|
3357
|
+
// re-derived so the edge leaves its node facing the first bend, and
|
|
3358
|
+
// each re-derived terminal segment is re-checked: if it would hit an
|
|
3359
|
+
// obstacle the old endpoint stays.
|
|
3360
|
+
{
|
|
3361
|
+
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}));
|
|
3362
|
+
const ep0=pts[0], ep1=pts[pts.length-1];
|
|
3363
|
+
for(const k in gBox){
|
|
3364
|
+
const b=gBox[k];
|
|
3365
|
+
const inG=(px,py)=>px>b.x0&&px<b.x1&&py>b.yA&&py<b.yB;
|
|
3366
|
+
if(!inG(ep0[0],ep0[1])&&!inG(ep1[0],ep1[1]))
|
|
3367
|
+
pobs.push({x:b.x0,y:b.yA,w:b.x1-b.x0,h:b.yB-b.yA});
|
|
3368
|
+
}
|
|
3369
|
+
let ch=true, pruned=false;
|
|
3370
|
+
while(ch){ ch=false;
|
|
3371
|
+
for(let i=1;i+1<pts.length;i++)
|
|
3372
|
+
if(!segHitsObs(pts[i-1],pts[i+1],pobs)){ pts.splice(i,1); ch=true; pruned=true; i--; }
|
|
3373
|
+
}
|
|
3374
|
+
// untouched chains stay byte-identical: endpoints and the label
|
|
3375
|
+
// segment are only re-derived when the prune removed a waypoint
|
|
3376
|
+
if(pruned){
|
|
3377
|
+
const nb0=borderPoint(A,pts[1][0],pts[1][1]);
|
|
3378
|
+
if(!segHitsObs(nb0,pts[1],pobs)) pts[0]=nb0;
|
|
3379
|
+
const nb1=borderPoint(B,pts[pts.length-2][0],pts[pts.length-2][1]);
|
|
3380
|
+
if(!segHitsObs(nb1,pts[pts.length-2],pobs)) pts[pts.length-1]=nb1;
|
|
3381
|
+
if(midSeg){ let bi=0,bl=-1;
|
|
3382
|
+
for(let i=0;i+1<pts.length;i++){
|
|
3383
|
+
const l=Math.hypot(pts[i+1][0]-pts[i][0],pts[i+1][1]-pts[i][1]);
|
|
3384
|
+
if(l>bl){ bl=l; bi=i; }
|
|
3385
|
+
}
|
|
3386
|
+
midSeg=[pts[bi],pts[bi+1]];
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3233
3390
|
esvg.push('<path data-edge="'+e.line+'" d="'+roundPath(pts)+'" fill="none" stroke="'+col+'" stroke-width="1.6"'+dash+'/>');
|
|
3234
3391
|
noteSegs(e,pts);
|
|
3235
3392
|
if(midSeg) reqLabel({p:midSeg[0],q:midSeg[1],text:e.mid,fs:11,col:lcol,halo,e,A,B,kind:'mid',first:false});
|
|
@@ -3322,7 +3479,7 @@ function renderScene(doc,y0){
|
|
|
3322
3479
|
const CLAMP=t=>Math.max(0.06,Math.min(0.94,t));
|
|
3323
3480
|
const cand=(r,t,side)=>{
|
|
3324
3481
|
const lines=String(r.text).split('\n'), n=lines.length;
|
|
3325
|
-
const w=Math.max(...lines.map(
|
|
3482
|
+
const w=Math.max(...lines.map(cw))*6.5*r.fs/11;
|
|
3326
3483
|
const lh=r.fs*1.3, h=(n-1)*lh+r.fs*1.1;
|
|
3327
3484
|
const up=(n-1)*lh/2+r.fs*0.85; // baseline y = box top + up
|
|
3328
3485
|
const mx=r.p[0]+(r.q[0]-r.p[0])*t, my=r.p[1]+(r.q[1]-r.p[1])*t;
|
|
@@ -3428,7 +3585,7 @@ function renderScene(doc,y0){
|
|
|
3428
3585
|
// label with shrink-to-fit when size is rigid (`UNDECLARED-ATTRIBUTE-BEHAVIOUR`); multi-line via "\n"
|
|
3429
3586
|
let fs=FONT;
|
|
3430
3587
|
const nl=String(n.label).split('\n');
|
|
3431
|
-
const need=Math.max(...nl.map(
|
|
3588
|
+
const need=Math.max(...nl.map(cw))*CH;
|
|
3432
3589
|
// budget = the width the OUTLINE offers at the label's own height, minus
|
|
3433
3590
|
// 8 px clearance each side. For a box that is n.w-16, exactly as before;
|
|
3434
3591
|
// for a rhombus or an ellipse it is the inscribed width, so a rigid
|
|
@@ -3972,7 +4129,7 @@ function renderBitfield(b,y0){
|
|
|
3972
4129
|
const cap=bi===0?String(f.name)+suffix:CONT;
|
|
3973
4130
|
let lead=bx[0]; for(const s of bx) if(s.w>lead.w) lead=s;
|
|
3974
4131
|
const ly=flat ? bx[0].y+bx.length*rh/2 : lead.y+rh/2;
|
|
3975
|
-
let fs=11; const need=cap
|
|
4132
|
+
let fs=11; const need=cw(cap)*6.2;
|
|
3976
4133
|
// The font shrinks to fit the segment that carries it, floor 7px — the
|
|
3977
4134
|
// rule a too-narrow one-row field has always had. Below the floor the
|
|
3978
4135
|
// caption overflows its box rather than vanishing: a name half outside
|
|
@@ -4066,7 +4223,7 @@ function renderBitfield(b,y0){
|
|
|
4066
4223
|
// job.
|
|
4067
4224
|
svg.push('<line x1="'+e.x+'" y1="'+e.y+'" x2="'+e.x+'" y2="'+(e.y+EL_H)+'" stroke="'+st+'" stroke-dasharray="2 3"/>');
|
|
4068
4225
|
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"/>');
|
|
4069
|
-
let fs=10.5; const need=e.text
|
|
4226
|
+
let fs=10.5; const need=cw(e.text)*6.2;
|
|
4070
4227
|
if(need>e.w-6) fs=Math.max(7,10.5*(e.w-6)/need);
|
|
4071
4228
|
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>');
|
|
4072
4229
|
}
|
|
@@ -4086,7 +4243,7 @@ function renderBitfield(b,y0){
|
|
|
4086
4243
|
for(const f of capt){
|
|
4087
4244
|
const s=f.name+' — present: '+f.present;
|
|
4088
4245
|
svg.push('<text x="0" y="'+(yb+9)+'" font-size="10.5" fill="#6f6e69">'+esc(s)+'</text>');
|
|
4089
|
-
wb=Math.max(wb, s
|
|
4246
|
+
wb=Math.max(wb, cw(s)*6.3+2); yb+=14;
|
|
4090
4247
|
}
|
|
4091
4248
|
yb+=2;
|
|
4092
4249
|
}
|