(import rl "vendor:raylib") ;; raylib's Font family, headless. ;; ;; A previous lane refused this whole family by name and gave a real reason: a ;; Font is three ints beside a Texture2D, a Rectangle* and a GlyphInfo*, a ;; GlyphInfo embeds an Image, and that was two more aggregates and two owned ;; arrays "for something with no headless test at the end of it". The second ;; half of that turned out to be wrong, and this file is the counter-example. ;; ;; **Nothing here makes a font.** That is the whole trick. Every raylib call ;; that produces one needs something a headless run has not got — ;; get-font-default needs init-window, load-font and load-font-ex need a TTF ;; on disk and a GL context to upload the atlas to — so this file *builds* a ;; Font out of Flan arrays, field by field, and hands it to raylib to compute ;; with. raylib's text measuring is pure CPU arithmetic: it walks the glyph ;; array for a codepoint, takes the advance out of the glyph or the width out ;; of the atlas rectangle, and scales by the base size. ;; ;; Which makes this the scalars-in/fields-out shape rather than the ;; store-and-return one. There is no raylib-produced struct anywhere in the ;; file for a permutation to cancel against: Flan chose every field, and what ;; comes back is a number raylib worked out from them. ;; ;; What is pinned, and by what: ;; ;; - **base-size** by the scale factor. Measuring "AB" at size 10 against a ;; base size of 10 is scale 1 and at size 20 is scale 2, and both are ;; asserted, so the field cannot be a constant that happens to fit. ;; - **glyph-count** by the search bound. 'Z' is not in the font, and ;; raylib's miss falls back to index 0 rather than reading past the array. ;; - **recs and glyphs** as pointers, by being read through at all. ;; - **advance-x against offset-x**, because glyph C deliberately has an ;; advance of 0: raylib then falls back to the atlas rectangle's width ;; plus offset-x, so measuring "ABC" reaches three more fields than ;; measuring "AB" does. ;; - **where the Texture2D sits inside the Font**, by a trap in raylib ;; rather than a choice here: MeasureTextEx returns (0,0) at once when ;; `texture.id` is 0. The hand-built font claims an id of 1 — there is no ;; texture — and if the texture landed anywhere else in the struct the id ;; would read 0 and every measurement below would collapse to zero. ;; ;; And what is NOT pinned, which matters as much: `glyph-padding` is read by ;; nothing raylib computes on the CPU, `offset-y` only moves a glyph when one ;; is actually drawn, and of each atlas rectangle only `width` is ever looked ;; at — `x`, `y` and `height` come back from get-glyph-atlas-rec exactly as ;; they were stored, which is the symmetric trap and proves nothing. Those ;; four rest on raylib's header and on sand.flan looking right. This is the ;; same limit raylib-ffi.flan records for Texture2D's width, height and ;; mipmaps, and it is written down for the same reason. ;; ;; The numbers are chosen so that no two are equal: base size 10, glyph count ;; 3, padding 2, advances 11 and 13, offsets 1, 2 and 3, atlas widths 5, 7 ;; and 9. Everything prints exactly — no trigonometry, so unlike the rotated ;; camera in raylib-ffi.flan this can be compared as text. ;; The atlas rectangles, one per glyph. x and width differ per glyph so that ;; a fallback to the wrong index is a wrong number; height is the same 10 for ;; all three because raylib never reads it here. (defvar glyph-recs [3 rl/Rectangle]) ;; The glyphs themselves. `image` is raylib's own pixels for the glyph and is ;; left zeroed — it is present so the four ints in front of it are at the ;; right offsets and so a GlyphInfo is 40 bytes rather than 16. That claim is ;; checked: moving `image` to the front of the defstruct shifts the four ints ;; by 24 bytes, and the glyph search then finds nothing — every index reads 0 ;; and glyph C answers with A's numbers. (defvar glyphs [3 rl/GlyphInfo]) (defn build-glyphs [] (set (at glyph-recs 0) (rl/Rectangle {:x 0.0 :y 0.0 :width 5.0 :height 10.0})) (set (at glyph-recs 1) (rl/Rectangle {:x 5.0 :y 0.0 :width 7.0 :height 10.0})) (set (at glyph-recs 2) (rl/Rectangle {:x 12.0 :y 0.0 :width 9.0 :height 10.0})) ;; 65 66 67 are A B C. Advances 11 and 13 for the first two; C's advance is ;; 0 on purpose, which is what sends raylib down the other branch. (set (at glyphs 0) (rl/GlyphInfo {:value 65 :offset-x 1 :offset-y 0 :advance-x 11 :image (rl/Image {})})) (set (at glyphs 1) (rl/GlyphInfo {:value 66 :offset-x 2 :offset-y 0 :advance-x 13 :image (rl/Image {})})) (set (at glyphs 2) (rl/GlyphInfo {:value 67 :offset-x 3 :offset-y 0 :advance-x 0 :image (rl/Image {})}))) (defn show-bool [name string b bool] (print-str name) (print-str " ") (print-line (if b "yes" "no"))) (defn show-i [name string v i32] (print-str name) (print-str " ") (print-i64 (i64 v)) (newline)) (defn show-v [name string v rl/Vector2] (print-str name) (print-str " ") (print-f64 (f64 (.x v))) (print-str " ") (print-f64 (f64 (.y v))) (newline)) (defn show-rect [name string r rl/Rectangle] (print-str name) (print-str " ") (print-f64 (f64 (.x r))) (print-str " ") (print-f64 (f64 (.y r))) (print-str " ") (print-f64 (f64 (.width r))) (print-str " ") (print-f64 (f64 (.height r))) (newline)) (defn main [] i32 (rl/set-trace-log-level :warning) (build-glyphs) ;; The font. `texture` is a lie in every field but `id`, and the id is 1 for ;; the reason in the header note: at 0, raylib refuses to measure anything. ;; ;; It is built here rather than by a helper because a `defn` cannot say it ;; returns one: the parser decides whether the form after a parameter list ;; is a return type or the first body expression by looking the name up in ;; the set of types THIS FILE declares, and a package's types are not in it ;; — imports are resolved after parsing. `(defn the-font [] rl/Font ...)` ;; therefore parses rl/Font as an expression and fails with "unknown name". ;; That is a parser limit and not something this file wanted; it applies to ;; rl/Vector2 just as much as to rl/Font. (let [f (rl/Font {:base-size 10 :glyph-count 3 :glyph-padding 2 :texture (rl/Texture2D {:id 1 :width 32 :height 16 :mipmaps 1 :format 7}) :recs (addr (at glyph-recs 0)) :glyphs (addr (at glyphs 0))})] ;; font-valid? reads the texture id and both array pointers, so it is a ;; null check on the three things everything below dereferences. (show-bool "valid" (rl/font-valid? f)) ;; ── The search ────────────────────────────────────────────────────── ;; ;; A linear walk of `glyph-count` entries comparing `value`. Three hits at ;; three different indices, and one miss: 'Z' is not in the font and ;; raylib answers index 0 rather than running off the end. Shrink ;; glyph-count — or land the field somewhere else — and 'C' misses too. (show-i "index A" (rl/get-glyph-index f 65)) (show-i "index B" (rl/get-glyph-index f 66)) (show-i "index C" (rl/get-glyph-index f 67)) (show-i "index Z" (rl/get-glyph-index f 90)) ;; ── Reading back through the two pointers ─────────────────────────── ;; ;; The atlas rectangle for B is the second entry of `recs`, and the glyph ;; for C is the third of `glyphs`. Both are indexed by the search above, ;; so an exchange of the two pointers is a Rectangle read as a GlyphInfo. (show-rect "atlas B" (rl/get-glyph-atlas-rec f 66)) (let [g (rl/get-glyph-info f 67)] (show-i "glyph C value" (.value g)) (show-i "glyph C offset" (.offset-x g)) (show-i "glyph C advance" (.advance-x g))) ;; ── Measuring, which is where the arithmetic is ───────────────────── ;; ;; "AB" is 11 + 13 = 24 wide and base-size tall, at scale 1. (show-v "measure AB" (rl/measure-text-ex f "AB" 10.0 0.0)) ;; "ABC" adds C, whose advance is 0, so raylib falls back to the atlas ;; width plus the offset: 9 + 3 = 12, for 36. This line and the one above ;; disagree by exactly the amount that proves the fallback branch ran, and ;; that branch is the only thing in the file that reads Rectangle.width ;; out of the recs array — exchange x and width in the Rectangle defstruct ;; and this line reads 39 while the one above stays 24. (show-v "measure ABC" (rl/measure-text-ex f "ABC" 10.0 0.0)) ;; The same string at twice the size and a spacing of 3. Scale is 20/10, ;; so 24 doubles to 48, and spacing is added once per gap and not once per ;; glyph: 48 + 3 = 51. Exchange base-size and glyph-count and the scale ;; becomes 20/3 and the search bound becomes 10 — both wrong, in different ;; directions, on the same line. (show-v "measure AB big" (rl/measure-text-ex f "AB" 20.0 3.0)) ;; One glyph, so there is no gap for the spacing to land in: 11 at scale ;; 1, and the 3 does not appear. Without this line a spacing added per ;; glyph rather than per gap would pass everything above. (show-v "measure A spaced" (rl/measure-text-ex f "A" 10.0 3.0))) 0)