diff --git a/src/layout/symbol.zig b/src/layout/symbol.zig index db634b4..0d44610 100644 --- a/src/layout/symbol.zig +++ b/src/layout/symbol.zig @@ -237,6 +237,11 @@ fn wrapLines( /// `line_height_em`, the block anchored per the spec's text-anchor /// vocabulary. Returns the block's collision box, or null when nothing /// inked. +/// Codepoints a layout wanted and the atlas did not hold, so a host can fetch +/// them and lay the run out again. The set, not the count: what a host needs is +/// which characters to bake. +pub const Misses = std.AutoArrayHashMapUnmanaged(u21, void); + pub fn layoutText( gpa: std.mem.Allocator, text: []const u8, @@ -244,10 +249,23 @@ pub fn layoutText( opts: TextOpts, common: Common, quads: *std.ArrayList(types.Quad), + misses: ?*Misses, ) !?Box { const scale = opts.size_px / glyph_em_px; const em = opts.size_px; + // Every codepoint the run wants, before the wrap drops any of it: a label + // cut short for want of a glyph would otherwise never ask for the glyph + // that would have let it fit. + if (misses) |set| { + var scan = std.unicode.Utf8View.initUnchecked(text).iterator(); + while (scan.nextCodepoint()) |cp| { + if (cp == '\n' or cp == ' ') continue; + if (atlas.get(cp) != null) continue; + set.put(gpa, cp, {}) catch {}; + } + } + var lines: [max_lines][]const u8 = undefined; const n_lines = wrapLines(text, atlas, scale, opts.max_width_em * em, &lines); if (n_lines == 0) return null; diff --git a/src/map.zig b/src/map.zig index f1e3ab4..14b6746 100644 --- a/src/map.zig +++ b/src/map.zig @@ -95,6 +95,13 @@ pub const Built = struct { /// missing-image hook: the host renders them (tile57_render_symbol_run /// for sounding digit runs), calls Sprite.addImage, and rebuilds. missing_images: []const []const u8 = &.{}, + /// Codepoints a label wanted and the glyph atlas did not hold, + /// deduplicated — the missing-glyph hook, the text twin of + /// missing_images: the host bakes them into the atlas (from whatever face + /// its platform has for the script) and rebuilds. Without it a chart + /// naming its features in a script the atlas has no glyphs for lays out + /// half-em blanks and nothing says why. + missing_glyphs: []const u21 = &.{}, /// Features that failed a paint evaluation and fell to defaults. eval_errors: usize = 0, /// Per-layer properties the compiled tier claimed. The rest keep @@ -756,6 +763,7 @@ pub fn buildSceneWithRasters( // cell is remembered as NO_PATTERN so the sheet is walked once per name. var pattern_ids: std.StringHashMapUnmanaged(u32) = .empty; var missing: std.StringArrayHashMapUnmanaged(void) = .empty; + var missing_glyphs: symbol.Misses = .empty; var collider = symbol.Collider.init(arena); // Projection of a world anchor to reference px for collision boxes; the // view origin stands in for the screen center (they coincide for a @@ -969,7 +977,7 @@ pub fn buildSceneWithRasters( .depth = feat_depth, .size_scale = view.size_scale, .zoom = view.zoom, - }, &quads, &quad_paint, &text_scratch, &text_paint_scratch, &collider, &missing, &out.eval_errors); + }, &quads, &quad_paint, &text_scratch, &text_paint_scratch, &collider, &missing, &missing_glyphs, &out.eval_errors); continue; }, .fill => { @@ -1146,6 +1154,7 @@ pub fn buildSceneWithRasters( out.paint_zoom = view.zoom; out.patterns = patterns.items; out.missing_images = missing.keys(); + out.missing_glyphs = missing_glyphs.keys(); return out; } @@ -1433,6 +1442,7 @@ pub fn concatScenes(arena: std.mem.Allocator, parts: []const ScenePart) !Built { var patterns: std.ArrayList(types.PatternCell) = .empty; var spans: std.ArrayList(PaintSpan) = .empty; var missing: std.StringArrayHashMapUnmanaged(void) = .empty; + var missing_glyphs: symbol.Misses = .empty; for (parts) |part| { const b = part.built; @@ -1513,6 +1523,7 @@ pub fn concatScenes(arena: std.mem.Allocator, parts: []const ScenePart) !Built { spans.appendAssumeCapacity(moved); } for (b.missing_images) |name| try missing.put(arena, name, {}); + for (b.missing_glyphs) |cp| try missing_glyphs.put(arena, cp, {}); if (b.background_set) { out.background = b.background; out.background_set = true; @@ -1541,6 +1552,7 @@ pub fn concatScenes(arena: std.mem.Allocator, parts: []const ScenePart) !Built { out.patterns = patterns.items; out.paint_spans = spans.items; out.missing_images = missing.keys(); + out.missing_glyphs = missing_glyphs.keys(); return out; } @@ -1625,6 +1637,7 @@ fn layoutSymbolFeature( text_paint_scratch: *std.ArrayList(types.PaintVertex), collider: *symbol.Collider, missing: *std.StringArrayHashMapUnmanaged(void), + missing_glyphs: *symbol.Misses, errors: *usize, ) error{OutOfMemory}!void { if (f.parts.len == 0 or f.parts[0].len == 0) return; @@ -1740,7 +1753,7 @@ fn layoutSymbolFeature( tcommon.rotate_deg = 0; } const scratch_before = text_scratch.items.len; - const box = (try symbol.layoutText(arena, text, ga, topts, tcommon, text_scratch)) orelse break :text; + const box = (try symbol.layoutText(arena, text, ga, topts, tcommon, text_scratch, missing_glyphs)) orelse break :text; const text_allow = sc.sym.text_allow_overlap; const sbox = scaledBox(box, px, py, sc.size_scale); // An allow-overlap label always draws and is never gated; a