A prebuilt tool, bin/ludic-ui-preview (built by ludic-dev build, shipped beside ludic-lsp, run as `ludic ui-preview [--font DIR]`): ludic.ui with a backend that records every draw call as a line, and mock component classes the studio describes (load / model / calls). frame t runs ui_show at interface time t; text is measured on the CPU with the game's font.json metrics; locale goes through ludic.i18n; tree / box / rules inspect the frame. No game code, no GPU, no network. The wire protocol is frozen as tools/ui-preview/protocol-v1.md; smoke.txt is a transcript to run. ludic.ui gains, all additive: UiClass.make_of, UiBackend.said / emitted, UiRule.text / at (with comment line breaks kept so rule lines count true, and a class's styles read under its .lss path), and ui_root, ui_find, ui_rules_of, ui_rule_value, ui_building, ui_class, ui_class_load, ui_file_forget, ui_errors_clear; ui_nine_cuts_into exported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
2.9 KiB
Text
80 lines
2.9 KiB
Text
# font.ludic - text measured as ludic.render3d's overlay measures it (overlay_font, ov_text_w), from
|
|
# the same font.json and with no GPU: a glyph's advance times the size in whole pixels, summed, and
|
|
# the sum cut to a whole pixel. With no font, a character is half the size wide.
|
|
const PV_MAP_N: int = 8192
|
|
@alloc_ok("a font read: once per font line")
|
|
function pv_font_load(pv: mut PvState, dir: string) -> bool {
|
|
let meta = Fs.read_text(dir + "/font.json")
|
|
if meta == null { return false }
|
|
let j = Json.parse(meta)
|
|
if Value.kind(j) != 6 or Value.has(j, "adv") == 0 { return false }
|
|
let adv = Value.get(j, "adv")
|
|
let n = Value.count(adv)
|
|
var first = 32
|
|
if Value.has(j, "first") != 0 { first = Value.as_int(Value.get(j, "first")) }
|
|
let codes = Value.get(j, "codes")
|
|
pv.fn_adv = new []float
|
|
pv.fn_map = new []int
|
|
pv.fn_hi_cp = new []int
|
|
pv.fn_hi_g = new []int
|
|
for i in 0 .. n { push(pv.fn_adv, Value.as_float(Value.at(adv, i))) }
|
|
for c in 0 .. PV_MAP_N { push(pv.fn_map, 0) }
|
|
for i in 0 .. n {
|
|
var cp = first + i
|
|
if Value.kind(codes) == 5 and i < Value.count(codes) { cp = Value.as_int(Value.at(codes, i)) }
|
|
if cp >= 0 and cp < PV_MAP_N { pv.fn_map[cp] = i + 1 } else if cp >= PV_MAP_N {
|
|
push(pv.fn_hi_cp, cp)
|
|
push(pv.fn_hi_g, i)
|
|
}
|
|
}
|
|
pv.fn_space = 0
|
|
if pv.fn_map[32] > 0 { pv.fn_space = pv.fn_map[32] - 1 }
|
|
pv.fn_qmark = pv.fn_space
|
|
if pv.fn_map[63] > 0 { pv.fn_qmark = pv.fn_map[63] - 1 }
|
|
pv.font_dir = dir
|
|
pv.fn_on = true
|
|
return true
|
|
}
|
|
# the glyph for a code point, as ov_glyph: a control character is a space, a missing one is '?'
|
|
function pv_glyph(pv: PvState, cp: int) -> int {
|
|
if cp < 32 { return pv.fn_space }
|
|
if cp < PV_MAP_N {
|
|
let g = pv.fn_map[cp]
|
|
if g > 0 { return g - 1 }
|
|
return pv.fn_qmark
|
|
}
|
|
for i in 0 .. len(pv.fn_hi_cp) {
|
|
if pv.fn_hi_cp[i] == cp { return pv.fn_hi_g[i] }
|
|
}
|
|
return pv.fn_qmark
|
|
}
|
|
# the bytes the UTF-8 sequence at s[i] takes, as ov_u8 reads it (a broken one is one byte)
|
|
function pv_u8_len(s: string, i: int) -> int {
|
|
let n = len(s)
|
|
let b0 = s[i] & 255
|
|
if b0 >= 240 and b0 < 248 and i + 3 < n { return 4 }
|
|
if b0 >= 224 and b0 < 240 and i + 2 < n { return 3 }
|
|
if b0 >= 192 and b0 < 224 and i + 1 < n { return 2 }
|
|
return 1
|
|
}
|
|
function pv_u8_cp(s: string, i: int, k: int) -> int {
|
|
let b0 = s[i] & 255
|
|
if k == 4 { return ((b0 & 7) << 18) | ((s[i + 1] & 63) << 12) | ((s[i + 2] & 63) << 6) | (s[i + 3] & 63) }
|
|
if k == 3 { return ((b0 & 15) << 12) | ((s[i + 1] & 63) << 6) | (s[i + 2] & 63) }
|
|
if k == 2 { return ((b0 & 31) << 6) | (s[i + 1] & 63) }
|
|
if b0 >= 128 { return 63 }
|
|
return b0
|
|
}
|
|
# the width of s at `size` pixels a em, in whole pixels
|
|
function pv_measure(pv: PvState, s: string, size: float) -> float {
|
|
let px = float(int(size))
|
|
var w = 0.0
|
|
var i = 0
|
|
let n = len(s)
|
|
while i < n {
|
|
let k = pv_u8_len(s, i)
|
|
if pv.fn_on { w = w + pv.fn_adv[pv_glyph(pv, pv_u8_cp(s, i, k))] * px } else { w = w + 0.5 * px }
|
|
i += k
|
|
}
|
|
return float(int(w))
|
|
}
|