nx_font.nx source
↩ module page · 1588 lines · 83266 B
1// nx_font.nx -- RUNG 4 of the sovereign font BUILDER: the FONT ENGINE. A char -> its stroke-composition (in
2// 0..1000 em) + advance width; `font_text` lays out a string (pen advances per glyph). This is the table-driven
3// layer the browser calls. Glyphs are stroke-compositions (rung 1-3); growing the set = DATA. Subset here
4// (the letters for a demo phrase); the alphabet + kanji fill in the same way. ORIGINAL.
5import "nx_glyph_stroke.nx"
6import "nx_aa_raster.nx"
7import "nx_utf8.nx" // BR30: the ONE UTF-8 decoder (utf8_decode_one) -- a face measures CODEPOINTS
8
9// ---- ★FONT SPEC (the GENERATOR knobs; Metafont-style parameters -- one glyph program, many fonts) ----
10// 0 = pioneer default. Set via font_spec() BEFORE rendering; every glyph re-derives from these:
11// fs_w pen weight em (default 132 = Regular; ~95 Light, ~175 Bold -> OpenType wght)
12// fs_over overshoot em (default 12: how far round ink kisses past the metric lines)
13// fs_passes Chaikin passes (default 1 = crisp grotesque; 2 = softer/rounder)
14// fs_caps terminal style (default 0 = butt/flat = professional; 1 = round = soft/marker style)
15// The metric lines are FUNCTIONS of these (XT/XB/SH/BB/DB/CT in font_emit), so alignment survives any knob
16// change BY CONSTRUCTION -- the property nx_font_family_gate proves mechanically.
17static fs_w: i64
18static fs_over: i64
19static fs_passes: i64
20static fs_caps: i64
21static fs_mod: i64 // stroke MODULATION percent: horizontal pen = vertical pen * mod/100 (0 -> default 88 = grotesque contrast; 100 = uniform/monoline)
22static fs_swell: i64 // WALL SWELL percent: interior CURVE walls = pen * swell/100 (0 -> default 126; 100 = no swell). Grotesques swell round walls over stems (Arial ~130).
23static fs_slant: i64 // OBLIQUE slant percent (the OpenType slnt axis): x += (baseline - y) * slant/100, sheared
24 // ABOUT THE BASELINE so alignment survives BY CONSTRUCTION. 0 = upright; ~18 = 10deg oblique.
25static fs_geo: i64 // GEOMETRIC construction: 1 = machine-regular letterforms from exact shared primitives.
26static fs_wdth: i64 // WIDTH axis (the OpenType wdth): scales centreline x-spacing (stems keep their width ->
27 // TRUE condense/extend, not squish). 100 = normal; ~80 = condensed; ~125 = extended.
28func font_spec(w: i64, over: i64, passes: i64, caps: i64) -> i64 { fs_w=w; fs_over=over; fs_passes=passes; fs_caps=caps; return 0 }
29func font_spec_mod(m: i64) -> i64 { fs_mod=m; return 0 }
30func font_spec_swell(s: i64) -> i64 { fs_swell=s; return 0 }
31func font_spec_slant(s: i64) -> i64 { fs_slant=s; return 0 }
32func font_spec_reset() -> i64 { fs_w=0; fs_over=0; fs_passes=0; fs_caps=0; fs_mod=0; fs_swell=0; fs_slant=0; fs_geo=0; fs_wdth=0; return 0 }
33func font_mod() -> i64 { if fs_mod > 0 { return fs_mod } return 88 }
34func font_swell() -> i64 { if fs_swell > 0 { return fs_swell } return 126 }
35func font_slant() -> i64 { return fs_slant }
36func font_spec_geo(g: i64) -> i64 { fs_geo=g; return 0 }
37func font_spec_wdth(w: i64) -> i64 { fs_wdth=w; return 0 }
38func font_wdth() -> i64 { if fs_wdth <= 0 { return 100 } return fs_wdth }
39// ---- REAL FACE HOOK (2026-09-02, BR4 first half) --------------------------------------------------
40// A loaded TrueType face supplies 1000-em advances (and pair kerning) for the bytes it covers; the stroke
41// font's own tables answer for everything else. This is the ONE measure ruler: font_adv_em reads the
42// table, so layout (font_vec_*), the wrap scan and the paint pen all move together BY CONSTRUCTION.
43// The tables are OWNED by the render layer (which imports the TTF engine); this file stays engine-free.
44static fs_face_adv: i64 // *i64[FS_FACE_CPS]: 1000-em advance by CODEPOINT of the SELECTED face, 0 = no face / codepoint not covered
45static fs_face_kern: i64 // *i64[FS_FACE_BYTES*FS_FACE_BYTES]: 1000-em pair kern of the SELECTED face at prev*256+ch (ASCII pairs), 0 = none
46const FS_FACE_BYTES: i64 = 256
47// BR30 (2026-09-02): the advance table is indexed by CODEPOINT, not byte. FS_FACE_CPS spans U+0000..U+21FF:
48// Latin-1 and Extended, IPA, combining marks, Greek, Cyrillic, Hebrew, Arabic, General Punctuation (curly quotes, dashes,
49// ellipsis), superscripts, currency (euro), letterlike (trademark), number forms and arrows -- 8 faces x 68 KB of tables.
50// A codepoint at or beyond it is UNCOVERED and folds to ASCII exactly as a codepoint the face lacks does.
51const FS_FACE_CPS: i64 = 8704
52const FS_FACE_LATIN_END: i64 = 592 // U+0250: end of Latin Extended-B; below it an uncovered codepoint folds to its base letter
53static fs_cp_out: i64 // *i64[1] scratch for the decoded codepoint (single-threaded measure)
54static fs_cp_pos: i64 // *i64[1] scratch for utf8_decode_one's cursor
55// ---- FACE REGISTRY (BR27, 2026-09-02): up to FS_FACE_MAX faces, selected per text run by the ONE rule
56// font_face_pick(bold, italic, serif) that layout and paint both call, so a run is measured and painted
57// with the same face by construction. Slot 0 is the default (sans regular); an unregistered slot falls
58// back to slot 0, never to the stroke font, so a page never mixes engines inside one run.
59const FS_FACE_MAX: i64 = 8
60const FS_FACE_BOLD: i64 = 1
61const FS_FACE_ITALIC: i64 = 2
62const FS_FACE_SERIF: i64 = 4
63static fs_faces_adv: i64 // *i64[FS_FACE_MAX] of advance-table pointers (0 = slot empty)
64static fs_faces_kern: i64 // *i64[FS_FACE_MAX] of kern-table pointers
65static fs_face_cur: i64 // the selected slot (effective, after fallback)
66func font_face_pick(bold: i64, italic: i64, serif: i64) -> i64 {
67 var idx: i64 = 0
68 if bold == 1 { idx = idx + FS_FACE_BOLD }
69 if italic == 1 { idx = idx + FS_FACE_ITALIC }
70 if serif == 1 { idx = idx + FS_FACE_SERIF }
71 return idx
72}
73func font_face_register(idx: i64, adv: *i64, kern: *i64) -> i64 {
74 if idx < 0 { return 0 }
75 if idx >= FS_FACE_MAX { return 0 }
76 if fs_faces_adv == 0 { fs_faces_adv = sys_mmap(8*FS_FACE_MAX) as i64; fs_faces_kern = sys_mmap(8*FS_FACE_MAX) as i64 }
77 let fa: *i64 = fs_faces_adv as *i64
78 let fk: *i64 = fs_faces_kern as *i64
79 fa[idx] = adv as i64
80 fk[idx] = kern as i64
81 if idx == 0 { fs_face_adv = adv as i64; fs_face_kern = kern as i64; fs_face_cur = 0 }
82 return 1
83}
84// select the face for the run about to be measured or painted; returns the EFFECTIVE slot
85func font_face_select(idx: i64) -> i64 {
86 if fs_faces_adv == 0 { return 0 }
87 let fa: *i64 = fs_faces_adv as *i64
88 let fk: *i64 = fs_faces_kern as *i64
89 var e: i64 = idx
90 if e < 0 { e = 0 }
91 if e >= FS_FACE_MAX { e = 0 }
92 if fa[e] == 0 { e = 0 }
93 fs_face_adv = fa[e]
94 fs_face_kern = fk[e]
95 fs_face_cur = e
96 return e
97}
98func font_face_cur() -> i64 { return fs_face_cur }
99// compatibility entry: ONE face = slot 0 selected; (0, 0) clears the whole registry
100func font_set_face(adv: *i64, kern: *i64) -> i64 {
101 if (adv as i64) == 0 {
102 if fs_faces_adv != 0 { let fa: *i64 = fs_faces_adv as *i64; let fk: *i64 = fs_faces_kern as *i64; var i: i64 = 0; while i < FS_FACE_MAX { fa[i] = 0; fk[i] = 0; i = i + 1 } }
103 fs_face_adv = 0; fs_face_kern = 0; fs_face_cur = 0
104 return 0
105 }
106 font_face_register(0, adv, kern)
107 font_face_select(0)
108 return 0
109}
110func font_face_on() -> i64 { if fs_face_adv != 0 { return 1 } return 0 }
111func fs_face_adv_ptr() -> *i64 { return fs_face_adv as *i64 } // BR39: the render's atlas key needs to see an in-table zero
112func font_face_has(ch: i64) -> i64 {
113 if fs_face_adv == 0 { return 0 }
114 if ch < 0 { return 0 }
115 let t: *i64 = fs_face_adv as *i64
116 // BR39: slot 0 holds the face NOTDEF advance; a codepoint beyond the table is COVERED by the notdef
117 // box whenever the face registered one (measure uses t[0], paint draws gid 0), so foreign scripts
118 // occupy their real width instead of vanishing. Inside the table an unmapped codepoint still folds.
119 if ch >= FS_FACE_CPS { if t[0] > 0 { return 1 } return 0 }
120 if t[ch] > 0 { return 1 }
121 // BR39: an in-table codepoint the face lacks is FOLDED when it is Latin (a base letter reads better than a
122 // box: e-acute -> e), and drawn as the notdef box when it is not (Greek, Cyrillic, symbols, arrows have no
123 // honest ASCII fold; vanishing is the dishonest option). FS_FACE_LATIN_END is the first non-Latin block.
124 if ch >= FS_FACE_LATIN_END { if t[0] > 0 { return 1 } }
125 return 0
126}
127// BR30: decode ONE codepoint at text[i] into cp[0] and return the index after it. A malformed or
128// truncated sequence yields the lead BYTE (>= 128), which no face table covers, so the run folds.
129func font_next_cp(text: *u8, i: i64, len: i64, cp: *i64) -> i64 {
130 let b0: i64 = (text[i] as i64) & 255
131 if b0 < 128 { cp[0] = b0; return i + 1 }
132 if fs_cp_pos == 0 { fs_cp_pos = sys_mmap(8) as i64 }
133 let pos: *i64 = fs_cp_pos as *i64
134 pos[0] = i
135 let v: i64 = utf8_decode_one(text, len, pos)
136 if v < 0 { cp[0] = b0; return i + 1 }
137 cp[0] = v
138 return pos[0]
139}
140func font_cp_scratch() -> *i64 { if fs_cp_out == 0 { fs_cp_out = sys_mmap(8) as i64 } return fs_cp_out as *i64 }
141func font_run_has_face(text: *u8, len: i64) -> i64 {
142 let cpo: *i64 = font_cp_scratch()
143 var i: i64 = 0
144 while i < len { i = font_next_cp(text, i, len, cpo); if font_face_has(cpo[0]) == 0 { return 0 } }
145 return 1
146}
147// THE ONE run test both stages use: with a face, every byte the face covers; else the extended stroke
148// set. Layout decides vec-measure with it and paint decides vec-paint with it, so a run can never be
149// measured by one font and painted by another.
150func font_run_vec_ok(text: *u8, len: i64) -> i64 {
151 if fs_face_adv != 0 { return font_run_has_face(text, len) }
152 return font_run_has_ext(text, len)
153}
154// pair kerning in 1000-em (negative pulls the pair closer); 0 without a face, outside its byte range,
155// or at a run start (prev < 0)
156func font_pair_kern_em(prev: i64, ch: i64) -> i64 {
157 if fs_face_kern == 0 { return 0 }
158 if prev < 0 { return 0 }
159 if prev >= FS_FACE_BYTES { return 0 }
160 if ch < 0 { return 0 }
161 if ch >= FS_FACE_BYTES { return 0 }
162 let k: *i64 = fs_face_kern as *i64
163 return k[prev*FS_FACE_BYTES + ch]
164}
165// effective pen weight (em). fs_w overrides. GEOMETRIC defaults LIGHTER (85 vs 123) -- measured vs the pros:
166// our old 123 gave a 7px stem at 64px where Century Gothic=4, Segoe/Arial=5 -> ours read chunky/"meh". 85
167// lands a ~5px stem = the refined professional geometric weight. Applies to geo letters AND the i/j/punct
168// that fall through to the organic path, so the whole geometric face is one consistent colour.
169func font_weight() -> i64 { if fs_w > 0 { return fs_w } if fs_geo == 1 { return 91 } return 123 }
170func font_geo() -> i64 { return fs_geo }
171
172// ★GEOMETRIC ARC primitive: a polyline sampling the 16-gon ring from index k0 to k1 (wraps if k1<k0), scaled
173// rx/ry about (cx,cy). EXACT circles/arcs -> bowls and arches are identical by construction (machine regular).
174// Downstream Chaikin + variable-width outline smooth it. Needs the trig table (cs/sn) filled.
175func geo_arc(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
176 cx: i64, cy: i64, r: i64, krange: i64, lw: i64, ox: i64, oy: i64, S: i64) -> i64 {
177 let k0: i64 = krange / 100
178 let k1: i64 = krange % 100
179 let ax: *i64 = sys_mmap(8*40) as *i64
180 let ay: *i64 = sys_mmap(8*40) as *i64
181 var kend: i64 = k1
182 if kend < k0 { kend = k1 + 16 }
183 var m: i64 = 0
184 var kk: i64 = k0
185 while kk <= kend {
186 let idx: i64 = kk % 16
187 ax[m] = cx + (r * cs[idx]) / 1000
188 ay[m] = cy + (r * sn[idx]) / 1000
189 m = m + 1
190 kk = kk + 1
191 }
192 fe_stroke(xs, ys, cstart, clen, np, nc, cs, sn, ax, ay, m, lw, ox, oy, S)
193 return 0
194}
195
196// ★ELLIPTICAL arc: like geo_arc but independent rx/ry (for m's narrow-but-full-height humps, g's tail).
197// klw packs krange + lw*100000 (16-arg cap): lw = klw/100000, krange = klw%100000, k0=krange/100, k1=krange%100.
198func geo_earc(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
199 cx: i64, cy: i64, rx: i64, ry: i64, klw: i64, ox: i64, oy: i64, S: i64) -> i64 {
200 let lw: i64 = klw / 100000
201 let krange: i64 = klw % 100000
202 let k0: i64 = krange / 100
203 let k1: i64 = krange % 100
204 let ax: *i64 = sys_mmap(8*40) as *i64
205 let ay: *i64 = sys_mmap(8*40) as *i64
206 var kend: i64 = k1
207 if kend < k0 { kend = k1 + 16 }
208 var m: i64 = 0
209 var kk: i64 = k0
210 while kk <= kend {
211 let idx: i64 = kk % 16
212 ax[m] = cx + (rx * cs[idx]) / 1000
213 ay[m] = cy + (ry * sn[idx]) / 1000
214 m = m + 1
215 kk = kk + 1
216 }
217 fe_stroke(xs, ys, cstart, clen, np, nc, cs, sn, ax, ay, m, lw, ox, oy, S)
218 return 0
219}
220// ★WIDTH COMPENSATION, percent (the wght-width interaction every real family has): heavier pens need WIDER
221// letterforms or the counters CLOG (the Bold 'e' filled in -- caught by the family-sheet eyeball). Pure
222// x-scale about 0 applied to glyph geometry AND advances together, so set-width and ink stay proportional:
223// Bold(175) -> +8%, Regular(132) -> 100 (pioneer byte-identical), Light(95) -> -7% (sets a touch tighter).
224func font_ws() -> i64 {
225 var w: i64 = 123 // anchored at the Regular pen (isolated-stem-tuned)
226 if fs_w > 0 { w = fs_w }
227 return 100 + (w - 123) / 5
228}
229
230// stroke an em-space polyline (lx/ly in 0..1000) at pen (ox,oy) grid, scale S (= px*ss), width lw(em).
231// ★CHAIKIN CORNER-CUTTING (fs_passes iterations) smooths faceted polyline curves into true smooth curves
232// before stroking -- kills the "hand-drawn kink" tell on bowls (e/o/a/R/s/g...). Open-polyline variant KEEPS
233// the endpoints (stroke starts/ends stay put); 2-point stems (no interior corner) pass through straight.
234func fe_chaikin(sx: *i64, sy: *i64, n: i64, dx: *i64, dy: *i64) -> i64 {
235 if n < 3 { var k: i64=0; while k<n { dx[k]=sx[k]; dy[k]=sy[k]; k=k+1 } return n }
236 dx[0]=sx[0]; dy[0]=sy[0]
237 var m: i64 = 1
238 var i: i64 = 0
239 while i < n-1 {
240 dx[m] = (3*sx[i] + sx[i+1])/4; dy[m] = (3*sy[i] + sy[i+1])/4; m = m + 1
241 dx[m] = (sx[i] + 3*sx[i+1])/4; dy[m] = (sy[i] + 3*sy[i+1])/4; m = m + 1
242 i = i + 1
243 }
244 dx[m]=sx[n-1]; dy[m]=sy[n-1]; m = m + 1
245 return m
246}
247// ★CORNER-PRESERVING Chaikin with per-output-segment SOURCE tags. Chaikin smooths CURVES; a sharp turn
248// (>~60deg: V/W/A apexes, z corners, k joints) is a CORNER and must stay a corner -- plain corner-cutting
249// FLATTENED the V apex at 25% of its arm length (the truncated-V bug). Interior vertices whose turn has
250// cos(theta) < 500 (x1000) are re-emitted EXACTLY between their two cut points. stag_in = per-input-segment
251// swell tags; stag_out = per-OUTPUT-segment tags (junction segments take the max of the two sources).
252func fe_chaikin_cp(sx: *i64, sy: *i64, stag: *i64, n: i64, dx: *i64, dy: *i64, dtag: *i64) -> i64 {
253 if n < 3 {
254 var k: i64=0
255 while k<n { dx[k]=sx[k]; dy[k]=sy[k]; k=k+1 }
256 if n >= 2 { dtag[0]=stag[0] }
257 return n
258 }
259 dx[0]=sx[0]; dy[0]=sy[0]
260 var m: i64 = 1
261 var i: i64 = 0
262 while i < n-1 {
263 // cut pair inside input segment i; the out-segment ENTERING each new point carries a source tag
264 dx[m] = (3*sx[i] + sx[i+1])/4; dy[m] = (3*sy[i] + sy[i+1])/4
265 var tprev: i64 = stag[i]
266 if i > 0 { if stag[i-1] > tprev { tprev = stag[i-1] } } // junction from the previous segment
267 dtag[m-1] = tprev
268 m = m + 1
269 dx[m] = (sx[i] + 3*sx[i+1])/4; dy[m] = (sy[i] + 3*sy[i+1])/4
270 dtag[m-1] = stag[i]
271 m = m + 1
272 // corner preservation: if vertex i+1 turns sharply, re-emit it exactly
273 if i < n-2 {
274 let d1x: i64 = sx[i+1]-sx[i]
275 let d1y: i64 = sy[i+1]-sy[i]
276 let d2x: i64 = sx[i+2]-sx[i+1]
277 let d2y: i64 = sy[i+2]-sy[i+1]
278 let l1: i64 = gs_isqrt(d1x*d1x+d1y*d1y)
279 let l2: i64 = gs_isqrt(d2x*d2x+d2y*d2y)
280 if l1 > 0 { if l2 > 0 {
281 let cosn: i64 = (d1x*d2x + d1y*d2y) * 1000 / (l1*l2)
282 if cosn < 500 { // turn > 60deg = CORNER
283 dx[m] = sx[i+1]; dy[m] = sy[i+1]
284 var tj: i64 = stag[i]
285 if stag[i+1] > tj { tj = stag[i+1] }
286 dtag[m-1] = tj
287 m = m + 1
288 }
289 } }
290 }
291 i = i + 1
292 }
293 dx[m]=sx[n-1]; dy[m]=sy[n-1]
294 dtag[m-1] = stag[n-2]
295 m = m + 1
296 return m
297}
298func fe_stroke(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
299 lx: *i64, ly: *i64, n: i64, lw: i64, ox: i64, oy: i64, S: i64) -> i64 {
300 let ax: *i64 = sys_mmap(8*512) as *i64
301 let ay: *i64 = sys_mmap(8*512) as *i64
302 let bx: *i64 = sys_mmap(8*512) as *i64
303 let by: *i64 = sys_mmap(8*512) as *i64
304 let st: *i64 = sys_mmap(8*512) as *i64 // per-SEGMENT swell tags (1 = interior curve wall)
305 let st2: *i64 = sys_mmap(8*512) as *i64
306 var k: i64 = 0
307 while k < n { ax[k]=lx[k]; ay[k]=ly[k]; k=k+1 }
308 // tag the ORIGINAL segments: interior segments of a curve polyline swell; a 2-point stroke is a STEM
309 // (never swells); the FIRST/LAST segments of open strokes are tails/junction legs (never swell) --
310 // this is what keeps n's right leg the same weight as its left stem. Closed loops swell everywhere.
311 var closed0: i64 = 0
312 if lx[0] == lx[n-1] { if ly[0] == ly[n-1] { closed0 = 1 } }
313 k = 0
314 while k < n - 1 {
315 var t: i64 = 0
316 if closed0 == 1 { t = 1 }
317 else { if k >= 1 { if k <= n-3 { t = 1 } } }
318 st[k] = t
319 k = k + 1
320 }
321 var passes: i64 = 1
322 if fs_passes > 0 { passes = fs_passes } // generator knob (1 = crisp, 2 = soft)
323 if passes > 3 { passes = 3 }
324 var m: i64 = n
325 var p: i64 = 0
326 while p < passes {
327 let m2: i64 = fe_chaikin_cp(ax, ay, st, m, bx, by, st2) // corner-preserving + carries the tags
328 var j: i64 = 0
329 while j < m2 - 1 { st[j] = st2[j]; j = j + 1 }
330 m = m2
331 var k2: i64 = 0
332 while k2 < m { ax[k2]=bx[k2]; ay[k2]=by[k2]; k2=k2+1 }
333 p = p + 1
334 }
335 // generator knob: ROUND terminals (soft style). The cap disc extends w_end/2 past the endpoint, so INSET
336 // each open end by w_end/2 along its direction first -- the cap's ink then lands exactly where butt ink
337 // did (keeps the metric alignment: stems still end ON the baseline). Standard round-cap geometry.
338 let wh2: i64 = (lw * font_mod()) / 100 // horizontal pen (stroke contrast)
339 var w0: i64 = lw
340 var w1: i64 = lw
341 if m >= 2 {
342 var edx: i64 = ax[1]-ax[0]
343 var edy: i64 = ay[1]-ay[0]
344 if edy < 0 { edy = 0 - edy }
345 var el: i64 = gs_isqrt(edx*edx+edy*edy)
346 if el > 0 { w0 = wh2 + ((lw - wh2) * edy) / el }
347 edx = ax[m-1]-ax[m-2]
348 edy = ay[m-1]-ay[m-2]
349 if edy < 0 { edy = 0 - edy }
350 el = gs_isqrt(edx*edx+edy*edy)
351 if el > 0 { w1 = wh2 + ((lw - wh2) * edy) / el }
352 }
353 var capped: i64 = 0
354 if fs_caps == 1 {
355 var closed: i64 = 0
356 if ax[0] == ax[m-1] { if ay[0] == ay[m-1] { closed = 1 } }
357 if closed == 0 { if m >= 2 {
358 capped = 1
359 var h: i64 = w0/2
360 var ddx: i64 = ax[1]-ax[0]
361 var ddy: i64 = ay[1]-ay[0]
362 var dl: i64 = gs_isqrt(ddx*ddx+ddy*ddy)
363 if dl > h { ax[0] = ax[0] + (ddx*h)/dl; ay[0] = ay[0] + (ddy*h)/dl }
364 h = w1/2
365 ddx = ax[m-2]-ax[m-1]
366 ddy = ay[m-2]-ay[m-1]
367 dl = gs_isqrt(ddx*ddx+ddy*ddy)
368 if dl > h { ax[m-1] = ax[m-1] + (ddx*h)/dl; ay[m-1] = ay[m-1] + (ddy*h)/dl }
369 } }
370 }
371 let gx: *i64 = sys_mmap(8*512) as *i64
372 let gy: *i64 = sys_mmap(8*512) as *i64
373 let ws: i64 = (font_ws() * font_wdth()) / 100 // family width-comp x WIDTH axis (x-only; y untouched -> metrics hold)
374 let sl: i64 = font_slant() // oblique shear about the BASELINE (y=750): x += (750-y)*sl/100
375 var i: i64 = 0
376 while i < m {
377 gx[i] = ox + (((ax[i] * ws) / 100 + ((750 - ay[i]) * sl) / 100) * S) / 1000
378 gy[i] = oy + (ay[i] * S) / 1000
379 i = i + 1
380 }
381 // per-SEGMENT pen widths (grid units): role-aware modulated nib -- vertical curve walls swell toward
382 // wsw, stems/tails stay wv, horizontals thin to wh; blended by |dy|/len. Fed to the VARIABLE-WIDTH
383 // OUTLINE stroke (one smooth polygon, width interpolates continuously -> no join-disc lumps).
384 let wvg: i64 = (lw * S) / 1000
385 let whg: i64 = (wh2 * S) / 1000
386 let wswg: i64 = ((lw * font_swell()) / 100 * S) / 1000
387 let segw: *i64 = sys_mmap(8*512) as *i64
388 var closed2: i64 = 0
389 if gx[0] == gx[m-1] { if gy[0] == gy[m-1] { closed2 = 1 } }
390 i = 0
391 while i < m - 1 {
392 let sdx: i64 = gx[i+1]-gx[i]
393 var sdy: i64 = gy[i+1]-gy[i]
394 if sdy < 0 { sdy = 0 - sdy }
395 let sl: i64 = gs_isqrt(sdx*sdx + sdy*sdy)
396 var wtop: i64 = wvg
397 if st[i] == 1 { wtop = wswg }
398 var w2: i64 = wtop
399 if sl > 0 { w2 = whg + ((wtop - whg) * sdy) / sl }
400 segw[i] = w2
401 i = i + 1
402 }
403 gs_stroke_var(xs, ys, cstart, clen, np, nc, gx, gy, m, segw, closed2)
404 if capped == 1 {
405 gs_disc(xs, ys, cstart, clen, np, nc, cs, sn, gx[0], gy[0], (w0*S)/2000, 0)
406 gs_disc(xs, ys, cstart, clen, np, nc, cs, sn, gx[m-1], gy[m-1], (w1*S)/2000, 0)
407 }
408 return 0
409}
410
411// ---- GENERATED-BY nx_font_space_tool (optical sidebearings, Tracy/autowidth method: per-glyph ink
412// measured through the REAL engine in the x-height optical band; every pair gap reads ~2x SB_T=100em ->
413// EVEN RHYTHM, the fix for "ransom-note spacing"). Rerun the tool after any glyph-geometry change. ----
414// font_sp_shift: x-offset applied at layout so each glyph's optical left edge sits at SB_T.
415func font_sp_shift(ch: i64) -> i64 {
416 if ch == 97 { return 35 }
417 if ch == 98 { return -20 }
418 if ch == 99 { return 25 }
419 if ch == 100 { return 35 }
420 if ch == 101 { return 30 }
421 if ch == 102 { return -35 }
422 if ch == 103 { return 35 }
423 if ch == 104 { return -20 }
424 if ch == 105 { return -20 }
425 if ch == 106 { return -20 }
426 if ch == 107 { return -20 }
427 if ch == 108 { return -70 }
428 if ch == 110 { return -20 }
429 if ch == 111 { return 40 }
430 if ch == 112 { return -20 }
431 if ch == 113 { return 35 }
432 if ch == 114 { return -50 }
433 if ch == 115 { return -20 }
434 if ch == 116 { return -20 }
435 if ch == 117 { return -5 }
436 if ch == 118 { return -25 }
437 if ch == 120 { return -40 }
438 if ch == 121 { return -25 }
439 if ch == 122 { return -45 }
440 if ch == 65 { return -30 }
441 if ch == 66 { return -20 }
442 if ch == 67 { return 5 }
443 if ch == 68 { return -20 }
444 if ch == 69 { return -5 }
445 if ch == 70 { return -20 }
446 if ch == 71 { return 5 }
447 if ch == 72 { return -20 }
448 if ch == 73 { return -140 }
449 if ch == 74 { return -25 }
450 if ch == 75 { return -20 }
451 if ch == 76 { return -30 }
452 if ch == 78 { return -20 }
453 if ch == 79 { return 15 }
454 if ch == 80 { return -20 }
455 if ch == 81 { return 15 }
456 if ch == 82 { return -20 }
457 if ch == 83 { return -15 }
458 if ch == 84 { return -85 }
459 if ch == 85 { return -5 }
460 if ch == 86 { return -40 }
461 if ch == 87 { return -10 }
462 if ch == 88 { return -55 }
463 if ch == 89 { return -50 }
464 if ch == 90 { return -85 }
465 if ch == 49 { return -140 }
466 if ch == 50 { return -85 }
467 if ch == 51 { return -60 }
468 if ch == 52 { return -95 }
469 if ch == 53 { return -45 }
470 if ch == 54 { return -10 }
471 if ch == 55 { return -115 }
472 if ch == 56 { return -5 }
473 if ch == 57 { return -20 }
474 if ch == 46 { return -125 }
475 if ch == 44 { return -110 }
476 if ch == 45 { return -80 }
477 if ch == 58 { return -95 }
478 if ch == 33 { return -90 }
479 if ch == 63 { return -85 }
480 if ch == 40 { return -95 }
481 if ch == 41 { return -95 }
482 if ch == 39 { return -105 }
483 if ch == 47 { return -95 }
484 return 0
485}
486func font_adv_em_base(ch: i64) -> i64 {
487 if ch == 32 { return 300 }
488 if ch == 97 { return 490 }
489 if ch == 98 { return 485 }
490 if ch == 99 { return 490 }
491 if ch == 100 { return 490 }
492 if ch == 101 { return 530 }
493 if ch == 102 { return 380 }
494 if ch == 103 { return 510 }
495 if ch == 104 { return 490 }
496 if ch == 105 { return 215 }
497 if ch == 106 { return 350 }
498 if ch == 107 { return 410 }
499 if ch == 108 { return 215 }
500 if ch == 109 { return 710 }
501 if ch == 110 { return 490 }
502 if ch == 111 { return 590 }
503 if ch == 112 { return 485 }
504 if ch == 113 { return 490 }
505 if ch == 114 { return 365 }
506 if ch == 115 { return 470 }
507 if ch == 116 { return 345 }
508 if ch == 117 { return 490 }
509 if ch == 118 { return 450 }
510 if ch == 119 { return 600 }
511 if ch == 120 { return 415 }
512 if ch == 121 { return 450 }
513 if ch == 122 { return 400 }
514 if ch == 65 { return 540 }
515 if ch == 66 { return 515 }
516 if ch == 67 { return 555 }
517 if ch == 68 { return 555 }
518 if ch == 69 { return 500 }
519 if ch == 70 { return 440 }
520 if ch == 71 { return 600 }
521 if ch == 72 { return 555 }
522 if ch == 73 { return 295 }
523 if ch == 74 { return 525 }
524 if ch == 75 { return 505 }
525 if ch == 76 { return 465 }
526 if ch == 77 { return 615 }
527 if ch == 78 { return 555 }
528 if ch == 79 { return 625 }
529 if ch == 80 { return 505 }
530 if ch == 81 { return 625 }
531 if ch == 82 { return 505 }
532 if ch == 83 { return 560 }
533 if ch == 84 { return 425 }
534 if ch == 85 { return 585 }
535 if ch == 86 { return 515 }
536 if ch == 87 { return 585 }
537 if ch == 88 { return 485 }
538 if ch == 89 { return 495 }
539 if ch == 90 { return 440 }
540 if ch == 48 { return 595 }
541 if ch == 49 { return 335 }
542 if ch == 50 { return 475 }
543 if ch == 51 { return 500 }
544 if ch == 52 { return 425 }
545 if ch == 53 { return 525 }
546 if ch == 54 { return 570 }
547 if ch == 55 { return 400 }
548 if ch == 56 { return 585 }
549 if ch == 57 { return 555 }
550 if ch == 46 { return 175 } // hand-clamped: the period dot sits below the optical band (tool sees a sliver)
551 if ch == 44 { return 185 } // hand-clamped, same reason
552 if ch == 45 { return 365 }
553 if ch == 58 { return 205 }
554 if ch == 33 { return 215 }
555 if ch == 63 { return 460 }
556 if ch == 40 { return 290 }
557 if ch == 41 { return 300 }
558 if ch == 39 { return 180 }
559 if ch == 47 { return 365 }
560 if ch == 91 { return 330 } // [
561 if ch == 93 { return 330 } // ]
562 if ch == 37 { return 720 } // %
563 if ch == 59 { return 205 } // ;
564 if ch == 34 { return 430 } // "
565 if ch == 61 { return 600 } // =
566 if ch == 43 { return 600 } // +
567 return 520
568}
569// ---- END GENERATED ----
570// ---- GENERATED-BY nx_font_space_tool (GEOMETRIC mode) ----
571func font_sp_shift_geo(ch: i64) -> i64 {
572 if ch == 97 { return -40 }
573 if ch == 98 { return -35 }
574 if ch == 99 { return -40 }
575 if ch == 100 { return -40 }
576 if ch == 101 { return -40 }
577 if ch == 102 { return -70 }
578 if ch == 103 { return -40 }
579 if ch == 104 { return -35 }
580 if ch == 105 { return -35 }
581 if ch == 106 { return -30 }
582 if ch == 107 { return -35 }
583 if ch == 108 { return -85 }
584 if ch == 109 { return -35 }
585 if ch == 110 { return -35 }
586 if ch == 111 { return -40 }
587 if ch == 112 { return -35 }
588 if ch == 113 { return -40 }
589 if ch == 114 { return -35 }
590 if ch == 115 { return -50 }
591 if ch == 116 { return -55 }
592 if ch == 117 { return -35 }
593 if ch == 118 { return -35 }
594 if ch == 119 { return -20 }
595 if ch == 120 { return -50 }
596 if ch == 121 { return -35 }
597 if ch == 122 { return -45 }
598 if ch == 65 { return -45 }
599 if ch == 66 { return -35 }
600 if ch == 67 { return -30 }
601 if ch == 68 { return -35 }
602 if ch == 69 { return -35 }
603 if ch == 70 { return -35 }
604 if ch == 71 { return -30 }
605 if ch == 72 { return -35 }
606 if ch == 73 { return -175 }
607 if ch == 74 { return -45 }
608 if ch == 75 { return -35 }
609 if ch == 76 { return -45 }
610 if ch == 77 { return -15 }
611 if ch == 78 { return -35 }
612 if ch == 79 { return -20 }
613 if ch == 80 { return -35 }
614 if ch == 81 { return -20 }
615 if ch == 82 { return -35 }
616 if ch == 83 { return -30 }
617 if ch == 84 { return -85 }
618 if ch == 85 { return -40 }
619 if ch == 86 { return -50 }
620 if ch == 87 { return -30 }
621 if ch == 88 { return -65 }
622 if ch == 89 { return -60 }
623 if ch == 90 { return -65 }
624 if ch == 48 { return -35 }
625 if ch == 49 { return -155 }
626 if ch == 50 { return -80 }
627 if ch == 51 { return -60 }
628 if ch == 52 { return -45 }
629 if ch == 53 { return -50 }
630 if ch == 54 { return -45 }
631 if ch == 55 { return -115 }
632 if ch == 56 { return -40 }
633 if ch == 57 { return -55 }
634 if ch == 46 { return -110 }
635 if ch == 44 { return -85 }
636 if ch == 45 { return -80 }
637 if ch == 58 { return -110 }
638 if ch == 33 { return -105 }
639 if ch == 63 { return -100 }
640 if ch == 40 { return -130 }
641 if ch == 41 { return -120 }
642 if ch == 39 { return -120 }
643 if ch == 47 { return -105 }
644 return 0
645}
646func font_adv_em_base_geo(ch: i64) -> i64 {
647 if ch == 32 { return 300 }
648 if ch == 97 { return 610 }
649 if ch == 98 { return 610 }
650 if ch == 99 { return 505 }
651 if ch == 100 { return 610 }
652 if ch == 101 { return 605 }
653 if ch == 102 { return 330 }
654 if ch == 103 { return 610 }
655 if ch == 104 { return 615 }
656 if ch == 105 { return 185 }
657 if ch == 106 { return 310 }
658 if ch == 107 { return 385 }
659 if ch == 108 { return 185 }
660 if ch == 109 { return 705 }
661 if ch == 110 { return 615 }
662 if ch == 111 { return 605 }
663 if ch == 112 { return 610 }
664 if ch == 113 { return 610 }
665 if ch == 114 { return 330 }
666 if ch == 115 { return 390 }
667 if ch == 116 { return 285 }
668 if ch == 117 { return 615 }
669 if ch == 118 { return 430 }
670 if ch == 119 { return 560 }
671 if ch == 120 { return 395 }
672 if ch == 121 { return 430 }
673 if ch == 122 { return 400 }
674 if ch == 65 { return 510 }
675 if ch == 66 { return 500 }
676 if ch == 67 { return 505 }
677 if ch == 68 { return 505 }
678 if ch == 69 { return 435 }
679 if ch == 70 { return 425 }
680 if ch == 71 { return 540 }
681 if ch == 72 { return 525 }
682 if ch == 73 { return 225 }
683 if ch == 74 { return 475 }
684 if ch == 75 { return 480 }
685 if ch == 76 { return 415 }
686 if ch == 77 { return 580 }
687 if ch == 78 { return 525 }
688 if ch == 79 { return 555 }
689 if ch == 80 { return 460 }
690 if ch == 81 { return 555 }
691 if ch == 82 { return 480 }
692 if ch == 83 { return 510 }
693 if ch == 84 { return 425 }
694 if ch == 85 { return 520 }
695 if ch == 86 { return 495 }
696 if ch == 87 { return 545 }
697 if ch == 88 { return 465 }
698 if ch == 89 { return 475 }
699 if ch == 90 { return 460 }
700 if ch == 48 { return 525 }
701 if ch == 49 { return 285 }
702 if ch == 50 { return 445 }
703 if ch == 51 { return 470 }
704 if ch == 52 { return 475 }
705 if ch == 53 { return 485 }
706 if ch == 54 { return 500 }
707 if ch == 55 { return 405 }
708 if ch == 56 { return 515 }
709 if ch == 57 { return 485 }
710 if ch == 46 { return 175 }
711 if ch == 44 { return 185 }
712 if ch == 45 { return 365 }
713 if ch == 58 { return 175 }
714 if ch == 33 { return 185 }
715 if ch == 63 { return 405 }
716 if ch == 40 { return 245 }
717 if ch == 41 { return 240 }
718 if ch == 39 { return 150 }
719 if ch == 47 { return 345 }
720 return 520
721}
722// ---- END GEO GENERATED ----
723func font_adv_em(ch: i64) -> i64 {
724 if font_face_has(ch) == 1 { let ft: *i64 = fs_face_adv as *i64; if ch >= FS_FACE_CPS { return ft[0] } if ft[ch] > 0 { return ft[ch] } return ft[0] } // a REAL face answers first (its advances are absolute); beyond the table or unmapped non-Latin: the notdef advance (BR39)
725 var b: i64 = font_adv_em_base(ch)
726 if fs_geo == 1 { b = font_adv_em_base_geo(ch) }
727 return (((b * font_ws()) / 100) * font_wdth()) / 100 // advance tracks the WIDTH axis so spacing stays proportional
728}
729func font_sp_shift_disp(ch: i64) -> i64 { if fs_geo == 1 { return font_sp_shift_geo(ch) } return font_sp_shift(ch) }
730func font_adv(ch: i64, S: i64) -> i64 { return (font_adv_em(ch) * S) / 1000 }
731
732// ★GEOMETRIC (machine-regular) lowercase: bowls = ONE exact circle reused; arches = ONE exact semicircle
733// reused; stems = exact verticals on a fixed grid. Returns 1 if it drew the glyph, 0 = fall through to the
734// organic font_emit. Monoline is expected (set mod=100, swell=100 for the "Geometric" style).
735func font_emit_geo(ch: i64, xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
736 ox: i64, oy: i64, S: i64) -> i64 {
737 var W: i64 = font_weight()
738 var OV: i64 = 12
739 if fs_over > 0 { OV = fs_over }
740 let WHp: i64 = (W * font_mod()) / 100
741 let V2H: i64 = (W + WHp) / 4
742 let XT: i64 = 250 - OV + V2H // round top / bottom extrema (ink kisses x-height/baseline +- overshoot)
743 let XB: i64 = 750 + OV - V2H
744 let CY: i64 = (XT + XB) / 2 // x-height circle centre y
745 let R: i64 = (XB - XT) / 2 // x-height circle radius (the MODULE -- every bowl/arch uses it)
746 let SPR: i64 = CY // arch spring line (arch = top half of the module circle)
747 let ax: *i64 = sys_mmap(8*16) as *i64
748 let ay: *i64 = sys_mmap(8*16) as *i64
749 // stem grid: left stem x=130, the module circle sits tangent to it (centre 130+R)
750 let LX: i64 = 130
751 let BC: i64 = LX + R // bowl centre x for stem+bowl letters (b d p q)
752 if ch == 111 { // o -- the module circle
753 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 130+R, CY, R, R, W, ox,oy,S)
754 return 1
755 }
756 if ch == 99 { // c -- module circle open on the right (idx 2..14)
757 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, 130+R, CY, R, 214, W, ox,oy,S)
758 return 1
759 }
760 if ch == 101 { // e -- c-arc (open lower-right) + horizontal bar at mid
761 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, 130+R, CY, R, 216, W, ox,oy,S) // idx 2..16: bottom->left->top->right (closes top-right, opens lower-right)
762 ax[0]=130; ay[0]=CY; ax[1]=130+2*R; ay[1]=CY
763 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
764 return 1
765 }
766 if ch == 110 { // n -- left stem + top-half arch + right stem
767 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=750
768 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
769 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, LX+R, SPR, R, 816, W, ox,oy,S) // left->top->right semicircle
770 ax[0]=LX+2*R;ay[0]=SPR-40; ax[1]=LX+2*R;ay[1]=750 // stem overlaps into the arch (fills the junction seam)
771 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
772 return 1
773 }
774 if ch == 104 { // h -- ascender stem + arch + right stem
775 ax[0]=LX;ay[0]=110; ax[1]=LX;ay[1]=750
776 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
777 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, LX+R, SPR, R, 816, W, ox,oy,S)
778 ax[0]=LX+2*R;ay[0]=SPR-40; ax[1]=LX+2*R;ay[1]=750 // h: overlap stem into arch
779 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
780 return 1
781 }
782 if ch == 117 { // u -- left stem + bottom-half arch + right stem
783 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=SPR
784 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
785 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, LX+R, SPR, R, 8, W, ox,oy,S) // right->bottom->left semicircle
786 ax[0]=LX+2*R;ay[0]=250; ax[1]=LX+2*R;ay[1]=SPR
787 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
788 return 1
789 }
790 if ch == 114 { // r -- stem + top-left quarter arch
791 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=750
792 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
793 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, LX+R, SPR, R, 812, W, ox,oy,S) // left->top quarter
794 return 1
795 }
796 if ch == 109 { // m -- 3 stems + 2 ELLIPTICAL arches (narrow rx, FULL ry -> humps reach x-height top like n)
797 let mr: i64 = (R*3)/5 // narrow hump half-width (m is a double-arch)
798 let mk: i64 = 816 + W*100000 // packed krange(top-half 8..16) + weight
799 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=750
800 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
801 geo_earc(xs,ys,cstart,clen,np,nc,cs,sn, LX+mr, CY, mr, R, mk, ox,oy,S)
802 ax[0]=LX+2*mr;ay[0]=CY-40; ax[1]=LX+2*mr;ay[1]=750 // overlap into arch (seam fill)
803 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
804 geo_earc(xs,ys,cstart,clen,np,nc,cs,sn, LX+3*mr, CY, mr, R, mk, ox,oy,S)
805 ax[0]=LX+4*mr;ay[0]=CY-40; ax[1]=LX+4*mr;ay[1]=750 // overlap into arch (seam fill)
806 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
807 return 1
808 }
809 if ch == 98 { // b -- ascender stem + module circle (tangent)
810 ax[0]=LX;ay[0]=110; ax[1]=LX;ay[1]=750
811 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
812 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
813 return 1
814 }
815 if ch == 100 { // d -- right ascender stem + module circle
816 ax[0]=LX+2*R;ay[0]=110; ax[1]=LX+2*R;ay[1]=750
817 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
818 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
819 return 1
820 }
821 if ch == 112 { // p -- descender stem + module circle
822 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=900
823 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
824 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
825 return 1
826 }
827 if ch == 113 { // q -- module circle + right descender stem
828 ax[0]=LX+2*R;ay[0]=250; ax[1]=LX+2*R;ay[1]=900
829 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
830 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
831 return 1
832 }
833 if ch == 97 { // a -- module circle + right stem (single-storey, geometric)
834 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
835 ax[0]=LX+2*R;ay[0]=CY-R+40; ax[1]=LX+2*R;ay[1]=750
836 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
837 return 1
838 }
839 if ch == 115 { // s -- two stacked circles sharing the CENTRE point (top circle's bottom == bottom circle's top == (sx,CY))
840 let rs: i64 = (XB - XT) / 4
841 let sx: i64 = 130 + rs + 12
842 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, sx, CY-rs, rs, 414, W, ox,oy,S) // top bowl: centre(4)->left(8)->top(12)->upper-right(14)
843 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, sx, CY+rs, rs, 1206, W, ox,oy,S) // bottom bowl: centre(12)->right(0)->bottom(4)->lower-left(6)
844 return 1
845 }
846 if ch == 103 { // g -- single-storey, smooth gentle-J tail (Futura model): closed circle bowl + a tail that descends with
847 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S) // MINIMAL rightward bulge (so it is not a 9) and hooks decisively LEFT (so it is not a q); 6 pts, Chaikin-smoothed
848 ax[0]=BC+(R*52)/100; ay[0]=712 // on the bowl, lower-right
849 ax[1]=BC+(R*58)/100; ay[1]=802 // descend nearly straight (small bulge)
850 ax[2]=BC+(R*46)/100; ay[2]=880 // begin the curve
851 ax[3]=BC+(R*4)/100; ay[3]=928 // DEEP bottom of the descender (registers at small px)
852 ax[4]=BC-(R*50)/100; ay[4]=910 // strong LEFT hook
853 ax[5]=BC-(R*88)/100; ay[5]=846 // open terminal, up-left
854 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
855 return 1
856 }
857 return 0 // straights (l i t k v w x y z s f j m) -> organic path (already geometric enough)
858}
859
860// emit one glyph's stroke-composition at pen (ox,oy) grid, scale S. Em: baseline 750, x-height-top 250,
861// ascender-top 100, descender 900, stem width ~70. Strokes -> curves come free.
862func font_emit(ch: i64, xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
863 ox: i64, oy: i64, S: i64) -> i64 {
864 if fs_geo == 1 { if font_emit_geo(ch, xs, ys, cstart, clen, np, nc, cs, sn, ox, oy, S) == 1 { return 0 } }
865 let ax: *i64 = sys_mmap(8*16) as *i64
866 let ay: *i64 = sys_mmap(8*16) as *i64
867 var W: i64 = font_weight() // pen weight (em): organic Regular=123 (tuned on isolated-stem vs Arial-600);
868 // fs_w overrides (Light ~95, Bold ~160); geo defaults 85 (lighter=pro)
869 var OV: i64 = 12 // overshoot (em): how far round ink kisses past the metric lines
870 if fs_over > 0 { OV = fs_over } // generator knob
871 // ★PARAMETRIC METRIC LINES (METAFONT-style: coordinates as functions of the pen). The stroke's INK
872 // extends V2=W/2 past the centerline, so CURVE EXTREMA paths sit V2 INSIDE the intended ink line, while
873 // stem BUTT ends sit exactly ON it. This kills the systematic stem-vs-bowl misalignment (baseline bounce,
874 // uneven x-height) BY CONSTRUCTION -- change W and the whole font re-aligns. The generator core.
875 let V2: i64 = W/2
876 let WH2: i64 = (W * font_mod()) / 100 // horizontal pen (stroke contrast/modulation)
877 let V2H: i64 = (W + WH2) / 4 // curve-extremum half-pen: the segments AROUND an extremum are
878 // part-diagonal, so their width blends W..WH2 -- use the mean
879 // half-pen so extremum ink still kisses the overshoot line (at
880 // mod=100 this reduces to W/2 exactly)
881 let XT: i64 = 250 - OV + V2H // round TOP extremum: ink kisses x-height - overshoot
882 let XB: i64 = 750 + OV - V2H // round BOTTOM extremum: ink kisses baseline + overshoot
883 let SH: i64 = 250 + V2H // arch/shoulder top + x-height BAR: ink exactly AT x-height 250
884 let BB: i64 = 750 - V2H // baseline BAR: ink exactly AT baseline 750
885 let DB: i64 = 900 + OV - V2H // descender round bottom: ink kisses descender + overshoot
886 let CT: i64 = 130 - OV + V2H // cap round TOP extremum: ink kisses cap height - overshoot
887 let CB: i64 = 130 + V2H // cap corner/bar top path: ink AT cap height 130
888 let WSC: i64 = (font_ws() * font_wdth()) / 100 // family width-comp x WIDTH axis (applies to disc x-positions here;
889 // stroke geometry gets it inside fe_stroke)
890 let SL: i64 = font_slant() // oblique shear for disc x-positions (strokes get it in fe_stroke)
891 let DR: i64 = (45 * W) / 123 // i/j dot radius: scales with the pen weight
892 let PR: i64 = (57 * W) / 123 // punctuation dot radius
893 if ch == 110 { // n -- wide counter (condensed fix): stems 130/390
894 ax[0]=130;ay[0]=750; ax[1]=130;ay[1]=250
895 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
896 ax[0]=130;ay[0]=430; ax[1]=180;ay[1]=SH+28; ax[2]=260;ay[2]=SH; ax[3]=340;ay[3]=SH+28; ax[4]=390;ay[4]=430; ax[5]=390;ay[5]=750
897 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
898 return 0
899 }
900 if ch == 104 { // h -- ascender stem + wide arch
901 ax[0]=130;ay[0]=750; ax[1]=130;ay[1]=110
902 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
903 ax[0]=130;ay[0]=430; ax[1]=180;ay[1]=SH+28; ax[2]=260;ay[2]=SH; ax[3]=340;ay[3]=SH+28; ax[4]=390;ay[4]=430; ax[5]=390;ay[5]=750
904 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
905 return 0
906 }
907 if ch == 105 { // i
908 ax[0]=130;ay[0]=750; ax[1]=130;ay[1]=250
909 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
910 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((130*WSC)/100 + ((750-160)*SL)/100)*S)/1000, oy+(160*S)/1000, (DR*S)/1000, 0)
911 return 0
912 }
913 if ch == 115 { // s -- double curve (extrema on the parametric lines)
914 ax[0]=372;ay[0]=XT+44; ax[1]=292;ay[1]=XT+4; ax[2]=190;ay[2]=XT; ax[3]=140;ay[3]=350; ax[4]=178;ay[4]=436
915 ax[5]=275;ay[5]=478; ax[6]=350;ay[6]=528; ax[7]=380;ay[7]=616; ax[8]=330;ay[8]=XB+4; ax[9]=222;ay[9]=XB; ax[10]=118;ay[10]=XB-42
916 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,11, W, ox,oy,S)
917 return 0
918 }
919 if ch == 116 { // t -- stem + foot hook (ink lands ~750) + crossbar ON x-height
920 ax[0]=180;ay[0]=130; ax[1]=180;ay[1]=614; ax[2]=222;ay[2]=684; ax[3]=298;ay[3]=670
921 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
922 ax[0]=70;ay[0]=SH; ax[1]=316;ay[1]=SH
923 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
924 return 0
925 }
926 if ch == 101 { // e -- bar + open bowl on the parametric lines
927 ax[0]=95;ay[0]=505; ax[1]=395;ay[1]=505
928 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
929 ax[0]=395;ay[0]=505; ax[1]=385;ay[1]=372; ax[2]=255;ay[2]=XT; ax[3]=128;ay[3]=XT+36; ax[4]=88;ay[4]=500
930 ax[5]=126;ay[5]=XB-44; ax[6]=250;ay[6]=XB; ax[7]=378;ay[7]=XB-36
931 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,8, W, ox,oy,S)
932 return 0
933 }
934 if ch == 111 { // o -- ring on the parametric lines
935 ax[0]=432;ay[0]=500; ax[1]=388;ay[1]=XT+62; ax[2]=255;ay[2]=XT; ax[3]=122;ay[3]=XT+62; ax[4]=78;ay[4]=500
936 ax[5]=122;ay[5]=XB-62; ax[6]=255;ay[6]=XB; ax[7]=388;ay[7]=XB-62; ax[8]=432;ay[8]=500
937 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
938 return 0
939 }
940 if ch == 114 { // r -- stem + shoulder arm at x-height
941 ax[0]=160;ay[0]=750; ax[1]=160;ay[1]=250
942 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
943 ax[0]=160;ay[0]=430; ax[1]=205;ay[1]=SH+28; ax[2]=290;ay[2]=SH; ax[3]=356;ay[3]=SH+16
944 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
945 return 0
946 }
947 if ch == 108 { // l -- ascender vertical
948 ax[0]=180;ay[0]=110; ax[1]=180;ay[1]=750
949 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
950 return 0
951 }
952 if ch == 99 { // c -- open arc on the parametric lines
953 ax[0]=398;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=140;ay[2]=XT+28; ax[3]=90;ay[3]=500; ax[4]=140;ay[4]=XB-28; ax[5]=270;ay[5]=XB+4; ax[6]=398;ay[6]=648
954 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
955 return 0
956 }
957 if ch == 100 { // d -- bowl + right ascender stem
958 ax[0]=350;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=170;ay[2]=XT; ax[3]=110;ay[3]=XT+34; ax[4]=85;ay[4]=500; ax[5]=110;ay[5]=XB-34; ax[6]=195;ay[6]=XB; ax[7]=290;ay[7]=XB+6; ax[8]=350;ay[8]=648
959 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
960 ax[0]=350;ay[0]=110; ax[1]=350;ay[1]=750
961 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
962 return 0
963 }
964 if ch == 97 { // a -- bowl + right x-height stem
965 ax[0]=350;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=170;ay[2]=XT; ax[3]=108;ay[3]=XT+34; ax[4]=85;ay[4]=500; ax[5]=108;ay[5]=XB-34; ax[6]=185;ay[6]=XB; ax[7]=290;ay[7]=XB+6; ax[8]=350;ay[8]=648
966 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
967 ax[0]=350;ay[0]=280; ax[1]=350;ay[1]=750
968 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
969 return 0
970 }
971 if ch == 117 { // u -- wide counter + bottom curve on the parametric line
972 ax[0]=130;ay[0]=250; ax[1]=130;ay[1]=580; ax[2]=180;ay[2]=XB-28; ax[3]=255;ay[3]=XB; ax[4]=330;ay[4]=XB-28; ax[5]=390;ay[5]=580
973 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
974 ax[0]=390;ay[0]=250; ax[1]=390;ay[1]=750
975 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
976 return 0
977 }
978 if ch == 109 { // m -- wide double counter: stems 110/350/590
979 ax[0]=110;ay[0]=750; ax[1]=110;ay[1]=250
980 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
981 ax[0]=110;ay[0]=430; ax[1]=155;ay[1]=SH+28; ax[2]=230;ay[2]=SH; ax[3]=305;ay[3]=SH+28; ax[4]=350;ay[4]=430; ax[5]=350;ay[5]=750
982 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
983 ax[0]=350;ay[0]=430; ax[1]=395;ay[1]=SH+28; ax[2]=470;ay[2]=SH; ax[3]=545;ay[3]=SH+28; ax[4]=590;ay[4]=430; ax[5]=590;ay[5]=750
984 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
985 return 0
986 }
987 if ch == 119 { // w -- zigzag (apex ink kisses 762)
988 ax[0]=85;ay[0]=250; ax[1]=185;ay[1]=XB; ax[2]=300;ay[2]=400; ax[3]=415;ay[3]=XB; ax[4]=515;ay[4]=250
989 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
990 return 0
991 }
992 if ch == 112 { // p -- descender stem + bowl (bowl bottom kisses baseline)
993 ax[0]=130;ay[0]=250; ax[1]=130;ay[1]=900
994 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
995 ax[0]=130;ay[0]=344; ax[1]=215;ay[1]=SH+2; ax[2]=320;ay[2]=SH+22; ax[3]=384;ay[3]=430; ax[4]=386;ay[4]=550; ax[5]=340;ay[5]=652; ax[6]=228;ay[6]=XB; ax[7]=130;ay[7]=644
996 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,8, W, ox,oy,S)
997 return 0
998 }
999 if ch == 98 { // b -- left ascender stem + right bowl
1000 ax[0]=130;ay[0]=110; ax[1]=130;ay[1]=750
1001 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1002 ax[0]=130;ay[0]=344; ax[1]=215;ay[1]=SH+2; ax[2]=320;ay[2]=SH+22; ax[3]=384;ay[3]=430; ax[4]=386;ay[4]=550; ax[5]=340;ay[5]=652; ax[6]=228;ay[6]=XB; ax[7]=130;ay[7]=644
1003 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,8, W, ox,oy,S)
1004 return 0
1005 }
1006 if ch == 102 { // f -- hooked ascender (hook ink kisses ~108) + bar ON x-height
1007 ax[0]=362;ay[0]=240; ax[1]=290;ay[1]=100+V2H; ax[2]=214;ay[2]=122+V2H; ax[3]=190;ay[3]=300; ax[4]=190;ay[4]=750
1008 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1009 ax[0]=85;ay[0]=SH; ax[1]=330;ay[1]=SH
1010 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1011 return 0
1012 }
1013 if ch == 103 { // g -- bowl (kisses baseline) + descender loop (kisses 912)
1014 ax[0]=355;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=170;ay[2]=XT; ax[3]=110;ay[3]=XT+34; ax[4]=88;ay[4]=495; ax[5]=112;ay[5]=XB-36; ax[6]=190;ay[6]=XB; ax[7]=282;ay[7]=XB-8; ax[8]=355;ay[8]=628
1015 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
1016 ax[0]=355;ay[0]=270; ax[1]=355;ay[1]=778; ax[2]=316;ay[2]=DB; ax[3]=212;ay[3]=DB+8; ax[4]=126;ay[4]=DB-32
1017 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1018 return 0
1019 }
1020 if ch == 106 { // j -- descender hook (kisses 912) + dot
1021 ax[0]=250;ay[0]=250; ax[1]=250;ay[1]=780; ax[2]=214;ay[2]=DB; ax[3]=134;ay[3]=DB+6; ax[4]=76;ay[4]=DB-38
1022 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1023 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((250*WSC)/100 + ((750-160)*SL)/100)*S)/1000, oy+(160*S)/1000, (DR*S)/1000, 0)
1024 return 0
1025 }
1026 if ch == 107 { // k -- ascender stem + two diagonals
1027 ax[0]=130;ay[0]=110; ax[1]=130;ay[1]=750
1028 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1029 ax[0]=360;ay[0]=300; ax[1]=130;ay[1]=530
1030 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1031 ax[0]=210;ay[0]=455; ax[1]=375;ay[1]=750
1032 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1033 return 0
1034 }
1035 if ch == 113 { // q -- bowl + right descender stem
1036 ax[0]=350;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=170;ay[2]=XT; ax[3]=110;ay[3]=XT+34; ax[4]=85;ay[4]=500; ax[5]=110;ay[5]=XB-34; ax[6]=185;ay[6]=XB; ax[7]=290;ay[7]=XB+6; ax[8]=350;ay[8]=648
1037 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
1038 ax[0]=350;ay[0]=280; ax[1]=350;ay[1]=900
1039 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1040 return 0
1041 }
1042 if ch == 118 { // v -- two diagonals (apex ink kisses 762)
1043 ax[0]=90;ay[0]=250; ax[1]=250;ay[1]=XB; ax[2]=410;ay[2]=250
1044 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1045 return 0
1046 }
1047 if ch == 120 { // x -- crossing diagonals
1048 ax[0]=100;ay[0]=250; ax[1]=400;ay[1]=750
1049 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1050 ax[0]=400;ay[0]=250; ax[1]=100;ay[1]=750
1051 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1052 return 0
1053 }
1054 if ch == 121 { // y -- v + descender tail
1055 ax[0]=90;ay[0]=250; ax[1]=250;ay[1]=670
1056 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1057 ax[0]=410;ay[0]=250; ax[1]=250;ay[1]=670; ax[2]=196;ay[2]=828; ax[3]=122;ay[3]=872
1058 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1059 return 0
1060 }
1061 if ch == 122 { // z -- bars ON the metric lines + diagonal
1062 ax[0]=95;ay[0]=SH; ax[1]=390;ay[1]=SH; ax[2]=105;ay[2]=BB; ax[3]=400;ay[3]=BB
1063 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1064 return 0
1065 }
1066 // ---- UPPERCASE A-Z (cap band: top 130 .. baseline 750) ----
1067 if ch == 65 { // A -- apex ink at cap height
1068 ax[0]=100;ay[0]=750; ax[1]=300;ay[1]=CB; ax[2]=500;ay[2]=750
1069 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1070 ax[0]=185;ay[0]=520; ax[1]=415;ay[1]=520
1071 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1072 return 0
1073 }
1074 if ch == 66 { // B -- bars on the cap metric lines
1075 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1076 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1077 ax[0]=130;ay[0]=CB; ax[1]=320;ay[1]=CB; ax[2]=405;ay[2]=280; ax[3]=320;ay[3]=430; ax[4]=130;ay[4]=434
1078 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1079 ax[0]=130;ay[0]=434; ax[1]=340;ay[1]=438; ax[2]=440;ay[2]=560; ax[3]=340;ay[3]=BB; ax[4]=130;ay[4]=BB
1080 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1081 return 0
1082 }
1083 if ch == 67 { // C -- open arc on the cap lines
1084 ax[0]=475;ay[0]=262; ax[1]=380;ay[1]=196; ax[2]=245;ay[2]=CT; ax[3]=140;ay[3]=262; ax[4]=115;ay[4]=440
1085 ax[5]=140;ay[5]=618; ax[6]=245;ay[6]=XB; ax[7]=380;ay[7]=684; ax[8]=475;ay[8]=618
1086 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
1087 return 0
1088 }
1089 if ch == 68 { // D
1090 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1091 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1092 ax[0]=130;ay[0]=CB; ax[1]=300;ay[1]=CB; ax[2]=430;ay[2]=300; ax[3]=465;ay[3]=440; ax[4]=430;ay[4]=580; ax[5]=300;ay[5]=BB; ax[6]=130;ay[6]=BB
1093 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1094 return 0
1095 }
1096 if ch == 69 { // E
1097 ax[0]=450;ay[0]=CB; ax[1]=130;ay[1]=CB; ax[2]=130;ay[2]=BB; ax[3]=460;ay[3]=BB
1098 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1099 ax[0]=130;ay[0]=436; ax[1]=400;ay[1]=436
1100 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1101 return 0
1102 }
1103 if ch == 70 { // F
1104 ax[0]=450;ay[0]=CB; ax[1]=130;ay[1]=CB; ax[2]=130;ay[2]=750
1105 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1106 ax[0]=130;ay[0]=436; ax[1]=390;ay[1]=436
1107 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1108 return 0
1109 }
1110 if ch == 71 { // G -- C + inner bar
1111 ax[0]=475;ay[0]=262; ax[1]=380;ay[1]=196; ax[2]=245;ay[2]=CT; ax[3]=140;ay[3]=262; ax[4]=115;ay[4]=440
1112 ax[5]=140;ay[5]=618; ax[6]=245;ay[6]=XB; ax[7]=390;ay[7]=684; ax[8]=480;ay[8]=608; ax[9]=480;ay[9]=486
1113 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,10, W, ox,oy,S)
1114 ax[0]=480;ay[0]=486; ax[1]=340;ay[1]=486
1115 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1116 return 0
1117 }
1118 if ch == 72 { // H
1119 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1120 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1121 ax[0]=470;ay[0]=130; ax[1]=470;ay[1]=750
1122 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1123 ax[0]=130;ay[0]=436; ax[1]=470;ay[1]=436
1124 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1125 return 0
1126 }
1127 if ch == 73 { // I -- stem + top/bottom serifs (vs l/1)
1128 ax[0]=190;ay[0]=CB; ax[1]=390;ay[1]=CB
1129 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1130 ax[0]=290;ay[0]=CB; ax[1]=290;ay[1]=BB
1131 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1132 ax[0]=190;ay[0]=BB; ax[1]=390;ay[1]=BB
1133 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1134 return 0
1135 }
1136 if ch == 74 { // J
1137 ax[0]=430;ay[0]=130; ax[1]=430;ay[1]=590; ax[2]=390;ay[2]=XB-24; ax[3]=280;ay[3]=XB; ax[4]=172;ay[4]=XB-24; ax[5]=135;ay[5]=600
1138 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
1139 return 0
1140 }
1141 if ch == 75 { // K
1142 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1143 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1144 ax[0]=450;ay[0]=130; ax[1]=135;ay[1]=470
1145 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1146 ax[0]=250;ay[0]=395; ax[1]=470;ay[1]=750
1147 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1148 return 0
1149 }
1150 if ch == 76 { // L
1151 ax[0]=140;ay[0]=130; ax[1]=140;ay[1]=BB; ax[2]=450;ay[2]=BB
1152 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1153 return 0
1154 }
1155 if ch == 77 { // M -- corner joins ink at cap height
1156 ax[0]=110;ay[0]=750; ax[1]=112;ay[1]=CB; ax[2]=310;ay[2]=540; ax[3]=508;ay[3]=CB; ax[4]=510;ay[4]=750
1157 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1158 return 0
1159 }
1160 if ch == 78 { // N -- corner joins on the cap lines
1161 ax[0]=130;ay[0]=750; ax[1]=130;ay[1]=CB; ax[2]=470;ay[2]=BB; ax[3]=470;ay[3]=130
1162 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1163 return 0
1164 }
1165 if ch == 79 { // O -- closed ring (ry from the parametric lines)
1166 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,440, 190,(XB-CT)/2, W, ox,oy,S)
1167 return 0
1168 }
1169 if ch == 80 { // P
1170 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1171 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1172 ax[0]=130;ay[0]=CB; ax[1]=330;ay[1]=CB; ax[2]=430;ay[2]=300; ax[3]=330;ay[3]=474; ax[4]=130;ay[4]=474
1173 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1174 return 0
1175 }
1176 if ch == 81 { // Q -- O + tail
1177 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,440, 190,(XB-CT)/2, W, ox,oy,S)
1178 ax[0]=370;ay[0]=610; ax[1]=505;ay[1]=780
1179 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1180 return 0
1181 }
1182 if ch == 82 { // R -- P + leg
1183 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1184 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1185 ax[0]=130;ay[0]=CB; ax[1]=330;ay[1]=CB; ax[2]=430;ay[2]=300; ax[3]=330;ay[3]=474; ax[4]=130;ay[4]=474
1186 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1187 ax[0]=280;ay[0]=474; ax[1]=470;ay[1]=750
1188 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1189 return 0
1190 }
1191 if ch == 83 { // S -- cap double curve on the lines
1192 ax[0]=455;ay[0]=266; ax[1]=350;ay[1]=CT+12; ax[2]=205;ay[2]=CT+14; ax[3]=125;ay[3]=286; ax[4]=165;ay[4]=400
1193 ax[5]=300;ay[5]=448; ax[6]=430;ay[6]=506; ax[7]=468;ay[7]=612; ax[8]=382;ay[8]=XB-4; ax[9]=222;ay[9]=XB; ax[10]=112;ay[10]=614
1194 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,11, W, ox,oy,S)
1195 return 0
1196 }
1197 if ch == 84 { // T
1198 ax[0]=100;ay[0]=CB; ax[1]=500;ay[1]=CB
1199 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1200 ax[0]=300;ay[0]=CB; ax[1]=300;ay[1]=750
1201 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1202 return 0
1203 }
1204 if ch == 85 { // U
1205 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=560; ax[2]=180;ay[2]=XB-34; ax[3]=300;ay[3]=XB; ax[4]=420;ay[4]=XB-34; ax[5]=470;ay[5]=560; ax[6]=470;ay[6]=130
1206 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1207 return 0
1208 }
1209 if ch == 86 { // V -- apex ink kisses 762
1210 ax[0]=105;ay[0]=130; ax[1]=300;ay[1]=XB; ax[2]=495;ay[2]=130
1211 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1212 return 0
1213 }
1214 if ch == 87 { // W
1215 ax[0]=85;ay[0]=130; ax[1]=185;ay[1]=XB; ax[2]=305;ay[2]=360; ax[3]=425;ay[3]=XB; ax[4]=525;ay[4]=130
1216 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1217 return 0
1218 }
1219 if ch == 88 { // X
1220 ax[0]=115;ay[0]=130; ax[1]=485;ay[1]=750
1221 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1222 ax[0]=485;ay[0]=130; ax[1]=115;ay[1]=750
1223 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1224 return 0
1225 }
1226 if ch == 89 { // Y
1227 ax[0]=110;ay[0]=130; ax[1]=300;ay[1]=440
1228 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1229 ax[0]=490;ay[0]=130; ax[1]=300;ay[1]=440; ax[2]=300;ay[2]=750
1230 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1231 return 0
1232 }
1233 if ch == 90 { // Z
1234 ax[0]=120;ay[0]=CB; ax[1]=470;ay[1]=CB; ax[2]=125;ay[2]=BB; ax[3]=480;ay[3]=BB
1235 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1236 return 0
1237 }
1238 // ---- DIGITS 0-9 (cap band, on the parametric lines) ----
1239 if ch == 48 { // 0
1240 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,440, 175,(XB-CT)/2, W, ox,oy,S)
1241 return 0
1242 }
1243 if ch == 49 { // 1
1244 ax[0]=200;ay[0]=268; ax[1]=310;ay[1]=CB; ax[2]=310;ay[2]=BB
1245 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1246 ax[0]=190;ay[0]=BB; ax[1]=430;ay[1]=BB
1247 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1248 return 0
1249 }
1250 if ch == 50 { // 2
1251 ax[0]=145;ay[0]=272; ax[1]=225;ay[1]=CB+6; ax[2]=370;ay[2]=CB; ax[3]=450;ay[3]=292; ax[4]=425;ay[4]=420; ax[5]=140;ay[5]=BB; ax[6]=470;ay[6]=BB
1252 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1253 return 0
1254 }
1255 if ch == 51 { // 3
1256 ax[0]=150;ay[0]=246; ax[1]=250;ay[1]=CT+12; ax[2]=380;ay[2]=CT+20; ax[3]=430;ay[3]=292; ax[4]=380;ay[4]=418; ax[5]=285;ay[5]=446
1257 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
1258 ax[0]=285;ay[0]=446; ax[1]=400;ay[1]=482; ax[2]=455;ay[2]=588; ax[3]=392;ay[3]=XB-26; ax[4]=242;ay[4]=XB; ax[5]=140;ay[5]=624
1259 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
1260 return 0
1261 }
1262 if ch == 52 { // 4
1263 ax[0]=400;ay[0]=130; ax[1]=140;ay[1]=515; ax[2]=475;ay[2]=515
1264 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1265 ax[0]=400;ay[0]=130; ax[1]=400;ay[1]=750
1266 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1267 return 0
1268 }
1269 if ch == 53 { // 5
1270 ax[0]=440;ay[0]=CB; ax[1]=165;ay[1]=CB; ax[2]=155;ay[2]=405
1271 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1272 ax[0]=155;ay[0]=405; ax[1]=300;ay[1]=368; ax[2]=425;ay[2]=428; ax[3]=462;ay[3]=556; ax[4]=392;ay[4]=XB-26; ax[5]=235;ay[5]=XB; ax[6]=130;ay[6]=618
1273 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1274 return 0
1275 }
1276 if ch == 54 { // 6
1277 ax[0]=420;ay[0]=222; ax[1]=310;ay[1]=CT+6; ax[2]=200;ay[2]=246; ax[3]=145;ay[3]=380; ax[4]=135;ay[4]=540; ax[5]=170;ay[5]=636; ax[6]=240;ay[6]=678
1278 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1279 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 305,565, 158,(XB-434)/2, W, ox,oy,S)
1280 return 0
1281 }
1282 if ch == 55 { // 7
1283 ax[0]=130;ay[0]=CB; ax[1]=470;ay[1]=CB; ax[2]=260;ay[2]=750
1284 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1285 return 0
1286 }
1287 if ch == 56 { // 8 -- two rings on the lines
1288 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,304, 148,304-CT, W, ox,oy,S)
1289 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,580, 170,XB-580, W, ox,oy,S)
1290 return 0
1291 }
1292 if ch == 57 { // 9 -- mirrored 6
1293 ax[0]=180;ay[0]=658; ax[1]=290;ay[1]=XB-4; ax[2]=400;ay[2]=636; ax[3]=452;ay[3]=505; ax[4]=462;ay[4]=345; ax[5]=430;ay[5]=264; ax[6]=360;ay[6]=222
1294 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1295 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 295,308, 158,308-CT, W, ox,oy,S)
1296 return 0
1297 }
1298 // ---- HEADING PUNCTUATION ----
1299 if ch == 46 { // .
1300 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-695)*SL)/100)*S)/1000, oy+(695*S)/1000, (PR*S)/1000, 0)
1301 return 0
1302 }
1303 if ch == 44 { // ,
1304 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-695)*SL)/100)*S)/1000, oy+(695*S)/1000, (PR*S)/1000, 0)
1305 ax[0]=210;ay[0]=740; ax[1]=155;ay[1]=855
1306 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (80*W)/123, ox,oy,S)
1307 return 0
1308 }
1309 if ch == 45 { // -
1310 ax[0]=130;ay[0]=440; ax[1]=400;ay[1]=440
1311 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1312 return 0
1313 }
1314 if ch == 58 { // :
1315 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-360)*SL)/100)*S)/1000, oy+(360*S)/1000, (PR*S)/1000, 0)
1316 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-695)*SL)/100)*S)/1000, oy+(695*S)/1000, (PR*S)/1000, 0)
1317 return 0
1318 }
1319 if ch == 33 { // !
1320 ax[0]=200;ay[0]=135; ax[1]=200;ay[1]=545
1321 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1322 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-700)*SL)/100)*S)/1000, oy+(700*S)/1000, (PR*S)/1000, 0)
1323 return 0
1324 }
1325 if ch == 63 { // ?
1326 ax[0]=140;ay[0]=268; ax[1]=225;ay[1]=CB+6; ax[2]=365;ay[2]=CB; ax[3]=435;ay[3]=296; ax[4]=400;ay[4]=410; ax[5]=290;ay[5]=466; ax[6]=290;ay[6]=560
1327 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1328 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((290*WSC)/100 + ((750-700)*SL)/100)*S)/1000, oy+(700*S)/1000, (PR*S)/1000, 0)
1329 return 0
1330 }
1331 if ch == 40 { // (
1332 ax[0]=330;ay[0]=110; ax[1]=245;ay[1]=270; ax[2]=215;ay[2]=440; ax[3]=245;ay[3]=610; ax[4]=330;ay[4]=780
1333 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1334 return 0
1335 }
1336 if ch == 41 { // )
1337 ax[0]=170;ay[0]=110; ax[1]=255;ay[1]=270; ax[2]=285;ay[2]=440; ax[3]=255;ay[3]=610; ax[4]=170;ay[4]=780
1338 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1339 return 0
1340 }
1341 if ch == 39 { // '
1342 ax[0]=200;ay[0]=130; ax[1]=190;ay[1]=270
1343 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (85*W)/123, ox,oy,S)
1344 return 0
1345 }
1346 if ch == 47 { // /
1347 ax[0]=400;ay[0]=110; ax[1]=160;ay[1]=790
1348 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1349 return 0
1350 }
1351 if ch == 91 { // [ -- bracket
1352 ax[0]=320;ay[0]=140; ax[1]=200;ay[1]=140; ax[2]=200;ay[2]=760; ax[3]=320;ay[3]=760
1353 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1354 return 0
1355 }
1356 if ch == 93 { // ]
1357 ax[0]=180;ay[0]=140; ax[1]=300;ay[1]=140; ax[2]=300;ay[2]=760; ax[3]=180;ay[3]=760
1358 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1359 return 0
1360 }
1361 if ch == 37 { // % -- diagonal + two small rings
1362 ax[0]=500;ay[0]=150; ax[1]=160;ay[1]=750
1363 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1364 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 250,270, 100,120, (80*W)/123, ox,oy,S)
1365 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 470,630, 100,120, (80*W)/123, ox,oy,S)
1366 return 0
1367 }
1368 if ch == 59 { // ; -- top dot + comma tail
1369 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-360)*SL)/100)*S)/1000, oy+(360*S)/1000, (PR*S)/1000, 0)
1370 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-695)*SL)/100)*S)/1000, oy+(695*S)/1000, (PR*S)/1000, 0)
1371 ax[0]=210;ay[0]=740; ax[1]=155;ay[1]=855
1372 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (80*W)/123, ox,oy,S)
1373 return 0
1374 }
1375 if ch == 34 { // " -- double quote
1376 ax[0]=180;ay[0]=140; ax[1]=170;ay[1]=290
1377 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (85*W)/123, ox,oy,S)
1378 ax[0]=320;ay[0]=140; ax[1]=310;ay[1]=290
1379 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (85*W)/123, ox,oy,S)
1380 return 0
1381 }
1382 if ch == 61 { // = -- two bars
1383 ax[0]=140;ay[0]=390; ax[1]=460;ay[1]=390
1384 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1385 ax[0]=140;ay[0]=530; ax[1]=460;ay[1]=530
1386 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1387 return 0
1388 }
1389 if ch == 43 { // + -- cross
1390 ax[0]=300;ay[0]=300; ax[1]=300;ay[1]=600
1391 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1392 ax[0]=150;ay[0]=450; ax[1]=450;ay[1]=450
1393 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1394 return 0
1395 }
1396 return 0 // space / unknown -> just advance
1397}
1398
1399// closed 16-point ellipse ring stroked as a polyline (O, 0, Q, 8-bowls). cx,cy center; rx,ry radii (em).
1400// 16 points (matches the 16-gon join discs -> one consistent trig table -> smooth, no wobble).
1401func fe_ring(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
1402 cx: i64, cy: i64, rx: i64, ry: i64, lw: i64, ox: i64, oy: i64, S: i64) -> i64 {
1403 let rx2: *i64 = sys_mmap(8*20) as *i64
1404 let ry2: *i64 = sys_mmap(8*20) as *i64
1405 var k: i64 = 0
1406 while k < 16 {
1407 rx2[k] = cx + (rx * cs[k]) / 1000
1408 ry2[k] = cy + (ry * sn[k]) / 1000
1409 k = k + 1
1410 }
1411 rx2[16] = rx2[0]
1412 ry2[16] = ry2[0]
1413 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, rx2,ry2,17, lw, ox,oy,S)
1414 return 0
1415}
1416
1417// which chars have a vector glyph (headings switch to the vector font only when a whole run qualifies)
1418func font_has(ch: i64) -> i64 {
1419 if ch == 32 { return 1 }
1420 if ch >= 97 { if ch <= 122 { return 1 } } // a-z
1421 if ch >= 65 { if ch <= 90 { return 1 } } // A-Z
1422 if ch >= 48 { if ch <= 57 { return 1 } } // 0-9
1423 if ch == 46 { return 1 }
1424 if ch == 44 { return 1 }
1425 if ch == 45 { return 1 }
1426 if ch == 58 { return 1 }
1427 if ch == 33 { return 1 }
1428 if ch == 63 { return 1 }
1429 if ch == 40 { return 1 }
1430 if ch == 41 { return 1 }
1431 if ch == 39 { return 1 }
1432 if ch == 47 { return 1 }
1433 return 0
1434}
1435func font_run_has(text: *u8, len: i64) -> i64 {
1436 var i: i64 = 0
1437 while i < len { if font_has(text[i] & 0xff) == 0 { return 0 } i = i + 1 }
1438 return 1
1439}
1440// EXTENDED coverage (the glyphs added for the browser: % [ ] ; " = +). Used ONLY by the GUI/atlas fast
1441// path so the EXACT gate path (font_run_has above) is byte-unchanged -> no NishiOS/OCR/polish regression.
1442func font_has_ext(ch: i64) -> i64 {
1443 if font_has(ch) == 1 { return 1 }
1444 if ch == 37 { return 1 }
1445 if ch == 91 { return 1 }
1446 if ch == 93 { return 1 }
1447 if ch == 59 { return 1 }
1448 if ch == 34 { return 1 }
1449 if ch == 61 { return 1 }
1450 if ch == 43 { return 1 }
1451 return 0
1452}
1453func font_run_has_ext(text: *u8, len: i64) -> i64 {
1454 var i: i64 = 0
1455 while i < len { if font_has_ext(text[i] & 0xff) == 0 { return 0 } i = i + 1 }
1456 return 1
1457}
1458// fill the 16-step trig table (22.5deg steps) that BOTH gs_disc (round joins/caps on every glyph) AND
1459// fe_ring use -- one consistent table. ★BUG FIXED: the old 12-entry init left cs[12..15] uninitialized,
1460// so the 16-gon join discs read garbage -> malformed joins = the heading stroke WOBBLE. Now 16 clean.
1461func font_trig_init(cs: *i64, sn: *i64) -> i64 {
1462 cs[0]=1000; sn[0]=0
1463 cs[1]=924; sn[1]=383
1464 cs[2]=707; sn[2]=707
1465 cs[3]=383; sn[3]=924
1466 cs[4]=0; sn[4]=1000
1467 cs[5]=0-383; sn[5]=924
1468 cs[6]=0-707; sn[6]=707
1469 cs[7]=0-924; sn[7]=383
1470 cs[8]=0-1000;sn[8]=0
1471 cs[9]=0-924; sn[9]=0-383
1472 cs[10]=0-707;sn[10]=0-707
1473 cs[11]=0-383;sn[11]=0-924
1474 cs[12]=0; sn[12]=0-1000
1475 cs[13]=383; sn[13]=0-924
1476 cs[14]=707; sn[14]=0-707
1477 cs[15]=924; sn[15]=0-383
1478 return 0
1479}
1480// ---- VECTOR METRICS for the measured layout (heading runs, scale sc): em size 15*sc px ----
1481func font_vec_adv_px(ch: i64, sc: i64) -> i64 { return (font_adv_em(ch) * 15 * sc) / 1000 }
1482// ---- ABSOLUTE-PX METRIC PAIR (2026-08-25) ----
1483// font_vec_adv_px above assumes a 15px em and takes a SCALE. The LAYOUT works in absolute
1484// font-size px, so nx_layout_block grew its OWN private copy of the same arithmetic
1485// (_layout_vec_text_w: font_adv_em(ch)*fs/1000). Two copies of one metric is precisely how a
1486// measurer and a breaker drift apart -- and they had: layout reserved VECTOR widths for a run
1487// (_layout_vec_text_w) and then wrapped that same run with the 8x8 BITMAP break function,
1488// because the wrap branch had only a measured arm and a legacy arm and no vector arm at all.
1489// Every wrapped run therefore BROKE at bitmap positions and PAINTED at vector ones, which is
1490// the visible gap around styled inline spans and links the inline-vec-measure gate names.
1491// ONE metric below, used by BOTH the width sum and the break scan, makes that disagreement
1492// impossible by construction instead of by discipline.
1493func font_vec_adv_fs(ch: i64, fs: i64) -> i64 { return (font_adv_em(ch) * fs) / 1000 }
1494func font_vec_text_w_fs(text: *u8, start: i64, end: i64, fs: i64) -> i64 {
1495 var w: i64 = 0
1496 var i: i64 = start
1497 var prev: i64 = 0 - 1
1498 if fs_face_adv != 0 { // BR30: a face measures CODEPOINTS (kern stays an ASCII-pair table)
1499 let cpo: *i64 = font_cp_scratch()
1500 while i < end { i = font_next_cp(text, i, end, cpo); let cc: i64 = cpo[0]; w = w + font_vec_adv_fs(cc, fs) + (font_pair_kern_em(prev, cc) * fs) / 1000; prev = cc }
1501 return w
1502 }
1503 while i < end { let c: i64 = (text[i] as i64) & 255; w = w + font_vec_adv_fs(c, fs) + (font_pair_kern_em(prev, c) * fs) / 1000; prev = c; i = i + 1 }
1504 return w
1505}
1506// Word-wrap counterpart of font_vec_text_w_fs: the SAME per-char advance, breaking at the last
1507// space that still fits. It always consumes at least one character, so a column narrower than a
1508// single glyph cannot spin the caller's while-loop forever.
1509func font_vec_next_break_fs(text: *u8, len: i64, avail_px: i64, start: i64, fs: i64) -> i64 {
1510 if start >= len { return len }
1511 var w: i64 = 0
1512 var i: i64 = start
1513 var brk: i64 = 0 - 1
1514 var prev: i64 = 0 - 1
1515 if fs_face_adv != 0 { // BR30: break positions stay BYTE indices; the advance is per CODEPOINT
1516 let cpo: *i64 = font_cp_scratch()
1517 while i < len {
1518 let i0: i64 = i
1519 i = font_next_cp(text, i, len, cpo)
1520 let cc: i64 = cpo[0]
1521 w = w + font_vec_adv_fs(cc, fs) + (font_pair_kern_em(prev, cc) * fs) / 1000
1522 prev = cc
1523 if w > avail_px {
1524 if brk > start { return brk + 1 }
1525 if i0 > start { return i0 }
1526 return i // always consume one whole codepoint, never split a sequence
1527 }
1528 if cc == 32 { brk = i0 }
1529 }
1530 return len
1531 }
1532 while i < len {
1533 let c: i64 = (text[i] as i64) & 255
1534 w = w + font_vec_adv_fs(c, fs) + (font_pair_kern_em(prev, c) * fs) / 1000
1535 prev = c
1536 if w > avail_px {
1537 if brk > start { return brk + 1 }
1538 if i > start { return i }
1539 return start + 1
1540 }
1541 if ((text[i] as i64) & 255) == 32 { brk = i }
1542 i = i + 1
1543 }
1544 return len
1545}
1546func font_vec_text_w(text: *u8, start: i64, end: i64, sc: i64) -> i64 {
1547 var w: i64 = 0
1548 var i: i64 = start
1549 var prev: i64 = 0 - 1
1550 while i < end { let c: i64 = text[i] & 0xff; w = w + font_vec_adv_px(c, sc) + (font_pair_kern_em(prev, c) * 15 * sc) / 1000; prev = c; i = i + 1 }
1551 return w
1552}
1553func font_vec_next_break(text: *u8, len: i64, avail_px: i64, start: i64, sc: i64) -> i64 {
1554 if start >= len { return len }
1555 var w: i64 = 0
1556 var i: i64 = start
1557 var brk: i64 = 0 - 1
1558 var prev: i64 = 0 - 1
1559 while i < len {
1560 let c: i64 = text[i] & 0xff
1561 w = w + font_vec_adv_px(c, sc) + (font_pair_kern_em(prev, c) * 15 * sc) / 1000
1562 prev = c
1563 if w > avail_px {
1564 if brk > start { return brk + 1 }
1565 if i > start { return i }
1566 return start + 1
1567 }
1568 if (text[i] & 0xff) == 32 { brk = i }
1569 i = i + 1
1570 }
1571 return len
1572}
1573
1574// lay out + emit a whole string at (ox,oy) grid, scale S. Returns the pen x after the string.
1575func font_text(text: *u8, len: i64, xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64,
1576 cs: *i64, sn: *i64, ox: i64, oy: i64, S: i64) -> i64 {
1577 var pen: i64 = ox
1578 var i: i64 = 0
1579 while i < len {
1580 let ch: i64 = text[i] & 0xff
1581 // optical spacing: shift each glyph so its measured optical left edge sits at the target sidebearing
1582 let sh: i64 = (((((font_sp_shift_disp(ch) * font_ws()) / 100) * font_wdth()) / 100) * S) / 1000
1583 font_emit(ch, xs, ys, cstart, clen, np, nc, cs, sn, pen + sh, oy, S)
1584 pen = pen + font_adv(ch, S)
1585 i = i + 1
1586 }
1587 return pen
1588}